refactored for better UI/UX

This commit is contained in:
2023-08-25 01:01:36 -04:00
parent 8768a7721f
commit b0c0834a1a
13 changed files with 843 additions and 446 deletions

876
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,14 +28,17 @@
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0",
"lightningcss": "^1.21.7",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"rollup-plugin-sizes": "^1.0.5",
"svelte": "^4.0.5",
"svelte-check": "^3.4.5",
"svelte-kit": "^1.2.0",
"tailwindcss": "^3.3.2",
"terser": "^5.19.2",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.3.6"
@@ -48,7 +51,7 @@
"@semaphore-protocol/group": "^3.10.1",
"@semaphore-protocol/identity": "^3.10.1",
"date-fns": "^2.30.0",
"discreetly-interfaces": "^0.1.37",
"discreetly-interfaces": "^0.1.38",
"libsodium-wrappers": "^0.7.11",
"poseidon-lite": "^0.2.0",
"qr-scanner": "^1.4.2",

View File

@@ -1,8 +1,10 @@
<script lang="ts">
import { selectedServer, selectedRoom, currentRoomsStore } from '$lib/stores';
import { updateMessages } from '$lib/utils';
function setRoom(roomId: string) {
$selectedRoom[$selectedServer] = roomId;
updateMessages($selectedServer, roomId);
}
</script>

View File

