2 Commits

Author SHA1 Message Date
1f22d5c41b Refactor rooms management 2025-11-28 18:41:43 +01:00
a656c077bc Follow unread messages 2025-11-28 18:06:40 +01:00
5 changed files with 119 additions and 54 deletions

View File

@@ -50,7 +50,10 @@ export class RoomEventsManager {
} }
processWsMessage(m: WsMessage) { processWsMessage(m: WsMessage) {
if (m.room_id !== this.room.id) return false; if (m.room_id !== this.room.id) {
console.debug("Not an event for current room.");
return false;
}
let data: MatrixEventData; let data: MatrixEventData;
if (m.type === "RoomReactionEvent") { if (m.type === "RoomReactionEvent") {

View File

@@ -1,12 +1,17 @@
import { Divider } from "@mui/material"; import { Divider } from "@mui/material";
import React from "react"; import React from "react";
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
import { import {
MatrixApiProfile, MatrixApiProfile,
type UsersMap, type UsersMap,
} from "../../api/matrix/MatrixApiProfile"; } from "../../api/matrix/MatrixApiProfile";
import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom"; import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom";
import { MatrixSyncApi } from "../../api/MatrixSyncApi"; import { MatrixSyncApi } from "../../api/MatrixSyncApi";
import type { WsMessage } from "../../api/WsApi";
import { RoomEventsManager } from "../../utils/RoomEventsManager";
import { AsyncWidget } from "../AsyncWidget"; import { AsyncWidget } from "../AsyncWidget";
import { useUserInfo } from "../dashboard/BaseAuthenticatedPage";
import { MatrixWS } from "./MatrixWS";
import { RoomSelector } from "./RoomSelector"; import { RoomSelector } from "./RoomSelector";
import { RoomWidget } from "./RoomWidget"; import { RoomWidget } from "./RoomWidget";
import { SpaceSelector } from "./SpaceSelector"; import { SpaceSelector } from "./SpaceSelector";
@@ -15,7 +20,7 @@ export function MainMessageWidget(): React.ReactElement {
const [rooms, setRooms] = React.useState<Room[] | undefined>(); const [rooms, setRooms] = React.useState<Room[] | undefined>();
const [users, setUsers] = React.useState<UsersMap | undefined>(); const [users, setUsers] = React.useState<UsersMap | undefined>();
const load = async () => { const loadRoomsList = async () => {
await MatrixSyncApi.Start(); await MatrixSyncApi.Start();
const rooms = await MatrixApiRoom.ListJoined(); const rooms = await MatrixApiRoom.ListJoined();
@@ -33,10 +38,16 @@ export function MainMessageWidget(): React.ReactElement {
return ( return (
<AsyncWidget <AsyncWidget
loadKey={1} loadKey={1}
load={load} load={loadRoomsList}
ready={!!rooms && !!users} ready={!!rooms && !!users}
errMsg="Failed to initialize messaging component!" errMsg="Failed to initialize messaging component!"
build={() => <_MainMessageWidget rooms={rooms!} users={users!} />} build={() => (
<_MainMessageWidget
rooms={rooms!}
users={users!}
onRoomsListUpdate={(cb) => setRooms((r) => cb(r!))}
/>
)}
/> />
); );
} }
@@ -44,9 +55,12 @@ export function MainMessageWidget(): React.ReactElement {
function _MainMessageWidget(p: { function _MainMessageWidget(p: {
rooms: Room[]; rooms: Room[];
users: UsersMap; users: UsersMap;
onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void;
}): React.ReactElement { }): React.ReactElement {
const user = useUserInfo();
const [space, setSpace] = React.useState<string | undefined>(); const [space, setSpace] = React.useState<string | undefined>();
const [room, setRoom] = React.useState<Room | undefined>(); const [currentRoom, setCurrentRoom] = React.useState<Room | undefined>();
const spaceRooms = React.useMemo(() => { const spaceRooms = React.useMemo(() => {
return p.rooms return p.rooms
@@ -56,18 +70,72 @@ function _MainMessageWidget(p: {
); );
}, [space, p.rooms]); }, [space, p.rooms]);
const [_refreshCount, setRefreshCount] = React.useState(0);
const [roomMgr, setRoomMgr] = React.useState<undefined | RoomEventsManager>();
const loadRoom = async () => {
setRoomMgr(undefined);
if (!currentRoom) {
console.warn("Cannot load manager for no room!");
return;
}
const messages = await MatrixApiEvent.GetRoomEvents(currentRoom);
const mgr = new RoomEventsManager(currentRoom!, messages);
setRoomMgr(mgr);
};
const handleWsEvent = (m: WsMessage) => {
// Process messages for current room
if (roomMgr?.processWsMessage(m)) {
console.info("Current room updated!");
setRefreshCount((c) => c + 1);
}
// Add a new unread message on left sidebar
if (
m.type === "RoomMessageEvent" &&
!m.data["m.new_content"] &&
m.sender !== user.info.matrix_user_id
) {
p.onRoomsListUpdate((r) => {
const n = [...r];
const idx = r.findIndex((el) => el.id === m.room_id);
if (idx)
n[idx] = {
...n[idx],
number_unread_messages: n[idx].number_unread_messages + 1,
};
return n;
});
}
};
return ( return (
<div style={{ display: "flex", height: "100%" }}> <div style={{ display: "flex", height: "100%" }}>
{/* Websocket */}
<div style={{ position: "absolute", right: "0px", padding: "10px" }}>
<MatrixWS onMessage={handleWsEvent} />
</div>
{/* Space selector */}
<SpaceSelector {...p} selectedSpace={space} onChange={setSpace} /> <SpaceSelector {...p} selectedSpace={space} onChange={setSpace} />
{/* Separator */}
<Divider orientation="vertical" /> <Divider orientation="vertical" />
{/* Room selector */}
<RoomSelector <RoomSelector
{...p} {...p}
rooms={spaceRooms} rooms={spaceRooms}
currRoom={room} currRoom={currentRoom}
onChange={setRoom} onChange={setCurrentRoom}
/> />
{/* Separator */}
<Divider orientation="vertical" /> <Divider orientation="vertical" />
{room === undefined && (
{/* If no room is selected */}
{currentRoom === undefined && (
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -79,7 +147,19 @@ function _MainMessageWidget(p: {
No room selected. No room selected.
</div> </div>
)} )}
{room && <RoomWidget {...p} room={room} />}
{/* In case of room */}
{currentRoom && (
<AsyncWidget
loadKey={currentRoom.id}
ready={!!roomMgr}
load={loadRoom}
errMsg="Failed to load room!"
build={() => (
<RoomWidget {...p} manager={roomMgr!} room={currentRoom} />
)}
/>
)}
</div> </div>
); );
} }

View File

@@ -15,18 +15,24 @@ export function MatrixWS(p: {
}): React.ReactElement { }): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
// Keep only the latest version of onMessage
const cbRef = React.useRef(p.onMessage);
React.useEffect(() => {
cbRef.current = p.onMessage;
}, [p.onMessage]);
const [state, setState] = React.useState<string>(State.Closed); const [state, setState] = React.useState<string>(State.Closed);
const wsRef = React.useRef<WebSocket | undefined>(undefined); const wsId = React.useRef<number | undefined>(undefined);
const [connCount, setConnCount] = React.useState(0); const [connCount, setConnCount] = React.useState(0);
React.useEffect(() => { React.useEffect(() => {
const count = connCount; const id = Math.random();
const ws = new WebSocket(WsApi.WsURL); const ws = new WebSocket(WsApi.WsURL);
wsRef.current = ws; wsId.current = id;
ws.onopen = () => setState(State.Connected); ws.onopen = () => setState(State.Connected);
ws.onerror = (e) => { ws.onerror = (e) => {
if (count != connCount) return; if (wsId.current != id) return;
console.error(`WS Debug error!`, e); console.error(`WS Debug error!`, e);
snackbar(`WebSocket error!`); snackbar(`WebSocket error!`);
@@ -34,15 +40,19 @@ export function MatrixWS(p: {
setTimeout(() => setConnCount(connCount + 1), 500); setTimeout(() => setConnCount(connCount + 1), 500);
}; };
ws.onclose = () => { ws.onclose = () => {
if (wsId.current !== id) return;
setState(State.Closed); setState(State.Closed);
wsRef.current = undefined; wsId.current = undefined;
}; };
ws.onmessage = (msg) => { ws.onmessage = (msg) => {
if (wsId.current !== id) return;
const dec = JSON.parse(msg.data); const dec = JSON.parse(msg.data);
console.info("WS message", dec); console.info("WS message", dec);
p.onMessage(dec); cbRef.current(dec);
}; };
return () => ws.close(); return () => ws.close();

View File

@@ -38,7 +38,7 @@ import { AccountIcon } from "./AccountIcon";
export function RoomMessagesList(p: { export function RoomMessagesList(p: {
room: Room; room: Room;
users: UsersMap; users: UsersMap;
mgr: RoomEventsManager; manager: RoomEventsManager;
}): React.ReactElement { }): React.ReactElement {
const messagesEndRef = React.createRef<HTMLDivElement>(); const messagesEndRef = React.createRef<HTMLDivElement>();
@@ -46,7 +46,7 @@ export function RoomMessagesList(p: {
React.useEffect(() => { React.useEffect(() => {
if (messagesEndRef) if (messagesEndRef)
messagesEndRef.current?.scrollIntoView({ behavior: "instant" }); messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
}, [p.mgr.messages.length]); }, [p.manager.messages.length]);
return ( return (
<div <div
@@ -58,20 +58,20 @@ export function RoomMessagesList(p: {
paddingLeft: "20px", paddingLeft: "20px",
}} }}
> >
{p.mgr.messages.map((m, idx) => ( {p.manager.messages.map((m, idx) => (
<RoomMessage <RoomMessage
key={m.event_id} key={m.event_id}
{...p} {...p}
message={m} message={m}
previousFromSamePerson={ previousFromSamePerson={
idx > 0 && idx > 0 &&
p.mgr.messages[idx - 1].account === m.account && p.manager.messages[idx - 1].account === m.account &&
m.time_sent - p.mgr.messages[idx - 1].time_sent < 60 * 3 * 1000 m.time_sent - p.manager.messages[idx - 1].time_sent < 60 * 3 * 1000
} }
firstMessageOfDay={ firstMessageOfDay={
idx === 0 || idx === 0 ||
m.time_sent_dayjs.startOf("day").unix() != m.time_sent_dayjs.startOf("day").unix() !=
p.mgr.messages[idx - 1].time_sent_dayjs.startOf("day").unix() p.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix()
} }
/> />
))} ))}

View File

@@ -1,47 +1,19 @@
import React from "react"; import React from "react";
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 { MatrixWS } from "./MatrixWS";
import { RoomMessagesList } from "./RoomMessagesList"; import { RoomMessagesList } from "./RoomMessagesList";
import { SendMessageForm } from "./SendMessageForm"; import { SendMessageForm } from "./SendMessageForm";
export function RoomWidget(p: { export function RoomWidget(p: {
room: Room; room: Room;
users: UsersMap; users: UsersMap;
manager: RoomEventsManager;
}): React.ReactElement { }): React.ReactElement {
const [_count, setCount] = React.useState(0);
const [roomMgr, setRoomMgr] = React.useState<undefined | RoomEventsManager>();
const load = async () => {
setRoomMgr(undefined);
const messages = await MatrixApiEvent.GetRoomEvents(p.room);
const mgr = new RoomEventsManager(p.room, messages);
setRoomMgr(mgr);
};
const handleNewMessage = (m: WsMessage) => {
if (roomMgr?.processWsMessage(m)) setCount((c) => c + 1);
};
return ( return (
<AsyncWidget <div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
loadKey={p.room.id} <RoomMessagesList {...p} />
ready={!!roomMgr} <SendMessageForm {...p} />
load={load} </div>
errMsg="Failed to load room!"
build={() => (
<div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
<div style={{ position: "absolute", right: "0px", padding: "10px" }}>
<MatrixWS onMessage={handleNewMessage} />
</div>
<RoomMessagesList mgr={roomMgr!} {...p} />
<SendMessageForm {...p} />
</div>
)}
/>
); );
} }