checkpoint - untested - major refactor to stores

This commit is contained in:
2023-08-15 23:52:14 -04:00
parent b781cb3b4d
commit 97c12f1c33
31 changed files with 276 additions and 603 deletions

8
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"dependencies": {
"@semaphore-protocol/group": "^3.10.1",
"@semaphore-protocol/identity": "^3.10.1",
"discreetly-interfaces": "^0.1.32",
"discreetly-interfaces": "^0.1.34",
"libsodium-wrappers": "^0.7.11",
"poseidon-lite": "^0.2.0",
"qr-scanner": "^1.4.2",
@@ -1905,9 +1905,9 @@
}
},
"node_modules/discreetly-interfaces": {
"version": "0.1.32",
"resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.32.tgz",
"integrity": "sha512-qATarb8KuU5IiZ1CQTGbERSL7y3fLhVFFdQ7Yvofeu47lL4e8EFzmzabJYXRabA+QgtTAPqbrazJ7TVXfwZXUw==",
"version": "0.1.34",
"resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.34.tgz",
"integrity": "sha512-7purPOWOowVH44ebdweBdZ4z2RsBQy5/H7xi6PdsHkaw1xwg8u3Ev2US5EdavP1igZ+SzebJdK8jT0ZTjzX8Kg==",
"dependencies": {
"poseidon-lite": "^0.2.0",
"rlnjs": "^3.1.4"

View File

@@ -44,7 +44,7 @@
"dependencies": {
"@semaphore-protocol/group": "^3.10.1",
"@semaphore-protocol/identity": "^3.10.1",
"discreetly-interfaces": "^0.1.32",
"discreetly-interfaces": "^0.1.34",
"libsodium-wrappers": "^0.7.11",
"poseidon-lite": "^0.2.0",
"qr-scanner": "^1.4.2",

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Button from './button.svelte';
import type { ButtonI } from '../types';
import type { ButtonI } from '$lib/types';
export let title: string;
export let buttons: ButtonI[] = [];
</script>

View File

@@ -3,9 +3,10 @@ import { Group } from '@semaphore-protocol/group';
import getMessageHash from './messageHasher';
import getRateCommitmentHash from './rateCommitmentHasher';
import type { Identity } from '@semaphore-protocol/identity';
import type { MessageI, RoomI } from 'discreetly-interfaces';
import type { MessageI } from 'discreetly-interfaces';
import type { RoomI } from '$lib/types';
import type { RLNFullProof, MerkleProof } from 'rlnjs';
import { getMerkleProof } from '../services/bandada';
import { getMerkleProof } from '$lib//services/bandada';
const wasmPath = '/rln/circuit.wasm';
const zkeyPath = '/rln/final.zkey';

View File

@@ -1,54 +0,0 @@
import type { ConfigurationI, IdentityStoreI } from '../types/';
import { IdentityStoreE } from '../types/';
import type { ServerI } from 'discreetly-interfaces';
import { storable, sessionable } from './storeFactory';
/**
* @description List of server URLs, this is mainly for bootstraping the app
*/
export const serverListStore = storable([] as string[], 'servers');
interface serverDataStoreI {
[key: string]: ServerI;
}
/**
* @description Server Metadata keyed by the server's URL
*/
export const serverDataStore = storable({} as serverDataStoreI, 'serverData');
/**
* @description The URL of the currently selected server
*/
export const selectedServer = storable('', 'selectedServer');
/**
* @description The room ID of the currently selected room
*/
export const selectedRoom = storable('', 'selectedRoom');
export const roomsStore = storable(
{
selectedRoomId: undefined,
roomsData: {}
},
'roomsStore'
);
export const configStore = storable(
{
signUpStatus: {
inviteAccepted: false,
identityBackedUp: false
},
identityStore: IdentityStoreE.NO_IDENTITY
} as ConfigurationI,
'signupStatus'
);
// Session store (removed after the session is cleared) of the last 500 messages or so of each room the user participates in; rooms they don't have selected will not be updated
export const messageStore = sessionable({}, 'messages');
// Stores the user's identity // TODO THIS NEEDS TO BE AN ENCRYPTED SEMAPHORE IDENTITY IN THE FUTURE
export const identityStore = storable({} as IdentityStoreI, 'identity');
serverListStore;

View File

@@ -0,0 +1,14 @@
import type { ConfigurationI } from './types';
import { IdentityStoreE } from './types';
const discreetlyURL = 'https://server.discreetly.chat/';
export const defaultServers = { discreetlyURL: { name: 'Discreetly Server', url: discreetlyURL } };
export const configDefaults: ConfigurationI = {
signUpStatus: {
inviteAccepted: false,
identityBackedUp: false
},
identityStore: IdentityStoreE.NO_IDENTITY
};

View File

@@ -25,8 +25,7 @@ function joinUrlParts(parts: string[] | string): string {
/**
* @description - makes a get request to the api
* @param {string} baseUrl - the base url of the api
* @param {string} endpoint - the endpoint to be added to the base url
* @param {string[] | string} urlParts - the url parts to be joined to form the url
* @returns {object} - the response from the api
* @throws {Error} - if the request fails
*/
@@ -46,8 +45,7 @@ export async function get(urlParts: string[] | string): Promise<object> {
/**
* @description - makes a get request to the api
* @param {string} baseUrl - the base url of the api
* @param {string} endpoint - the endpoint to be added to the base url
* @param {string[] | string} urlParts - the url parts to be joined to form the url
* @param {object} data - the data to be sent to the api
* @returns {object} - the response from the api
* @throws {Error} - if the request fails

View File

@@ -1,4 +1,6 @@
import type { RoomI, ServerI } from 'discreetly-interfaces';
import type { ServerI } from 'discreetly-interfaces';
import type { RoomI } from '$lib/types';
import { get, post } from './api';
export async function getIdentityRoomIds(server: string, idCommitment: string): Promise<string[]> {

60
src/lib/stores/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import type { ConfigurationI, IdentityStoreI } from '$lib/types';
import type { MessageI, ServerI } from 'discreetly-interfaces';
import type { RoomI } from '$lib/types';
import { storable, sessionable } from './storeFactory';
import { configDefaults } from '$lib/defaults';
export interface serverStoreI {
[key: string]: ServerI;
}
interface selectedRoomStoreI {
[key: string]: string;
}
interface roomStoreI {
[key: string]: RoomI;
}
interface messageStoreI {
[key: string]: MessageI[];
}
/* ------------------ Server State ------------------*/
/**
* @description Server Metadata keyed by the server's URL
*/
export const serverStore = storable({} as serverStoreI, 'serverData');
/**
* @description The URL of the currently selected server
*/
export const selectedServer = storable('' as string, 'selectedServer');
/* ------------------ Room State ------------------*/
/**
* @description Room information keyed by the roomId
*/
export const roomsStore = storable({} as roomStoreI, 'roomsStore');
/**
* @description The room ID of the currently selected room keyed by the server's URL
*/
export const selectedRoom = storable({} as selectedRoomStoreI, 'selectedRoom');
/**
* @description Session store (removed after the session is cleared) of the last 500 messages or so of each room the user participates in; rooms they don't have selected will not be updated
*/
export const messageStore = sessionable({} as messageStoreI, 'messages');
/* ------------------ Misc State ------------------*/
/**
* @description Configuration and misc state
*/
export const configStore = storable(configDefaults as ConfigurationI, 'configStore');
//TODO! This needs to be encrypted
/**
* @description Identity store, this is the user's identity
*/
export const identityStore = storable({} as IdentityStoreI, 'identity');

33
src/lib/stores/servers.ts Normal file
View File

@@ -0,0 +1,33 @@
import { get, type Writable } from 'svelte/store';
import type { serverStoreI } from '.';
import { serverStore, roomsStore } from '.';
import { getServerData } from '$lib/services/server';
import type { RoomI } from '$lib/types';
export function getServerList(store: Writable<serverStoreI> = serverStore): string[] {
return Object.keys(get(store));
}
export function getServerRooms(url: string, store: Writable<serverStoreI> = serverStore): RoomI[] {
let roomIds = get(store)[url].rooms;
if (!roomIds) {
roomIds = [];
}
return roomIds.map((roomId) => {
return get(roomsStore)[roomId];
}) as RoomI[];
}
export async function updateServer(
url: string,
store: Writable<serverStoreI> = serverStore
): Promise<void> {
const oldServerStore = get(store);
getServerData(url).then((newServerData) => {
const newServerStore = {
...oldServerStore,
[url]: newServerData
};
store.set(newServerStore);
});
}

View File

@@ -1,4 +1,5 @@
import type { Identity } from '@semaphore-protocol/identity';
import type { RoomI as RI } from 'discreetly-interfaces';
export interface ButtonI {
link: string;
@@ -8,14 +9,14 @@ export interface ButtonI {
// For rooms where a user has a unique identity
export interface RoomIdentityI {
[key: string]: Identity; // The key is the room id (bigint) as a string
[key: string]: Identity; // The key is the roomId (bigint) as a string
}
export interface IdentityStoreI {
identity: Identity;
}
export interface ServerListI {
export interface ServerUrlI {
name: string;
url: string;
}
@@ -41,3 +42,7 @@ export interface ConfigurationI {
signUpStatus: SignUpStatusI;
identityStore: IdentityStoreE;
}
export interface RoomI extends RI {
server?: string;
}

View File

@@ -1,48 +1,11 @@
import type { RoomI, ServerI } from 'discreetly-interfaces';
import { identityStore, serverDataStore, serverListStore, roomsStore } from './data/stores';
import type { RoomI } from '$lib/types';
import { identityStore, serverStore, roomsStore } from '$lib/stores';
import { get } from 'svelte/store';
import { getIdentityRoomIds, getRoomById, getServerData } from './services/server';
import type { ServerListI } from './types';
import { getIdentityRoomIds, getRoomById } from '$lib/services/server';
const defaultServers = [
{ name: 'Discreetly Server', url: 'https://server.discreetly.chat/' },
{ name: 'Localhost', url: 'http://localhost:3001/api/' } as ServerListI
];
// TODO! EVERYTHING IN THIS FILE SHOULD BE DEPRECATED AND MOVED TO PROPER LOCATIONS
export async function updateServers(): Promise<{ [key: string]: ServerI }> {
const serverList = get(serverListStore);
// If the server list is empty or doesn't have the discreetly server, add the default servers
if (
serverList.length < 1 ||
!serverList.find((s: ServerListI) => s.name === 'Discreetly Server')
) {
console.error('serverListStore is empty');
serverListStore.set(defaultServers);
}
const newServerData: { [key: string]: ServerI } = {};
await Promise.all(
serverList.map(async (server: ServerListI) => {
console.log(`Fetching server data from ${server.url}`);
const data = await getServerData(server.url);
console.log(`Setting server data for ${server.url}`);
if (data) {
newServerData[server.url] = { ...data };
}
})
);
serverDataStore.update((store: { [key: string]: ServerI } = {}) => ({
...store,
...newServerData
}));
return newServerData;
}
export async function setRooms(server: string, roomIds: string[] = []): Promise<string[]> {
export async function __setRooms(server: string, roomIds: string[] = []): Promise<string[]> {
const rooms: RoomI[] = [];
for (const roomId of roomIds) {
const result = await getRoomById(server, roomId);
@@ -67,7 +30,7 @@ export async function setRooms(server: string, roomIds: string[] = []): Promise<
return rooms.map((r: RoomI) => r.name);
}
export async function setSelectedRoomId(selectedRoomId: string): Promise<void> {
export async function __setSelectedRoomId(selectedRoomId: string): Promise<void> {
roomsStore.update(() => {
const roomsStoreData = get(roomsStore);
return {
@@ -77,7 +40,7 @@ export async function setSelectedRoomId(selectedRoomId: string): Promise<void> {
});
}
export async function updateRooms(
export async function __updateRooms(
selectedServer: string,
roomIds: string[] = []
): Promise<string[]> {
@@ -96,8 +59,8 @@ export async function updateRooms(
rooms.forEach((r: RoomI) => {
acceptedRoomNames = [...acceptedRoomNames, r.name];
});
serverDataStore.update(() => {
const serverData = get(serverDataStore);
serverStore.update(() => {
const serverData = get(serverStore);
serverData[selectedServer] = {
...serverData[selectedServer],
rooms
@@ -107,10 +70,10 @@ export async function updateRooms(
return acceptedRoomNames;
}
export async function updateSingleRoom(selectedServer: string, roomId: string) {
export async function __updateSingleRoom(selectedServer: string, roomId: string) {
const room = await getRoomById(selectedServer, roomId);
serverDataStore.update(() => {
const serverData = get(serverDataStore);
serverStore.update(() => {
const serverData = get(serverStore);
serverData[selectedServer] = {
...serverData[selectedServer],
rooms: [room]
@@ -119,13 +82,13 @@ export async function updateSingleRoom(selectedServer: string, roomId: string) {
});
}
export function getServerForSelectedRoom(): any {
export function __getServerForSelectedRoom(): any {
const roomsStoreData = get(roomsStore);
const selectedServer = roomsStoreData.roomsData[roomsStoreData.selectedRoomId]?.server;
return selectedServer;
}
export function getRoomsForServer(selectedServer: string): [] {
export function __getRoomsForServer(selectedServer: string): [] {
const roomsStoreData = get(roomsStore);
const rooms: any = Object.values(roomsStoreData.roomsData);
return rooms.filter((room: any) => room.server === selectedServer);

3
src/lib/utils/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import RateLimiter from './rateLimit';
export { RateLimiter };

View File

@@ -1,8 +1,21 @@
class RateLimiter {
numberMessages: number;
milliSecondsPerEpoch: number;
export interface State {
currentEpoch: number;
lastEpochMessageWasSent: number;
remainingMessages: number;
}
export interface EpochDetails {
epoch: number;
timestamp: number; // Unix epoch time
local: 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;
@@ -10,15 +23,11 @@ class RateLimiter {
this.remainingMessages = this.numberMessages;
}
public getCurrentEpoch() {
return Math.floor(new Date().getTime() / this.milliSecondsPerEpoch);
getCurrentEpoch(): number {
return Math.floor(Date.now() / this.milliSecondsPerEpoch);
}
public updateState(): {
currentEpoch: number;
lastEpochMessageWasSent: number;
remainingMessages: number;
} {
private updateState(): State {
const currentEpoch = this.getCurrentEpoch();
if (currentEpoch > this.lastEpochMessageWasSent) {
this.remainingMessages = this.numberMessages;
@@ -31,31 +40,22 @@ class RateLimiter {
};
}
public getRemainingMessages() {
public getRemainingMessages(): number {
this.updateState();
return this.remainingMessages;
}
// Returns the number of remaining messages
// -1 means you have no more messages left
public useMessage(): number {
this.updateState();
if (this.remainingMessages > 0) {
this.remainingMessages--;
return this.remainingMessages;
} else {
return -1;
}
return this.remainingMessages > 0 ? this.remainingMessages : -1;
}
public getEpochFromTimestamp(timestamp: number): {
epoch: number;
timestamp: number; // Unix epoch time
local: string;
} {
const _timestamp = timestamp ? new Date(timestamp).toString() : Date.now().toString();
const epoch = Math.floor(Number(_timestamp) / this.milliSecondsPerEpoch);
const local = new Date(_timestamp).toLocaleString('en-US', {
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
@@ -63,8 +63,8 @@ class RateLimiter {
return { epoch, timestamp, local };
}
public getTimestampFromEpoch(epoch?: number | bigint): string {
const time = epoch ? Number(epoch) * this.milliSecondsPerEpoch : new Date();
public getTimestampFromEpoch(epoch: number = this.getCurrentEpoch()): string {
const time = epoch * this.milliSecondsPerEpoch;
return new Date(time).toLocaleString('en-US', {
hour: 'numeric',
minute: 'numeric',

View File

@@ -0,0 +1,45 @@
import { defaultServers } from '$lib/defaults';
import { getServerData } from '$lib/services/server';
import { serverStore } from '$lib/stores';
import type { ServerUrlI } from '$lib/types';
import type { ServerI } from 'discreetly-interfaces';
import { get } from 'svelte/store';
// Function to update a single server
export async function updateServer(server: ServerUrlI): Promise<ServerI | null> {
console.log(`Fetching server data from ${server.url}`);
const data = await getServerData(server.url);
console.log(`Setting server data for ${server.url}`);
if (data) {
serverStore.update((store: { [key: string]: ServerI } = {}) => ({
...store,
[server.url]: data
}));
return data;
}
return null;
}
// Function to update all servers
export async function updateAllServers(): Promise<{ [key: string]: ServerI }> {
const serverList = Object.keys(get(serverStore));
// If the server list is empty or doesn't have the discreetly server, add the default servers
if (serverList.length < 1) {
console.error('serverStore is empty, populating with');
serverStore.set(defaultServers);
}
const newServerData: { [key: string]: ServerI } = {};
await Promise.all(
serverList.map(async (server: ServerUrlI) => {
const data = await updateServer(server);
if (data) {
newServerData[server.url] = data;
}
})
);
return newServerData;
}

View File

@@ -8,8 +8,8 @@
import AppHeader from './AppHeader.svelte';
import AppFooter from './AppFooter.svelte';
import Loading from '$lib/components/loading.svelte';
import { serverListStore, selectedServer } from '$lib/data/stores';
import { updateServers } from '$lib/utils';
import { serverListStore, selectedServer } from '$lib/stores';
import { updateServers } from '$lib/stores/servers';
// Hack to get BigInt <-> JSON compatibility
(BigInt.prototype as any).toJSON = function () {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { identityStore } from '$lib/data/stores';
import { identityStore } from '$lib/stores';
import { onMount } from 'svelte';
import Loading from '$lib/components/loading.svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { identityStore } from '$lib/data/stores';
import { identityStore } from '$lib/stores';
import { AppBar } from '@skeletonlabs/skeleton';
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { AppBar } from '@skeletonlabs/skeleton';
import { LightSwitch } from '@skeletonlabs/skeleton';
import { identityStore } from '$lib/data/stores';
import { identityStore } from '$lib/stores';
</script>
<AppBar background="bg-surface-50-800-token">

View File

@@ -1,15 +1,15 @@
<script lang="ts">
import { serverDataStore } from '$lib/data/stores';
import { updateServers } from '$lib/utils';
import { serverStore } from '$lib/stores';
import { updateServers } from '$lib/stores/servers';
import { onMount } from 'svelte';
onMount(() => {
if (!Object.keys($serverDataStore).length) {
if (!Object.keys($serverStore).length) {
updateServers();
}
});
</script>
{#if Object.keys($serverDataStore).length}
{#if Object.keys($serverStore).length}
<slot />
{/if}

View File

@@ -1,211 +0,0 @@
<script lang="ts">
import ChatRoom from './ChatRoom.svelte';
import Sidebar from './Sidebar.svelte';
import { onMount, onDestroy } from 'svelte';
import { Modal, modalStore } from '@skeletonlabs/skeleton';
import type { ModalSettings } from '@skeletonlabs/skeleton';
import type { RoomI, MessageI } from 'discreetly-interfaces';
import {
identityStore,
messageStore,
serverDataStore,
serverListStore,
roomsStore
} from '$lib/data/stores';
import { io } from 'socket.io-client';
import { genProof } from '$lib/crypto/prover';
import { Identity } from '@semaphore-protocol/identity';
import RateLimiter from '$lib/rateLimit';
import {
getRoomsForServer,
getServerForSelectedRoom,
updateServers,
setSelectedRoomId
} from '$lib/utils';
const setRoom = (roomId: string) => {
if (roomId) {
setSelectedRoomId(roomId);
}
};
let messageText = '';
let connected: boolean = false;
let rateManager: RateLimiter;
let currentEpoch: number = 0;
let messagesLeft: number = 0;
const selectedRoomsServer = getServerForSelectedRoom();
let serverSelection = selectedRoomsServer;
$: selectedRoomId = $roomsStore.selectedRoomId;
$: selectedRoomData = $roomsStore.roomsData[selectedRoomId];
$: roomListForServer = getRoomsForServer(serverSelection) as RoomI[];
$: () => {
if (!$messageStore[selectedRoomId]) {
$messageStore[selectedRoomId] = { messages: [] };
}
};
$: sendButtonText = messagesLeft > 0 ? 'Send (' + messagesLeft + ' left)' : 'X';
$: inRoom = true; // TODO fix this: $identityStore.rooms.hasOwnProperty(selectedRoom);
$: canSendMessage = inRoom && connected;
let elemChat: HTMLElement;
// For some reason, eslint thinks ScrollBehavior is undefined...
// eslint-disable-next-line no-undef
function scrollChatBottom(behavior: ScrollBehavior = 'smooth'): void {
setTimeout(() => {
elemChat.scrollTo({ top: elemChat.scrollHeight, behavior });
}, 1);
}
const socketURL: string = selectedRoomsServer;
const socket = io(socketURL);
function sendMessage() {
if (!connected) {
console.debug('Not connected to chat server');
return;
}
if (messageText.length === 0) {
console.debug('Message is empty');
return;
}
const identity = new Identity($identityStore.toString());
const messageID = rateManager.useMessage();
if (messageID == -1) {
console.debug('Rate limit exceeded');
return;
} else {
messagesLeft = messageID;
}
genProof(selectedRoomData, messageText, identity).then((msg) => {
socket.emit('validateMessage', msg);
console.debug('Sending message: ', msg);
messageText = '';
});
scrollChatBottom();
}
function onPromptKeydown(event: KeyboardEvent): void {
if (['Enter'].includes(event.code)) {
event.preventDefault();
sendMessage();
}
}
const addServerModal: ModalSettings = {
type: 'prompt',
// Data
title: 'Enter Server Address',
body: 'Provide the server address.',
// Populates the input value and attributes
value: 'http://discreetly.chat/',
valueAttr: { type: 'url', required: true },
// Returns the updated response value
response: (r: string) => {
console.log('response:', r);
if ($serverListStore.includes(r)) {
console.warn('Server already exists');
return;
}
$serverListStore.push({ url: r, name: 'LOADING...' + r });
$serverDataStore = updateServers();
}
};
onMount(() => {
rateManager = new RateLimiter(selectedRoomData.userMessageLimit, selectedRoomData.rateLimit);
scrollChatBottom('instant');
socket.on('connect', () => {
connected = true;
const engine = socket.io.engine;
engine.once('upgrade', () => {
console.debug('Upgraded connection to', engine.transport.name);
});
engine.on('close', (reason) => {
console.debug('socket-io-transport-closed', reason);
});
socket.emit('joiningRoom', selectedRoomData?.roomId);
});
socket.on('disconnected', () => {
connected = false;
console.debug('disconnected');
});
socket.on('connect_error', (err) => {
console.debug('chat connection error', err.message);
});
socket.on('connect_timeout', (err) => {
console.debug('chat connection timeout', err.message);
});
socket.on('error', (err) => {
console.debug('chat websocket error', err.message);
});
socket.on('messageBroadcast', (data: MessageI) => {
console.debug('Received Message: ', data);
const roomId = data.roomId?.toString();
if (roomId) {
if (!$messageStore[roomId]) {
console.debug('Creating room in message store', roomId);
$messageStore[roomId] = { messages: [] };
}
$messageStore[roomId].messages = [data, ...$messageStore[roomId].messages.reverse()].slice(
0,
500
);
scrollChatBottom();
}
});
setInterval(() => {
currentEpoch = rateManager.getCurrentEpoch();
messagesLeft = rateManager.getRemainingMessages();
}, 1000);
});
onDestroy(() => {
socket.emit('leavingRoom', selectedRoomData?.roomId);
socket.disconnect();
});
</script>
<section id="chat-wrapper" class="bg-surface-100-800-token">
<!-- Navigation -->
<Sidebar />
<!-- Chat -->
<ChatRoom />
</section>
<style>
#chat-wrapper {
height: 100%;
display: grid;
grid-template-columns: minmax(25%, 200px) 1fr;
grid-template-rows: auto;
grid-template-areas: 'sidebar chat';
}
#sidebar {
grid-area: sidebar;
}
#chat {
max-height: calc(100vh - 101px);
grid-area: chat;
}
#conversation {
overflow: scroll;
}
</style>

View File

@@ -1,49 +1,32 @@
<script lang="ts">
import Sidebar from './Sidebar.svelte';
import { onMount, onDestroy } from 'svelte';
import { Modal, modalStore } from '@skeletonlabs/skeleton';
import type { ModalSettings } from '@skeletonlabs/skeleton';
import type { RoomI, MessageI } from 'discreetly-interfaces';
import type { MessageI } from 'discreetly-interfaces';
import {
identityStore,
messageStore,
serverDataStore,
serverListStore,
roomsStore
} from '$lib/data/stores';
roomsStore,
selectedRoom,
selectedServer
} from '$lib/stores';
import { io } from 'socket.io-client';
import type { Socket } from 'socket.io-client';
import { genProof } from '$lib/crypto/prover';
import { Identity } from '@semaphore-protocol/identity';
import RateLimiter from '$lib/rateLimit';
import {
getRoomsForServer,
getServerForSelectedRoom,
updateServers,
setSelectedRoomId
} from '$lib/utils';
import RateLimiter from '$lib/utils/rateLimit';
import { __getRoomsForServer, __getServerForSelectedRoom, __setSelectedRoomId } from '$lib/utils';
const setRoom = (roomId: string) => {
if (roomId) {
setSelectedRoomId(roomId);
}
};
let messageText = '';
let connected: boolean = false;
let rateManager: RateLimiter;
let currentEpoch: number = 0;
let messagesLeft: number = 0;
const selectedRoomsServer = getServerForSelectedRoom();
let serverSelection = selectedRoomsServer;
$: selectedRoomId = $roomsStore.selectedRoomId;
$: selectedRoomData = $roomsStore.roomsData[selectedRoomId];
$: roomListForServer = getRoomsForServer(serverSelection) as RoomI[];
let socket: Socket;
$: selectedRoomId = $selectedRoom[$selectedServer];
$: selectedRoomData = $roomsStore[selectedRoomId];
$: () => {
if (!$messageStore[selectedRoomId]) {
$messageStore[selectedRoomId] = { messages: [] };
$messageStore[selectedRoomId] = [];
}
};
@@ -61,10 +44,6 @@
}, 1);
}
const socketURL: string = selectedRoomsServer;
const socket = io(socketURL);
function sendMessage() {
if (!connected) {
console.debug('Not connected to chat server');
@@ -98,28 +77,9 @@
}
}
const addServerModal: ModalSettings = {
type: 'prompt',
// Data
title: 'Enter Server Address',
body: 'Provide the server address.',
// Populates the input value and attributes
value: 'http://discreetly.chat/',
valueAttr: { type: 'url', required: true },
// Returns the updated response value
response: (r: string) => {
console.log('response:', r);
if ($serverListStore.includes(r)) {
console.warn('Server already exists');
return;
}
$serverListStore.push({ url: r, name: 'LOADING...' + r });
$serverDataStore = updateServers();
}
};
onMount(() => {
rateManager = new RateLimiter(selectedRoomData.userMessageLimit, selectedRoomData.rateLimit);
socket = io($selectedServer);
rateManager = new RateLimiter(selectedRoomData.userMessageLimit!, selectedRoomData.rateLimit!);
scrollChatBottom('instant');
socket.on('connect', () => {
connected = true;
@@ -159,12 +119,9 @@
if (roomId) {
if (!$messageStore[roomId]) {
console.debug('Creating room in message store', roomId);
$messageStore[roomId] = { messages: [] };
$messageStore[roomId] = [] as MessageI[];
}
$messageStore[roomId].messages = [data, ...$messageStore[roomId].messages.reverse()].slice(
0,
500
);
$messageStore[roomId] = [data, ...$messageStore[roomId].reverse()].slice(0, 500);
scrollChatBottom();
}
});
@@ -198,12 +155,12 @@
<!-- Conversation -->
<section id="conversation" bind:this={elemChat} class="p-4 overflow-y-auto space-y-4">
{#if $messageStore[selectedRoomId]}
{#each $messageStore[selectedRoomId].messages.reverse() as bubble}
{#each $messageStore[selectedRoomId].reverse() as bubble}
<div class="flex">
<div class="card p-4 space-y-2 bg-surface-200-700-token">
<header class="flex justify-between items-center">
<small class="opacity-50 text-primary-500"
>{rateManager.getTimestampFromEpoch(bubble.epoch)}</small
>{rateManager.getTimestampFromEpoch(Number(bubble.epoch))}</small
>
<small class="opacity-50 text-primary-500">epoch: {bubble.epoch}</small>
</header>

View File

@@ -1,100 +1,22 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { Modal, modalStore } from '@skeletonlabs/skeleton';
import type { ModalSettings } from '@skeletonlabs/skeleton';
import type { RoomI, MessageI } from 'discreetly-interfaces';
import {
identityStore,
messageStore,
serverDataStore,
serverListStore,
roomsStore
} from '$lib/data/stores';
import { io } from 'socket.io-client';
import { genProof } from '$lib/crypto/prover';
import { Identity } from '@semaphore-protocol/identity';
import RateLimiter from '$lib/rateLimit';
import {
getRoomsForServer,
getServerForSelectedRoom,
updateServers,
setSelectedRoomId
} from '$lib/utils';
import type { RoomI } from 'discreetly-interfaces';
import { serverStore, selectedServer, selectedRoom } from '$lib/stores';
import { __getRoomsForServer, __getServerForSelectedRoom, __setSelectedRoomId } from '$lib/utils';
import { getServerList, getServerRooms, updateServer } from '$lib/stores/servers';
const setRoom = (roomId: string) => {
if (roomId) {
setSelectedRoomId(roomId);
__setSelectedRoomId(roomId);
}
};
let messageText = '';
let connected: boolean = false;
let rateManager: RateLimiter;
let currentEpoch: number = 0;
let messagesLeft: number = 0;
const selectedRoomsServer = getServerForSelectedRoom();
const selectedRoomsServer = __getServerForSelectedRoom();
let serverSelection = selectedRoomsServer;
$: selectedRoomId = $roomsStore.selectedRoomId;
$: selectedRoomData = $roomsStore.roomsData[selectedRoomId];
$: roomListForServer = getRoomsForServer(serverSelection) as RoomI[];
$: () => {
if (!$messageStore[selectedRoomId]) {
$messageStore[selectedRoomId] = { messages: [] };
}
};
$: sendButtonText = messagesLeft > 0 ? 'Send (' + messagesLeft + ' left)' : 'X';
$: inRoom = true; // TODO fix this: $identityStore.rooms.hasOwnProperty(selectedRoom);
$: canSendMessage = inRoom && connected;
let elemChat: HTMLElement;
// For some reason, eslint thinks ScrollBehavior is undefined...
// eslint-disable-next-line no-undef
function scrollChatBottom(behavior: ScrollBehavior = 'smooth'): void {
setTimeout(() => {
elemChat.scrollTo({ top: elemChat.scrollHeight, behavior });
}, 1);
}
const socketURL: string = selectedRoomsServer;
const socket = io(socketURL);
function sendMessage() {
if (!connected) {
console.debug('Not connected to chat server');
return;
}
if (messageText.length === 0) {
console.debug('Message is empty');
return;
}
const identity = new Identity($identityStore.toString());
const messageID = rateManager.useMessage();
if (messageID == -1) {
console.debug('Rate limit exceeded');
return;
} else {
messagesLeft = messageID;
}
genProof(selectedRoomData, messageText, identity).then((msg) => {
socket.emit('validateMessage', msg);
console.debug('Sending message: ', msg);
messageText = '';
});
scrollChatBottom();
}
function onPromptKeydown(event: KeyboardEvent): void {
if (['Enter'].includes(event.code)) {
event.preventDefault();
sendMessage();
}
}
$: roomListForServer = getServerRooms($selectedServer) as RoomI[];
const addServerModal: ModalSettings = {
type: 'prompt',
@@ -107,75 +29,13 @@
// Returns the updated response value
response: (r: string) => {
console.log('response:', r);
if ($serverListStore.includes(r)) {
if (getServerList().includes(r)) {
console.warn('Server already exists');
return;
}
$serverListStore.push({ url: r, name: 'LOADING...' + r });
$serverDataStore = updateServers();
updateServer(r);
}
};
onMount(() => {
rateManager = new RateLimiter(selectedRoomData.userMessageLimit, selectedRoomData.rateLimit);
scrollChatBottom('instant');
socket.on('connect', () => {
connected = true;
const engine = socket.io.engine;
engine.once('upgrade', () => {
console.debug('Upgraded connection to', engine.transport.name);
});
engine.on('close', (reason) => {
console.debug('socket-io-transport-closed', reason);
});
socket.emit('joiningRoom', selectedRoomData?.roomId);
});
socket.on('disconnected', () => {
connected = false;
console.debug('disconnected');
});
socket.on('connect_error', (err) => {
console.debug('chat connection error', err.message);
});
socket.on('connect_timeout', (err) => {
console.debug('chat connection timeout', err.message);
});
socket.on('error', (err) => {
console.debug('chat websocket error', err.message);
});
socket.on('messageBroadcast', (data: MessageI) => {
console.debug('Received Message: ', data);
const roomId = data.roomId?.toString();
if (roomId) {
if (!$messageStore[roomId]) {
console.debug('Creating room in message store', roomId);
$messageStore[roomId] = { messages: [] };
}
$messageStore[roomId].messages = [data, ...$messageStore[roomId].messages.reverse()].slice(
0,
500
);
scrollChatBottom();
}
});
setInterval(() => {
currentEpoch = rateManager.getCurrentEpoch();
messagesLeft = rateManager.getRemainingMessages();
}, 1000);
});
onDestroy(() => {
socket.emit('leavingRoom', selectedRoomData?.roomId);
socket.disconnect();
});
</script>
<div id="sidebar" class="hidden lg:grid grid-rows-[auto_1fr_auto] border-r border-surface-500/30">
@@ -189,7 +49,7 @@
serverSelection = event.target?.value;
}}
>
{#each Object.entries($serverDataStore) as [key, s]}
{#each Object.entries($serverStore) as [key, s]}
<option value={key}>{s.name}</option>
{/each}
</select>
@@ -211,7 +71,7 @@
}}
>
{#each roomListForServer as room}
{#if room.roomId == selectedRoomId}
{#if room.roomId == $selectedRoom[$selectedServer]}
<option value={room.roomId} title={room.roomId ? room.roomId.toString() : ''} selected
>{room.name}</option
>

View File

@@ -1,15 +1,13 @@
<script lang="ts">
import { identityStore, selectedServer, serverListStore } from '$lib/data/stores';
import { identityStore, selectedServer, serverStore } from '$lib/stores';
import DeleteIdentity from './DeleteIdentity.svelte';
import BackupIdentity from './BackupIdentity.svelte';
import RestoreIdentity from './RestoreIdentity.svelte';
import { Identity } from '@semaphore-protocol/identity';
import Join from '../signup/Join.svelte';
import { updateRooms } from '$lib/utils';
import type { ServerListI } from '$lib/types';
import { __updateRooms } from '$lib/utils';
import { RadioGroup, RadioItem } from '@skeletonlabs/skeleton';
$: serverList = $serverListStore as ServerListI[];
import { getServerList } from '$lib/stores/servers';
let identityExists = false;
$: if ($identityStore.identity == undefined) {
@@ -27,7 +25,7 @@
function refreshRooms() {
console.log('Refreshing rooms');
updateRooms($selectedServer);
__updateRooms($selectedServer);
}
function createIdentity(regenerate = false) {
@@ -64,12 +62,12 @@
display="flex-col"
hover="hover:variant-soft-primary"
>
{#each serverList as server}
{#each getServerList() as serverUrl}
<RadioItem
on:change={selectServer}
bind:group={$selectedServer}
name="server"
value={server.url}>{server.name}</RadioItem
value={$serverStore[serverUrl]}>{$serverStore[serverUrl].name}</RadioItem
>
{/each}
</RadioGroup>

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { identityStore } from '$lib/data/stores';
import { identityStore } from '$lib/stores';
import Loading from '$lib/components/loading.svelte';
import QRCode from 'qrcode';
import { Identity } from '@semaphore-protocol/identity';
import type { Identity } from '@semaphore-protocol/identity';
let loading: boolean = false;
let imageUrl: string | undefined = undefined;

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { identityStore } from '$lib/data/stores';
import { identityStore } from '$lib/stores';
import type { IdentityStoreI } from '$lib/types';
import { onMount } from 'svelte';
@@ -9,7 +8,7 @@
function deleteIdentity() {
console.warn('DELETING IDENTITY');
$identityStore = { identity: null, rooms: {} } as IdentityStoreI;
$identityStore = { identity: {} } as IdentityStoreI;
}
onMount(() => {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { identityStore } from '$lib/data/stores';
import { identityStore } from '$lib/stores';
import { Identity } from '@semaphore-protocol/identity';
import QrScanner from 'qr-scanner';
import { FileDropzone } from '@skeletonlabs/skeleton';
@@ -18,7 +18,7 @@
console.log($identityStore.identity);
console.log(
'Identity restored from backup file with identity commitment:',
$identityStore.identity._commitment
$identityStore.identity.commitment
);
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { identityStore, signUpStatusStore } from '$lib/data/stores';
import { identityStore, signUpStatusStore } from '$lib/stores';
import Loading from '$lib/components/loading.svelte';
import { Stepper, Step } from '@skeletonlabs/skeleton';
import { Identity } from '@semaphore-protocol/identity';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { identityStore, selectedServer, configStore } from '$lib/data/stores';
import { setRooms } from '$lib/utils';
import { identityStore, selectedServer, configStore } from '$lib/stores';
import { __setRooms } from '$lib/utils';
import { postInviteCode } from '$lib/services/server';
import type { JoinResponseI } from '$lib/types';
@@ -13,7 +13,7 @@
const result = (await postInviteCode($selectedServer, { code: newCode, idc })) as JoinResponseI;
console.log('INVITE CODE RESPONSE: ', result);
if (result.status == 'valid' || result.status == 'already-added') {
acceptedRoomNames = await setRooms($selectedServer, result.roomIds);
acceptedRoomNames = await __setRooms($selectedServer, result.roomIds);
code = '';
$configStore.signUpStatus.inviteAccepted = true;
} else {

View File

@@ -2,18 +2,18 @@
import {
messageStore,
serverListStore,
serverDataStore,
serverStore,
selectedServer,
identityStore
} from '$lib/data/stores';
import { updateServers } from '$lib/utils';
} from '$lib/stores';
import { updateServers } from '$lib/stores/servers';
import { genProof } from '$lib/crypto/prover';
import { io } from 'socket.io-client';
import BackupIdentity from '../identity/BackupIdentity.svelte';
import DeleteIdentity from '../identity/DeleteIdentity.svelte';
import RestoreIdentity from '../identity/RestoreIdentity.svelte';
import { Identity } from '@semaphore-protocol/identity';
import RateLimiter from '$lib/rateLimit';
import RateLimiter from '$lib/utils/rateLimit';
import { onMount } from 'svelte';
let messageText = '';
@@ -51,7 +51,7 @@
function updateServerData() {
console.debug('UPDATING SERVERS');
$serverDataStore = updateServers();
$serverStore = updateServers();
if ($selectedServer.name == undefined) {
$selectedServer = $serverListStore[0].url;
}