cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

commit 1f8a4b72987f22a23ab5eafc02296708a49588ea
parent 3d86c3e024c6de1b98cbd39f619b3a3b7e21f50b
Author: MTRNord <mtrnord1@gmail.com>
Date:   Tue,  9 May 2023 20:31:44 +0200

Add local echo

Diffstat:
Msrc/app/sdk/api/events.ts | 4+++-
Msrc/app/sdk/ownUser.ts | 3++-
Msrc/app/sdk/room.ts | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/app/sdk/slidingSync.ts | 5+++--
Msrc/components/input/chat/input.tsx | 4++--
5 files changed, 82 insertions(+), 34 deletions(-)

diff --git a/src/app/sdk/api/events.ts b/src/app/sdk/api/events.ts @@ -5,7 +5,9 @@ export interface IRoomEvent<Content = any> { origin_server_ts: number; sender: string; type: string; - unsigned?: any; + unsigned?: { + transaction_id?: string; + }; "m.relates.to"?: any; [key: string]: any; } diff --git a/src/app/sdk/ownUser.ts b/src/app/sdk/ownUser.ts @@ -19,7 +19,8 @@ export class OwnUser { // TODO: call logout endpoint on logout public async logout() { if (!this.mxid) { - throw Error("Not logged in"); + console.log("Not logged in"); + return; } if (!this.access_token) { throw Error("Not logged in"); diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts @@ -32,6 +32,7 @@ export declare interface Room { export class Room extends EventEmitter { private events: IRoomEvent[] = []; + private pendingEvents: IRoomEvent[] = []; private stateEvents: IRoomStateEvent[] = []; private name?: string; @@ -52,10 +53,16 @@ export class Room extends EventEmitter { public addEvents(events: IRoomEvent[]) { events.forEach((newEvent) => { + console.log("newEvent", newEvent) + if (newEvent.unsigned?.transaction_id) { + console.log("Found transaction id. Trying to remove pending event") + this.pendingEvents = this.pendingEvents.filter((event) => event.unsigned?.transaction_id !== newEvent.unsigned?.transaction_id); + } + this.events.push(newEvent); }); - this.emit("events", this.events); + this.emit("events", this.getEvents()); } public addStateEvents(state: IRoomStateEvent[]) { @@ -195,6 +202,10 @@ export class Room extends EventEmitter { } public getEvents(): IRoomEvent[] { + return [...this.events, ...this.pendingEvents]; + } + + public getPureEvents(): IRoomEvent[] { return this.events; } @@ -235,25 +246,47 @@ export class Room extends EventEmitter { return isEncrypted; } - public async sendHtmlMessage(html: string, plainText: string): Promise<string> { + // TODO: Workaround since txn id doesnt come down sync + private deletePendingByEventID(eventID: string) { + this.pendingEvents = this.pendingEvents.filter((event) => event.eventID !== eventID); + } + + public async sendHtmlMessage(html: string, plainText: string, callbackLocalEcho: () => void): Promise<string> { + const txn_id = Date.now().toString(); + // @ts-ignore: Intentionally incomplete + const event = { + type: "m.room.message", + unsigned: { + transaction_id: txn_id + }, + origin_server_ts: txn_id, + sender: this.client.mxid, + event_id: txn_id, + content: { + "msgtype": "m.text", + "body": plainText, + "format": "org.matrix.custom.html", + "formatted_body": html + } + } as IRoomEvent; + this.pendingEvents.push(event); + this.emit("events", this.getEvents()); + callbackLocalEcho(); + if (!this.isEncrypted()) { - const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.message/${Date.now().toString()}`, { + const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.message/${event.unsigned?.transaction_id}`, { method: "PUT", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.client.accessToken}` }, - body: JSON.stringify({ - "msgtype": "m.text", - "body": plainText, - "format": "org.matrix.custom.html", - "formatted_body": html - }) + body: JSON.stringify(event.content) }); if (!resp.ok) { throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); } const json = await resp.json(); + this.deletePendingByEventID(event.event_id); return json.event_id; } else { console.log("Sending encrypted message"); @@ -262,14 +295,9 @@ export class Room extends EventEmitter { const encrypted = await this.e2ee.encryptRoomEvent( new RoomId(this.roomID), "m.room.message", - JSON.stringify({ - "msgtype": "m.text", - "body": plainText, - "format": "org.matrix.custom.html", - "formatted_body": html - }) + JSON.stringify(event.content) ); - const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.encrypted/${Date.now().toString()}`, { + const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.encrypted/${event.unsigned?.transaction_id}`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -281,27 +309,45 @@ export class Room extends EventEmitter { throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); } const json = await resp.json(); + this.deletePendingByEventID(event.event_id); return json.event_id; } } - public async sendTextMessage(text: string): Promise<string> { + public async sendTextMessage(text: string, callbackLocalEcho: () => void): Promise<string> { + const txn_id = Date.now().toString(); + // @ts-ignore: Intentionally incomplete + const event = { + type: "m.room.message", + unsigned: { + transaction_id: txn_id + }, + origin_server_ts: txn_id, + event_id: txn_id, + sender: this.client.mxid, + content: { + "msgtype": "m.text", + "body": text, + } + } as IRoomEvent; + this.pendingEvents.push(event); + this.emit("events", this.getEvents()); + callbackLocalEcho(); + if (!this.isEncrypted()) { - const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.message/${Date.now().toString()}`, { + const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.message/${event.unsigned?.transaction_id}`, { method: "PUT", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.client.accessToken}` }, - body: JSON.stringify({ - "msgtype": "m.text", - "body": text - }) + body: JSON.stringify(event.content) }); if (!resp.ok) { throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); } const json = await resp.json(); + this.deletePendingByEventID(event.event_id); return json.event_id; } else { console.log("Sending encrypted message2"); @@ -310,12 +356,9 @@ export class Room extends EventEmitter { const encrypted = await this.e2ee.encryptRoomEvent( new RoomId(this.roomID), "m.room.message", - JSON.stringify({ - "msgtype": "m.text", - "body": text - }) + JSON.stringify(event.content) ); - const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.encrypted/${Date.now().toString()}`, { + const resp = await fetch(`${this.hostname}/_matrix/client/v3/rooms/${this.roomID}/send/m.room.encrypted/${event.unsigned?.transaction_id}`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -327,6 +370,7 @@ export class Room extends EventEmitter { throw new Error(`Failed to send message: ${resp.status} ${resp.statusText}`); } const json = await resp.json(); + this.deletePendingByEventID(event.event_id); return json.event_id; } } @@ -388,7 +432,7 @@ export function useEvents(room?: Room) { setEvents(room?.getEvents() || []); // Listen for event updates const listenForEvents = (events: IRoomEvent[]) => { - setEvents([...events]); + setEvents(events); }; room.on("events", listenForEvents); return () => { diff --git a/src/app/sdk/slidingSync.ts b/src/app/sdk/slidingSync.ts @@ -175,7 +175,8 @@ export class MatrixSlidingSync extends EventEmitter { throw Error("Hostname must be set first"); } - await this.user.e2ee.sendIdentifyAndOneTimeKeys(); + // TODO: This might cause future issues + Promise.all([this.user.e2ee.sendIdentifyAndOneTimeKeys()]); // This is the initial sync case for each list let lists_ranges: { @@ -710,7 +711,7 @@ export class MatrixSlidingSync extends EventEmitter { highlight_count: roomObj.getNotificationHighlightCount(), joined_count: roomObj.getJoinedCount(), invited_count: roomObj.getInvitedCount(), - events: roomObj.getEvents(), + events: roomObj.getPureEvents(), stateEvents: roomObj.getStateEvents(), avatarUrl: roomObj.getAvatarURL(), isSpace: roomObj.isSpace(), diff --git a/src/components/input/chat/input.tsx b/src/components/input/chat/input.tsx @@ -154,7 +154,7 @@ const SendButton: FC<SendButtonProps> = memo(({ roomID, onStartSending, onStopSe console.log("Sending message to: ", roomID) if (htmlMessage !== '<p class="editor-paragraph"><br></p>') { - room.sendHtmlMessage(htmlMessage, plainMessage).then(() => { + room.sendHtmlMessage(htmlMessage, plainMessage, () => { editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined); localStorage.removeItem(`editor-${roomID}`); @@ -164,7 +164,7 @@ const SendButton: FC<SendButtonProps> = memo(({ roomID, onStartSending, onStopSe onStopSending(); }) } else { - room.sendTextMessage(plainMessage).then(() => { + room.sendTextMessage(plainMessage, () => { editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined); localStorage.removeItem(`editor-${roomID}`);