From 7dbb36ddb0ae25e7ffdc216f3af33a7d9a87bc23 Mon Sep 17 00:00:00 2001 From: Talon Date: Sat, 24 Aug 2024 18:13:18 +0200 Subject: [PATCH] Handle message updates via websockets for current channel --- .../src/controllers/websocket-controller.ts | 16 ++--- frontend/src/events/message-events.ts | 28 +++++++++ frontend/src/events/messaging-system.ts | 60 +++++++++++++++++++ frontend/src/state.ts | 5 +- frontend/src/views/main.ts | 28 +++++++-- frontend/src/websockets.ts | 2 + 6 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 frontend/src/events/message-events.ts create mode 100644 frontend/src/events/messaging-system.ts diff --git a/backend/src/controllers/websocket-controller.ts b/backend/src/controllers/websocket-controller.ts index 71f1c0f..56479cc 100644 --- a/backend/src/controllers/websocket-controller.ts +++ b/backend/src/controllers/websocket-controller.ts @@ -3,27 +3,27 @@ import { WebSocket } from "ws"; export const attachEvents = (ws: WebSocket) => { events.on('file-uploaded', (id, channelId, messageId, filePath, fileType, fileSize, originalName) => { - ws.send(JSON.stringify({ type: 'file-uploaded', id, channelId, messageId, filePath, fileType, fileSize, originalName })); + ws.send(JSON.stringify({ type: 'file-uploaded', data: {id, channelId, messageId, filePath, fileType, fileSize, originalName }})); }); events.on('message-created', (id, channelId, content) => { - ws.send(JSON.stringify({ type: 'message-created', id, channelId, content })); + ws.send(JSON.stringify({ type: 'message-created', data: {id, channelId, content }})); }); events.on('message-updated', (id, content) => { - ws.send(JSON.stringify({ type: 'message-updated', id, content })); + ws.send(JSON.stringify({ type: 'message-updated', data: {id, content }})); }); events.on('message-deleted', (id) => { - ws.send(JSON.stringify({ type: 'message-deleted', id })); + ws.send(JSON.stringify({ type: 'message-deleted', data: {id }})); }); events.on('channel-created', (channel) => { - ws.send(JSON.stringify({ type: 'channel-created', channel })); + ws.send(JSON.stringify({ type: 'channel-created', data: {channel }})); }); events.on('channel-deleted', (id) => { - ws.send(JSON.stringify({ type: 'channel-deleted', id })); + ws.send(JSON.stringify({ type: 'channel-deleted', data: {id} })); }); events.on('channel-merged', (channelId, targetChannelId) => { - ws.send(JSON.stringify({ type: 'channel-merged', channelId, targetChannelId })); + ws.send(JSON.stringify({ type: 'channel-merged', data: {channelId, targetChannelId }})); }); events.on('channel-updated', (id, name) => { - ws.send(JSON.stringify({ type: 'channel-updated', id, name })); + ws.send(JSON.stringify({ type: 'channel-updated', data: {id, name }})); }); } \ No newline at end of file diff --git a/frontend/src/events/message-events.ts b/frontend/src/events/message-events.ts new file mode 100644 index 0000000..0b5ffaa --- /dev/null +++ b/frontend/src/events/message-events.ts @@ -0,0 +1,28 @@ +export type MessageCreated = { + channelId: string, + id: string, + content: string, +}; + +export type MessageDeleted = { + channelId: string, + messageId: string, +}; + +export type MessageUpdated = { + id: string, + content: string, +}; + +export type ChannelCreated = { + name: string, +}; + +export type ChannelDeleted = { + channelId: string, +}; + +export type ChannelUpdated = { + channelId: string, + name: string, +}; \ No newline at end of file diff --git a/frontend/src/events/messaging-system.ts b/frontend/src/events/messaging-system.ts new file mode 100644 index 0000000..2197443 --- /dev/null +++ b/frontend/src/events/messaging-system.ts @@ -0,0 +1,60 @@ +export type Message = { + type: string, + data?: T, +}; + +export type MessageHandler = (message: Message) => void; + +export class MessagingSystem { + private handlers: Record[]> = {}; + + public registerHandler(type: string, handler: MessageHandler): void { + if (!this.handlers[type]) { + this.handlers[type] = []; + } + if (!this.handlers[type].includes(handler)) { + this.handlers[type].push(handler); + } + } + + public unregisterHandler(type: string, handler: MessageHandler): void { + if (this.handlers[type]) { + this.handlers[type] = this.handlers[type].filter(h => h !== handler); + } + } + + public registerHandlerOnce(type: string, handler: MessageHandler): void { + const wrappedHandler = (message: Message) => { + handler(message); + this.unregisterHandler(type, wrappedHandler); + }; + this.registerHandler(type, wrappedHandler); + } + + public waitForMessage(type: string, timeout?: number): Promise { + return new Promise((resolve, reject) => { + const handler = (message: Message) => { + if (timer) clearTimeout(timer); + resolve(message.data!); + this.unregisterHandler(type, handler); + }; + + this.registerHandler(type, handler); + + let timer: ReturnType | undefined; + if (timeout) { + timer = setTimeout(() => { + this.unregisterHandler(type, handler); + reject(new Error(`Timeout waiting for message of type '${type}'`)); + }, timeout); + } + }); + } + + public sendMessage(message: Message): void { + const handlers = this.handlers[message.type]; + if (handlers) { + handlers.forEach(handler => handler(message)); + } + } +} diff --git a/frontend/src/state.ts b/frontend/src/state.ts index a28d836..8bb5fa6 100644 --- a/frontend/src/state.ts +++ b/frontend/src/state.ts @@ -1,3 +1,4 @@ +import { MessagingSystem } from "./events/messaging-system"; import { IChannel, Channel } from "./model/channel"; import { IChannelList, ChannelList } from "./model/channel-list"; import { IState } from "./model/state"; @@ -12,11 +13,13 @@ export class State implements IState { unsentMessages!: IUnsentMessage[]; currentChannel!: Channel | null; defaultChannelId!: number; + public events: MessagingSystem; constructor() { this.token = ""; this.channelList = new ChannelList(); this.unsentMessages = []; + this.events = new MessagingSystem(); } public getToken(): string { @@ -45,7 +48,7 @@ export class State implements IState { public async save(): Promise { // stringify everything here except the currentChannel object. - const { currentChannel, ...state } = this; + const { currentChannel, events, ...state } = this; await set("notebrook", state); } diff --git a/frontend/src/views/main.ts b/frontend/src/views/main.ts index 701ed91..12bd22c 100644 --- a/frontend/src/views/main.ts +++ b/frontend/src/views/main.ts @@ -7,6 +7,7 @@ import { RecordAudioDialog } from "../dialogs/record-audio"; import { SearchDialog } from "../dialogs/search"; import { SettingsDialog } from "../dialogs/settings"; import { TakePhotoDialog } from "../dialogs/take-photo"; +import { MessageUpdated } from "../events/message-events"; import { Channel } from "../model/channel"; import { IMessage, Message } from "../model/message"; import { UnsentMessage } from "../model/unsent-message"; @@ -52,6 +53,24 @@ export class MainView extends View { this.updateVisibleMessageShownTimestamps(); }, 1000); setTimeout(() => this.attemptToSendUnsentMessages(), 2000); + + state.events.registerHandler("message-updated", (message) => { + const { data } = message; + if (!data) return; + const channel = state.currentChannel; + if (!channel) return; + const existing = channel.getMessage(parseInt(data!.id)); + if (!existing) { + return; + } else { + existing.content = data.content; + state.save(); + const renderedMessage = this.messageElementMap.get(existing.id); + if (renderedMessage) { + (renderedMessage as ListItem).setText(`${existing.content}; ${this.convertIsoTimeStringToRelative(existing.createdAt)}`); + } + } + }); } @@ -166,7 +185,7 @@ export class MainView extends View { } public onDestroy(): void { - + } private async syncChannels() { @@ -257,7 +276,7 @@ export class MainView extends View { private renderMessage(message: IMessage): UINode { const itm = new ListItem(`${message.content}; ${this.convertIsoTimeStringToRelative(message.createdAt)}`); - itm.setUserData(message); + itm.setUserData(message.id); itm.onClick(() => { new MessageDialog(message).open(); }) @@ -371,7 +390,7 @@ export class MainView extends View { if (file) { playWater(); const msgContent = this.messageInput.getValue() !== "" ? this.messageInput.getValue() : "File upload"; - this.messageInput.setValue(""); + this.messageInput.setValue(""); try { const msg = await API.createMessage(state.currentChannel!.id.toString(), msgContent); const id = msg.id; @@ -430,7 +449,8 @@ export class MainView extends View { for (let i = lowerIndex; i < upperIndex; i++) { const child = this.messageList.children[i]; if (!child) break; - const message = child.getUserData() as IMessage; + const messageId = child.getUserData() as number; + const message = state.currentChannel?.getMessage(messageId); if (message) { (child as ListItem).setText(`${message.content}; ${this.convertIsoTimeStringToRelative(message.createdAt)}`); } diff --git a/frontend/src/websockets.ts b/frontend/src/websockets.ts index 68f9f0c..3a8cb6f 100644 --- a/frontend/src/websockets.ts +++ b/frontend/src/websockets.ts @@ -1,4 +1,5 @@ import { API } from "./api"; +import { state } from "./state"; export const connectToWebsocket = () => { const ws = new WebSocket(`ws://localhost:3000`); @@ -7,6 +8,7 @@ export const connectToWebsocket = () => { } ws.onmessage = (data) => { const message = JSON.parse(data.data.toString()); + state.events.sendMessage(message); console.log(message); } ws.onclose= () => {