diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts index 8560808..b213023 100644 --- a/matrixgw_frontend/src/api/WsApi.ts +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -1,9 +1,54 @@ import { APIClient } from "./ApiClient"; -export type WsMessage = { - type: string; - [k: string]: any; -}; +interface BaseRoomEvent { + time: number; + room_id: string; + event_id: string; + sender: string; + origin_server_ts: number; +} + +type MessageType = "m.text" | "m.image" | string; + +export interface RoomMessageEvent extends BaseRoomEvent { + type: "RoomMessageEvent"; + data: { + msgtype: MessageType; + body: string; + "m.relates_to"?: { + rel_type?: "m.replace" | string; + event_id?: string; + }; + "m.new_content"?: { + msgtype?: MessageType; + body?: string; + }; + file?: { url: string }; + }; +} + +export interface RoomReactionEvent extends BaseRoomEvent { + type: "RoomReactionEvent"; + data: { + "m.relates_to": { + rel_type: string; + event_id: string; + key: string; + }; + }; +} + +export interface RoomRedactionEvent extends BaseRoomEvent { + type: "RoomRedactionEvent"; + data: { + redacts: string; + }; +} + +export type WsMessage = + | RoomMessageEvent + | RoomReactionEvent + | RoomRedactionEvent; export class WsApi { /** diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 36e5a1a..7b1621b 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -1,9 +1,11 @@ import dayjs from "dayjs"; import type { MatrixEvent, + MatrixEventData, MatrixEventsList, } from "../api/matrix/MatrixApiEvent"; import type { Room } from "../api/matrix/MatrixApiRoom"; +import type { WsMessage } from "../api/WsApi"; export interface MessageReaction { event_id: string; @@ -45,7 +47,62 @@ export class RoomEventsManager { this.rebuildMessagesList(); } + processWsMessage(m: WsMessage) { + if (m.room_id !== this.room.id) return false; + + let data: MatrixEventData; + if (m.type === "RoomReactionEvent") { + data = { + type: "m.reaction", + content: { + "m.relates_to": { + key: m.data["m.relates_to"].key, + event_id: m.data["m.relates_to"].event_id, + }, + }, + }; + } else if (m.type === "RoomRedactionEvent") { + data = { + type: "m.room.redaction", + redacts: m.data.redacts, + }; + } else if (m.type === "RoomMessageEvent") { + data = { + type: "m.room.message", + content: { + body: m.data["m.new_content"]?.body ?? m.data.body, + msgtype: m.data.msgtype, + "m.relates_to": + m.data["m.relates_to"] && m.data["m.relates_to"].event_id + ? { + event_id: m.data["m.relates_to"].event_id!, + rel_type: m.data["m.relates_to"].rel_type ?? "", + } + : undefined, + file: m.data.file, + }, + }; + } else { + // Ignore event + console.info("Event not supported => ignored"); + return false; + } + + this.events.push({ + sender: m.sender, + id: m.event_id, + time: m.origin_server_ts, + data, + }); + + this.rebuildMessagesList(); + + return true; + } + private rebuildMessagesList() { + this.messages = []; + // Sorts events list to process oldest events first this.events.sort((a, b) => a.time - b.time); diff --git a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx new file mode 100644 index 0000000..8d826de --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { WsApi, type WsMessage } from "../../api/WsApi"; +import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider"; +import CircleIcon from "@mui/icons-material/Circle"; +import { Tooltip } from "@mui/material"; + +const State = { + Closed: "Closed", + Connected: "Connected", + Error: "Error", +} as const; + +export function MatrixWS(p: { + onMessage: (msg: WsMessage) => void; +}): React.ReactElement { + const snackbar = useSnackbar(); + + const [state, setState] = React.useState(State.Closed); + const wsRef = React.useRef(undefined); + const [connCount, setConnCount] = React.useState(0); + + React.useEffect(() => { + const count = connCount; + const ws = new WebSocket(WsApi.WsURL); + wsRef.current = ws; + + ws.onopen = () => setState(State.Connected); + ws.onerror = (e) => { + if (count != connCount) return; + + console.error(`WS Debug error!`, e); + snackbar(`WebSocket error!`); + setState(State.Error); + + setTimeout(() => setConnCount(connCount + 1), 500); + }; + ws.onclose = () => { + setState(State.Closed); + wsRef.current = undefined; + }; + + ws.onmessage = (msg) => { + const dec = JSON.parse(msg.data); + console.info("WS message", dec); + p.onMessage(dec); + }; + + return () => ws.close(); + }, [connCount]); + + return ( + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index e990666..4888f63 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -2,8 +2,10 @@ import React from "react"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; +import type { WsMessage } from "../../api/WsApi"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { AsyncWidget } from "../AsyncWidget"; +import { MatrixWS } from "./MatrixWS"; import { RoomMessagesList } from "./RoomMessagesList"; import { SendMessageForm } from "./SendMessageForm"; @@ -11,6 +13,7 @@ export function RoomWidget(p: { room: Room; users: UsersMap; }): React.ReactElement { + const [_count, setCount] = React.useState(0); const [roomMgr, setRoomMgr] = React.useState(); const load = async () => { @@ -20,6 +23,10 @@ export function RoomWidget(p: { setRoomMgr(mgr); }; + const handleNewMessage = (m: WsMessage) => { + if (roomMgr?.processWsMessage(m)) setCount((c) => c + 1); + }; + return ( (
+
+ +