@@ -1,13 +1,13 @@
import { RLNProver } from 'rlnjs';
import { Group } from '@semaphore-protocol/group';
import type { MessageI } from 'discreetly-interfaces';
import type { MessageI, MessageInterfaces } from 'discreetly-interfaces';
import type { IdentityStoreI, RoomI } from '$lib/types';
import type { RLNFullProof, MerkleProof } from 'rlnjs';
import { getMerkleProof } from '$lib//services/bandada';
import { updateRooms } from '$lib/utils';
import { get } from 'svelte/store';
import { selectedServer, roomsStore } from '$lib/stores';
import { calculateSignalHash } from './signalHash';
import { calculateSignalHash } from 'discreetly-interfaces';
import getRateCommitmentHash from './rateCommitmentHasher';
const wasmPath = '/rln/circuit.wasm';
@@ -32,7 +32,19 @@ async function merkleProofFromRoom(
identityCommitment: bigint
) {
const roomFromStore = get(roomsStore)[roomId];
const identities = roomFromStore.identities ? roomFromStore.identities.map((i) => BigInt(i)) : [];
let identities: bigint[];
try {
identities = roomFromStore.identities
? roomFromStore.identities.map((i) => {
// This removes any non-numeric characters from the string
// In particular there was a bug where the `n` at the end of a bigint wasn't removed and it wasn't parsing correctly.
return BigInt(String(i).replace(/\D/g, ''));
})
: [];
} catch (err) {
console.debug(roomFromStore.identities);
throw new Error('Could not parse identities from room');
}
const group = new Group(RLN_IDENIFIER, 20, identities);
let mp: MerkleProof;
try {
@@ -55,17 +67,17 @@ async function merkleProofFromRoom(
/**
*
* @param room
* @param message
* @param identity
* @param epoch
* @param messageId
* @param messageLimit
* @param {RoomI} room
* @param {MessageInterfaces} message
* @param {IdentityStoreI} identity
* @param {bigint | number} epoch
* @param {number} messageId
* @param {number} messageLimit
* @returns Message with proof attached
*/
async function genProof(
room: RoomI,
message: string,
message: MessageInterfaces,
identity: IdentityStoreI,
epoch: bigint | number,
messageId: bigint | number,
@@ -78,7 +90,7 @@ async function genProof(
const userMessageLimit = BigInt(messageLimit);
const identitySecret = BigInt(identity._secret);
const identityCommitment = BigInt(identity._commitment);
const messageHash: bigint = calculateSignalHash(message);
const messageHash: bigint = calculateSignalHash(JSON.stringify(message));
const rateCommitment: bigint = getRateCommitmentHash(identityCommitment, userMessageLimit);
let merkleProof: MerkleProof;
@@ -93,8 +105,16 @@ async function genProof(
break;
case 'BANDADA_GROUP':
if (room.bandadaAddress === undefined) throw new Error('Bandada address not defined');
merkleProof = await getMerkleProof(room.bandadaAddress, rateCommitment);
throw new Error('Bandada not implemented yet');
try {
merkleProof = await getMerkleProof(
room.bandadaAddress,
room.bandadaGroupId!,
rateCommitment
);
break;
} catch (err) {
throw new Error('GetMerkleProof failed' + err);
}
case 'CONTRACT':
//TODO
throw new Error('RLN contracts not implemented yet');

View File

@@ -1,13 +0,0 @@
import { hexlify } from '@ethersproject/bytes';
import { toUtf8Bytes } from '@ethersproject/strings';
import { keccak256 } from '@ethersproject/keccak256';
/**
* Hashes a signal string with Keccak256.
* @param signal The RLN signal.
* @returns The signal hash.
*/
export function calculateSignalHash(signal: string): bigint {
const converted = hexlify(toUtf8Bytes(signal));
return BigInt(keccak256(converted)) >> BigInt(8);
}

View File

@@ -1,14 +1,14 @@
import type { MerkleProof } from 'rlnjs';
import type { BandadaGroupI } from 'discreetly-interfaces';
import { get } from './api';
// https://api.bandada.pse.dev/groups/{group}/members/{member}/proof
export function getMerkleProof(
bandadaGroup: BandadaGroupI,
bandadaServerAddress: string,
groupId: string,
identityCommitment: string | bigint
): Promise<MerkleProof> {
const endpoint = `groups/${bandadaGroup.groupID}/members/${identityCommitment}/proof`;
return get([bandadaGroup.url, endpoint])
const endpoint = `groups/${groupId}/members/${identityCommitment}/proof`;
return get([bandadaServerAddress, endpoint])
.then((res) => {
return res as MerkleProof;
})

View File

@@ -9,98 +9,34 @@ export interface State {
export interface EpochDetails {
epoch: number;
timestamp: number; // Unix epoch time
local: string;
relative: string;
}
// class RateLimiter {
// private numberMessages: number;
// private milliSecondsPerEpoch: number;
// private lastEpochMessageWasSent: number;
// private remainingMessages: number;
// constructor(numberMessages: number, milliSecondsPerEpoch: number) {
// this.numberMessages = numberMessages;
// this.milliSecondsPerEpoch = milliSecondsPerEpoch;
// this.lastEpochMessageWasSent = this.getCurrentEpoch();
// this.remainingMessages = this.numberMessages;
// }
// getCurrentEpoch(): number {
// return Math.floor(Date.now() / this.milliSecondsPerEpoch);
// }
// private updateState(): State {
// const currentEpoch = this.getCurrentEpoch();
// if (currentEpoch > this.lastEpochMessageWasSent) {
// this.remainingMessages = this.numberMessages;
// this.lastEpochMessageWasSent = currentEpoch;
// }
// return {
// currentEpoch,
// lastEpochMessageWasSent: this.lastEpochMessageWasSent,
// remainingMessages: this.remainingMessages
// };
// }
// public getRemainingMessages(): number {
// this.updateState();
// return this.remainingMessages;
// }
// public useMessage(): number {
// this.updateState();
// if (this.remainingMessages > 0) {
// this.remainingMessages--;
// }
// return this.remainingMessages > 0 ? this.remainingMessages : -1;
// }
// public getEpochFromTimestamp(timestamp: number = Date.now()): EpochDetails {
// const epoch = Math.floor(timestamp / this.milliSecondsPerEpoch);
// const local = new Date(timestamp).toLocaleString('en-US', {
// hour: 'numeric',
// minute: 'numeric',
// hour12: true
// });
// return { epoch, timestamp, local };
// }
// public getTimestampFromEpoch(epoch: number = this.getCurrentEpoch()): string {
// const time = epoch * this.milliSecondsPerEpoch;
// return new Date(time).toLocaleString('en-US', {
// hour: 'numeric',
// minute: 'numeric',
// hour12: true
// });
// }
// }
export function getEpochFromTimestamp(
ratelimit: number,
timestamp: number = Date.now()
): EpochDetails {
const epoch = Math.floor(timestamp / ratelimit);
const local = new Date(timestamp).toLocaleString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true
});
return { epoch, timestamp, local };
let relative = '';
try {
relative = formatRelative(new Date(timestamp), new Date());
} catch (err) {
relative = 'Unknown';
console.debug(`${err.message}: ${epoch} * ${ratelimit} = ${timestamp}`);
}
return { epoch, relative, timestamp };
}
export function getTimestampFromEpoch(
epoch: number,
ratelimit: number
): { DateString: string; unixEpochTime: number } {
let DateString = '';
let unixEpochTime = 0;
export function getTimestampFromEpoch(ratelimit: number, epoch: number): EpochDetails {
let relative = '';
let timestamp = 0;
try {
unixEpochTime = epoch * ratelimit;
DateString = formatRelative(new Date(unixEpochTime), new Date());
timestamp = epoch * ratelimit;
relative = formatRelative(new Date(timestamp), new Date());
} catch (err) {
DateString = 'Unknown';
console.debug(err);
relative = 'Unknown';
//console.debug(`${err.message}: ${epoch} * ${ratelimit} = ${timestamp}`);
}
return { DateString, unixEpochTime };
return { epoch, relative, timestamp };
}

View File

@@ -1,9 +1,10 @@
import type { RoomI } from '$lib/types';
import { roomsStore, selectedRoom, selectedServer, serverStore } from '$lib/stores';
import { roomsStore, selectedRoom, selectedServer, serverStore, messageStore } from '$lib/stores';
import { get } from 'svelte/store';
import {
getIdentityRoomIds as getRoomIdsByIdentityCommitment,
getRoomById
getRoomById,
getMessages
} from '$lib/services/server';
import { getCommitment } from '.';
@@ -69,3 +70,12 @@ export async function updateRooms(
}
return acceptedRoomNames;
}
export function updateMessages(server: string, roomId: string) {
getMessages(server, roomId).then((messages) => {
messageStore.update((store) => {
store[roomId] = messages;
return store;
});
});
}

View File

@@ -4,25 +4,54 @@
import InputPrompt from './InputPrompt.svelte';
import Conversation from './Conversation.svelte';
import { onMount, onDestroy } from 'svelte';
import { selectedServer, currentSelectedRoom, messageStore } from '$lib/stores';
import { selectedServer, currentSelectedRoom, messageStore, rateLimitStore } from '$lib/stores';
import { io } from 'socket.io-client';
import type { Socket } from 'socket.io-client';
import type { MessageI } from 'discreetly-interfaces';
import { getEpochFromTimestamp, getTimestampFromEpoch } from '$lib/utils';
import { getMessages } from '$lib/services/server';
import { getEpochFromTimestamp, getTimestampFromEpoch, updateMessages } from '$lib/utils';
import Loading from '$lib/components/loading.svelte';
let scrollChatToBottom: () => {};
let socket: Socket;
let connected: boolean = false;
let lastRoom = '';
$: currentEpoch = 0;
$: timeLeftInEpoch = '0';
$: userMessageLimit = $currentSelectedRoom.userMessageLimit ?? 1;
$: roomRateLimit = $currentSelectedRoom.rateLimit ?? 0;
$: if (!$rateLimitStore[$currentSelectedRoom.roomId!.toString()]) {
$rateLimitStore[$currentSelectedRoom.roomId!.toString()] = {
lastEpoch: currentEpoch,
messagesSent: 0
};
}
$: currentRateLimit = $rateLimitStore[$currentSelectedRoom.roomId!.toString()];
$: messagesLeft = () => {
if (currentRateLimit.lastEpoch !== currentEpoch) {
currentRateLimit.lastEpoch = currentEpoch;
currentRateLimit.messagesSent = 0;
return userMessageLimit;
} else {
return userMessageLimit - currentRateLimit.messagesSent;
}
};
$: messageId = userMessageLimit - messagesLeft();
$: try {
if (lastRoom) {
socket.emit('leavingRoom', lastRoom);
}
socket.emit('joiningRoom', $currentSelectedRoom?.roomId.toString());
console.debug('Joining room', $currentSelectedRoom?.roomId.toString());
} catch {
} finally {
}
function updateEpoch() {
currentEpoch = Math.floor(Date.now() / $currentSelectedRoom.rateLimit!);
timeLeftInEpoch = (
($currentSelectedRoom.rateLimit! -
(Date.now() -
getTimestampFromEpoch(currentEpoch, $currentSelectedRoom.rateLimit!).unixEpochTime)) /
getTimestampFromEpoch($currentSelectedRoom.rateLimit!, currentEpoch).timestamp)) /
1000
).toFixed(1);
}
@@ -41,7 +70,7 @@
console.debug('socket-io-transport-closed', reason);
});
socket.emit('joiningRoom', $currentSelectedRoom?.roomId);
socket.emit('joiningRoom', $currentSelectedRoom?.roomId.toString());
});
socket.on('disconnected', () => {
@@ -74,8 +103,8 @@
}
if (!data.epoch) {
data.epoch = getEpochFromTimestamp(
+data.timeStamp!,
$currentSelectedRoom.rateLimit!
$currentSelectedRoom.rateLimit!,
+data.timeStamp!
).epoch;
}
$messageStore[roomId] = [...$messageStore[roomId], data];
@@ -84,13 +113,15 @@
}
scrollChatToBottom();
}
socket.on('Members', (data: string) => {
console.log(data);
});
});
getMessages($selectedServer, $currentSelectedRoom?.roomId.toString()).then((messages) => {
console.log(messages);
$messageStore[$currentSelectedRoom?.roomId.toString()] = messages;
});
updateMessages($selectedServer, $currentSelectedRoom?.roomId.toString());
scrollChatToBottom();
setInterval(() => {
updateEpoch();
}, 100);
@@ -101,14 +132,33 @@
});
</script>
<div id="chat" class="grid grid-rows-[auto,1fr,auto]">
<!-- Header -->
<ChatRoomHeader {currentEpoch} {timeLeftInEpoch} />
<!-- Conversation -->
<Conversation bind:scrollChatBottom={scrollChatToBottom} />
<!-- Prompt -->
<InputPrompt {socket} {connected} {currentEpoch} />
</div>
{#if $currentSelectedRoom}
<div id="chat" class="grid grid-rows-[auto,1fr,auto]">
<!-- Header -->
<ChatRoomHeader
{currentEpoch}
{timeLeftInEpoch}
{userMessageLimit}
{messageId}
{messagesLeft}
{roomRateLimit}
/>
<!-- Conversation -->
<Conversation bind:scrollChatBottom={scrollChatToBottom} {roomRateLimit} />
<!-- Prompt -->
<InputPrompt
{socket}
{connected}
{currentEpoch}
{userMessageLimit}
{messageId}
{currentRateLimit}
{messagesLeft}
/>
</div>
{:else}
<Loading />
{/if}
<style>
#chat {

View File

@@ -1,11 +1,64 @@
<script lang="ts">
import { currentSelectedRoom } from '$lib/stores';
import { currentSelectedRoom, rateLimitStore } from '$lib/stores';
import { onMount } from 'svelte';
export let currentEpoch: number;
export let timeLeftInEpoch: string;
export let userMessageLimit: number;
export let roomRateLimit: number;
export let messagesLeft: () => number;
export let messageId: number;
$: roomId = $currentSelectedRoom?.roomId!.toString();
$: roomName = $currentSelectedRoom?.name ?? 'Select Room';
$: userMessageLimit = $currentSelectedRoom.userMessageLimit ?? 1;
$: currentRateLimit = $currentSelectedRoom.rateLimit ?? 0;
$: actions(messagesLeft(), userMessageLimit);
function actions(msgsRemaining: number, totalMsgs: number) {
const d = document.getElementById('ActionPoints');
let canvas = d?.querySelector('canvas') as HTMLCanvasElement;
const circleRadius = 8;
const circleSpacing = 5;
const startX = circleRadius;
if (!canvas) {
canvas = document.createElement('canvas');
canvas.height = 20;
d?.appendChild(canvas);
}
canvas.width = (circleRadius * 2 + circleSpacing) * totalMsgs - circleSpacing;
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear previous drawing
let x = startX;
const y = canvas.height / 2;
for (let i = 0; i < msgsRemaining; i++) {
ctx.fillStyle = msgsRemaining === 1 ? '#fa5f5f' : '#45a164';
ctx.strokeStyle = msgsRemaining === 1 ? '#bc4747' : '#34794b'; // Outline color
ctx.lineWidth = 1; // Outline width
ctx.beginPath();
ctx.arc(x, y, circleRadius, 0, Math.PI * 2, true);
ctx.fill();
ctx.stroke(); // Draw the outline
x += circleRadius * 2 + circleSpacing;
}
for (let i = msgsRemaining; i < totalMsgs; i++) {
ctx.fillStyle = '#73888a';
ctx.strokeStyle = '#1a1f1f'; // Outline color
ctx.lineWidth = 1; // Outline width
ctx.beginPath();
ctx.arc(x, y, circleRadius, 0, Math.PI * 2, true);
ctx.fill();
ctx.stroke(); // Draw the outline
x += circleRadius * 2 + circleSpacing;
}
}
onMount(() => {
actions(messagesLeft(), userMessageLimit);
});
</script>
<header
@@ -14,13 +67,17 @@
<h2 class="h5 text-primary-500" title={roomId}>
{roomName}
</h2>
<div class="hidden md:block text-xs md:text-sm">
<div class="hidden md:inline text-xs md:text-sm">
<small title={roomId}
>you can send {userMessageLimit} messages every {currentRateLimit / 1000} seconds</small
>you can send {userMessageLimit} messages every {roomRateLimit / 1000} seconds / {messageId} of
{messagesLeft()}
used</small
>
<br class="hidden md:inline" />
<small class="code" title={String(currentEpoch)}
>Epoch: {currentEpoch} / Time Left in Epoch: {timeLeftInEpoch}s</small
>
</div>
<div id="ActionPoints" />
</header>

View File

@@ -1,18 +1,32 @@
<script lang="ts">
import { currentRoomMessages, currentSelectedRoom } from '$lib/stores';
import { getTimestampFromEpoch } from '$lib/utils/rateLimit';
import { getEpochFromTimestamp, getTimestampFromEpoch } from '$lib/utils/rateLimit';
import type { MessageI } from 'discreetly-interfaces';
import { onMount } from 'svelte';
$: rateLimit = $currentSelectedRoom?.rateLimit!;
export let roomRateLimit: number;
let elemChat: HTMLElement;
// For some reason, eslint thinks ScrollBehavior is undefined...
// eslint-disable-next-line no-undef
export function scrollChatBottom(behavior: ScrollBehavior = 'smooth'): void {
export function scrollChatBottom(behavior: ScrollBehavior = 'smooth', delay = 1): void {
setTimeout(() => {
elemChat.scrollTo({ top: elemChat.scrollHeight, behavior });
}, 1);
}, delay);
}
function getTime(bubble: MessageI): string {
let r = getTimestampFromEpoch(roomRateLimit, Number(bubble.epoch)).relative;
if (r === 'Unknown') {
r = getEpochFromTimestamp(roomRateLimit, Number(bubble.timeStamp)).relative;
}
return r;
}
onMount(() => {
scrollChatBottom('smooth', 500);
});
</script>
<section
@@ -25,10 +39,14 @@
<div class="flex flex-col items-start">
<div class="card p-2 md:p-4 space-y-1 md:space-y-2 bg-surface-200-700-token">
<header class="flex justify-between items-center text-xs md:text-sm">
<small class="opacity-50 text-primary-500 mr-2 md:mr-4"
>{getTimestampFromEpoch(Number(bubble.epoch), rateLimit).DateString}</small
>
<small class="hidden md:block opacity-50 text-primary-500">epoch: {bubble.epoch}</small>
<small class="opacity-50 text-primary-500 mr-2 md:mr-4">{getTime(bubble)}</small>
{#if bubble.epoch}
<small class="hidden md:block opacity-50 text-primary-500"
>epoch: {bubble.epoch}</small
>
{:else}
<small class="hidden md:block opacity-70 text-error-500">SYSTEM MESSAGE</small>
{/if}
</header>
<p class="text-primary-500">{bubble.message}</p>
</div>

View File

@@ -8,28 +8,14 @@
export let socket: Socket;
export let connected: boolean;
export let currentEpoch: number;
export let userMessageLimit: number;
export let currentRateLimit: { lastEpoch: number; messagesSent: number };
export let messageId: number;
export let messagesLeft: () => number;
let messageText = '';
let sendingMessage: boolean = false;
$: if (!$rateLimitStore[$currentSelectedRoom.roomId!.toString()]) {
$rateLimitStore[$currentSelectedRoom.roomId!.toString()] = {
lastEpoch: currentEpoch,
messagesSent: 0
};
}
$: currentRateLimit = $rateLimitStore[$currentSelectedRoom.roomId!.toString()];
$: userMessageLimit = $currentSelectedRoom.userMessageLimit ?? 1;
$: messagesLeft = () => {
if (currentRateLimit.lastEpoch !== currentEpoch) {
currentRateLimit.lastEpoch = currentEpoch;
currentRateLimit.messagesSent = 0;
return userMessageLimit;
} else {
return userMessageLimit - currentRateLimit.messagesSent;
}
};
$: messageId = userMessageLimit - messagesLeft();
$: placeholderText = () => {
if (!connected) {
return 'Connecting...';

View File

@@ -1,6 +1,20 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import sizes from 'rollup-plugin-sizes';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()],
build: {
minify: 'terser',
cssMinify: 'lightningcss',
rollupOptions: {
plugins: [sizes()],
output: {
manualChunks: {
'@sveltejs/kit': ['@sveltejs/kit']
},
compact: true
}
}
}
});