Basic WS sync

This commit is contained in:
2025-11-28 17:00:42 +01:00
parent 799341f77c
commit 4b30d67706
4 changed files with 180 additions and 4 deletions

View File

@@ -1,9 +1,54 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
export type WsMessage = { interface BaseRoomEvent {
type: string; time: number;
[k: string]: any; 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 { export class WsApi {
/** /**

View File

@@ -1,9 +1,11 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { import type {
MatrixEvent, MatrixEvent,
MatrixEventData,
MatrixEventsList, MatrixEventsList,
} from "../api/matrix/MatrixApiEvent"; } from "../api/matrix/MatrixApiEvent";
import type { Room } from "../api/matrix/MatrixApiRoom"; import type { Room } from "../api/matrix/MatrixApiRoom";
import type { WsMessage } from "../api/WsApi";
export interface MessageReaction { export interface MessageReaction {
event_id: string; event_id: string;
@@ -45,7 +47,62 @@ export class RoomEventsManager {
this.rebuildMessagesList(); 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() { private rebuildMessagesList() {
this.messages = [];
// Sorts events list to process oldest events first // Sorts events list to process oldest events first
this.events.sort((a, b) => a.time - b.time); this.events.sort((a, b) => a.time - b.time);

View File

@@ -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<string>(State.Closed);
const wsRef = React.useRef<WebSocket | undefined>(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 (
<Tooltip title={state}>
<CircleIcon
color={
state === State.Connected
? "success"
: state === State.Error
? "error"
: undefined
}
/>
</Tooltip>
);
}

View File

@@ -2,8 +2,10 @@ import React from "react";
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
import type { Room } from "../../api/matrix/MatrixApiRoom"; import type { Room } from "../../api/matrix/MatrixApiRoom";
import type { WsMessage } from "../../api/WsApi";
import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { RoomEventsManager } from "../../utils/RoomEventsManager";
import { AsyncWidget } from "../AsyncWidget"; import { AsyncWidget } from "../AsyncWidget";
import { MatrixWS } from "./MatrixWS";
import { RoomMessagesList } from "./RoomMessagesList"; import { RoomMessagesList } from "./RoomMessagesList";
import { SendMessageForm } from "./SendMessageForm"; import { SendMessageForm } from "./SendMessageForm";
@@ -11,6 +13,7 @@ export function RoomWidget(p: {
room: Room; room: Room;
users: UsersMap; users: UsersMap;
}): React.ReactElement { }): React.ReactElement {
const [_count, setCount] = React.useState(0);
const [roomMgr, setRoomMgr] = React.useState<undefined | RoomEventsManager>(); const [roomMgr, setRoomMgr] = React.useState<undefined | RoomEventsManager>();
const load = async () => { const load = async () => {
@@ -20,6 +23,10 @@ export function RoomWidget(p: {
setRoomMgr(mgr); setRoomMgr(mgr);
}; };
const handleNewMessage = (m: WsMessage) => {
if (roomMgr?.processWsMessage(m)) setCount((c) => c + 1);
};
return ( return (
<AsyncWidget <AsyncWidget
loadKey={p.room.id} loadKey={p.room.id}
@@ -28,6 +35,9 @@ export function RoomWidget(p: {
errMsg="Failed to load room!" errMsg="Failed to load room!"
build={() => ( build={() => (
<div style={{ display: "flex", flexDirection: "column", flex: 1 }}> <div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
<div style={{ position: "absolute", right: "0px", padding: "10px" }}>
<MatrixWS onMessage={handleNewMessage} />
</div>
<RoomMessagesList mgr={roomMgr!} {...p} /> <RoomMessagesList mgr={roomMgr!} {...p} />
<SendMessageForm {...p} /> <SendMessageForm {...p} />
</div> </div>