diff --git a/app/src/database/index.ts b/app/src/database/index.ts index 344d7f8..ace4ca0 100644 --- a/app/src/database/index.ts +++ b/app/src/database/index.ts @@ -1,7 +1,8 @@ -import { openDB } from "idb"; +import { deleteDB, openDB } from "idb"; import { Group, Schema } from "./schema"; -export const openDatabase = openDB("meal", 1, { +const NAME = "meal"; +export const openDatabase = openDB(NAME, 1, { upgrade(database) { console.debug("Service worker: upgrading database"); if (!database.objectStoreNames.contains("configuration")) { @@ -24,6 +25,12 @@ openDatabase.then((database) => { store.add({ isOnboarded: false }); }); +export async function deleteDatabase() { + const database = await openDatabase; + database.close(); + await deleteDB(NAME); +} + export async function getConfiguration(): Promise { // Get configuration const database = await openDatabase; diff --git a/app/src/database/schema.d.ts b/app/src/database/schema.d.ts index 7d7fd78..bf072d8 100644 --- a/app/src/database/schema.d.ts +++ b/app/src/database/schema.d.ts @@ -9,7 +9,7 @@ export type User = { */ name?: string; }; -export type Message = { +export type IncomingMessage = { sentAt: Date; receivedAt: Date; text: string; @@ -21,7 +21,7 @@ export type Group = { */ user?: User; friend: Friend; - messages: Message[]; + messages: IncomingMessage[]; }; interface Schema extends DBSchema { diff --git a/app/src/routes/chat.tsx b/app/src/routes/chat.tsx index bf008a7..8314ecd 100644 --- a/app/src/routes/chat.tsx +++ b/app/src/routes/chat.tsx @@ -1,6 +1,7 @@ import { Navigate, useParams } from "@solidjs/router"; import { createResource, For, JSX, Show, Suspense } from "solid-js"; import { getGroup } from "../database"; +import { setupCrackle } from "../useCrackle"; // TODO take this inspiration https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Fly7219l1-1.png?alt=media&token=67ff316b-7515-4e9f-9971-4e580290b1f2 // from https://m3.material.io/foundations/layout/applying-layout/compact#283b4432-e3ee-46df-aa66-9ec87965c6ef @@ -13,38 +14,22 @@ export default function Chat() { const [group] = createResource(parameters.groupId, getGroup); - // if (groupIndex() < 0) { - // return ( - // <> - //

Chat not found

- //

- // This chat does not seem to exist Go back - //

- // - // ); - // } - async function handleSend( event: Parameters>[0] ) { - // event.preventDefault(); - // const messageText = /** @type {HTMLInputElement} */ ( - // event.currentTarget.message - // ).value; - // event.currentTarget.reset(); - // /** @type {Message} */ - // const message = { - // sent: new Date(), - // text: messageText, - // }; - // setApp("groups", groupIndex(), "messages", messages().length, message); - // await sendMessage({ - // type: "sendMessage", - // groupId: group().id, - // friendId: group().friend.id, - // sent: new Date(), - // text: messageText, - // }); + event.preventDefault(); + const text = ( + event.currentTarget.elements.namedItem("message") as HTMLInputElement + ).value; + event.currentTarget.reset(); + + const handle = await setupCrackle; + + await handle.sendMessage({ + groupId: parameters.groupId, + sentAt: new Date(), + text, + }); } return ( diff --git a/app/src/service-worker/serviceWorker.ts b/app/src/service-worker/serviceWorker.ts index fdac686..79700d9 100644 --- a/app/src/service-worker/serviceWorker.ts +++ b/app/src/service-worker/serviceWorker.ts @@ -1,9 +1,10 @@ import { precacheAndRoute } from "workbox-precaching"; import init, { Client, DecodedPackage, Friend } from "meal-core"; import { expose } from "../crackle"; -import { Group, Message } from "../database/schema"; +import { Group, IncomingMessage } from "../database/schema"; import { broadcastMessage } from "../broadcast"; import { + deleteDatabase, getConfiguration, getGroup, insertGroup, @@ -11,11 +12,6 @@ import { updateGroup, } from "../database"; -/** - * @import { Schema } from "./schema" - * @import { Operation } from "./operation" - */ - // Reduce noise // @ts-expect-error self.__WB_DISABLE_DEV_LOGS = true; @@ -72,6 +68,22 @@ async function persistClient(client: Client) { return client; } +async function deleteClient() { + const directory = await navigator.storage.getDirectory(); + try { + await directory.removeEntry(FILE_NAME, { + recursive: false, + }); + } catch (error) { + if (error instanceof DOMException && error.name === "NotFoundError") { + console.debug("No client file found"); + return; + } + + throw error; + } +} + async function initializeClient(): Promise { // Have to use wasm-pack --target web to build the wasm package to get the init function because with the bundler target // it is included as a top level await which is not supported by service workers according to the web spec @@ -103,24 +115,22 @@ async function updateClient(client: Client) { /** The url for messages endpoint. Don't forget the trailing slash. */ const messagesUrl = new URL("/messages/", self.location.origin); -async function sendGroupInvite( - friendId: string, - welcomePackage: Uint8Array -) { +async function postMessage(friendId: string, body: Uint8Array) { const url = new URL(friendId, messagesUrl); - // Send welcome to peer const request = new Request(url, { method: "post", headers: { - // https://www.rfc-editor.org/rfc/rfc9420.html#name-the-message-mls-media-type + //https://www.rfc-editor.org/rfc/rfc9420.html#name-the-message-mls-media-type "Content-Type": "message/mls", }, - body: welcomePackage, + body, }); //TODO error handling + //TODO retry await fetch(request); } + function assertNotShared( data: Uint8Array ): asserts data is Uint8Array { @@ -128,6 +138,12 @@ function assertNotShared( throw new Error("Uint8Array uses a SharedArrayBuffer"); } +export type OutgoingMessage = { + groupId: string; + sentAt: Date; + text: string; +}; + const handler = { async getIsOnboarded() { console.debug("[Service worker] Getting isOnboarded"); @@ -207,7 +223,32 @@ const handler = { void updateClient(client); assertNotShared(welcomePackage); - await sendGroupInvite(keyPackage.friend.id, welcomePackage); + await postMessage(keyPackage.friend.id, welcomePackage); + }, + + async sendMessage(message: OutgoingMessage) { + const group = await getGroup(message.groupId); + if (group === undefined) throw new Error("Group not found"); + + const client = await getClient; + const body = client.send_message(group.id, { + sent_at: message.sentAt, + text: message.text, + }); + + assertNotShared(body); + await postMessage(group.friend.id, body); + }, + + async signOut() { + //TODO make sign out not suddenly delete everything + // Wipe client + await Promise.all([ + getSocket.then((socket) => socket.close()), + //TODO test what happens if clients have open connections to the database + deleteDatabase(), + deleteClient(), + ]); }, }; @@ -260,9 +301,9 @@ async function receiveMessage(event: MessageEvent) { case "Private": { console.debug("Processed private message", message); - const messageEntry: Message = { + const messageEntry: IncomingMessage = { receivedAt: new Date(), - sentAt: new Date(message.content.sent), + sentAt: new Date(message.content.sent_at), text: message.content.text, }; @@ -279,6 +320,7 @@ async function receiveMessage(event: MessageEvent) { } } +//TODO why did I decide to use a websocket instead of SSE? Replace with SSE async function setupWebsocket() { // Assume client id does not change const client = await getClient; diff --git a/core/src/lib.rs b/core/src/lib.rs index b307dcd..e21f78f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -54,11 +54,12 @@ pub struct Friend { pub name: Option, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Tsify, serde::Serialize, serde::Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] pub struct MessageContent { /// The time the sender said they supposedly sent the message #[serde(with = "time::serde::iso8601")] - sent: OffsetDateTime, + sent_at: OffsetDateTime, text: String, } diff --git a/core/src/v2/serializable.rs b/core/src/v2/serializable.rs index a007b4f..e5fc9f0 100644 --- a/core/src/v2/serializable.rs +++ b/core/src/v2/serializable.rs @@ -17,7 +17,7 @@ use tls_codec::Serialize as _; use wasm_bindgen::{JsError, prelude::wasm_bindgen}; use crate::{ - ApplicationMessage, CIPHERSUITE, DecodedPackage, Friend, ID_LENGTH, Message, + ApplicationMessage, CIPHERSUITE, DecodedPackage, Friend, ID_LENGTH, Message, MessageContent, encode_application_id, v2::provider::Provider, }; @@ -353,4 +353,26 @@ impl Client { Ok(value) } + + pub fn send_message( + &mut self, + group_id: &str, + message: MessageContent, + ) -> Result, JsError> { + let group_id = BASE64_URL_SAFE_NO_PAD.decode(group_id)?; + let group_id = GroupId::from_slice(&group_id); + let mut group = + MlsGroup::load(self.provider.storage(), &group_id)?.ok_or_else(|| GroupNotFound)?; + + let message = postcard::to_allocvec(&message)?; + let message = group.create_message(&self.provider, &self.user.signature_key, &message)?; + + // We can batch send messages so we need to wrap it in a collection + let messages = &[message]; + let message = TlsSliceU8(messages); + + let serialized = message.tls_serialize_detached()?; + + Ok(serialized.into_boxed_slice()) + } }