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) {
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;
if (m.type === "RoomReactionEvent") {

View File

@@ -1,12 +1,17 @@
import { Divider } from "@mui/material";
import React from "react";
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
import {
MatrixApiProfile,
type UsersMap,
} from "../../api/matrix/MatrixApiProfile";
import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom";
import { MatrixSyncApi } from "../../api/MatrixSyncApi";
import type { WsMessage } from "../../api/WsApi";
import { RoomEventsManager } from "../../utils/RoomEventsManager";
import { AsyncWidget } from "../AsyncWidget";
import { useUserInfo } from "../dashboard/BaseAuthenticatedPage";
import { MatrixWS } from "./MatrixWS";
import { RoomSelector } from "./RoomSelector";
import { RoomWidget } from "./RoomWidget";
import { SpaceSelector } from "./SpaceSelector";
@@ -15,7 +20,7 @@ export function MainMessageWidget(): React.ReactElement {
const [rooms, setRooms] = React.useState<Room[] | undefined>();
const [users, setUsers] = React.useState<UsersMap | undefined>();
const load = async () => {
const loadRoomsList = async () => {
await MatrixSyncApi.Start();
const rooms = await MatrixApiRoom.ListJoined();
@@ -33,10 +38,16 @@ export function MainMessageWidget(): React.ReactElement {
return (
<AsyncWidget
loadKey={1}
load={load}
load={loadRoomsList}
ready={!!rooms && !!users}
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: {
rooms: Room[];
users: UsersMap;
onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void;
}): React.ReactElement {
const user = useUserInfo();
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(() => {
return p.rooms
@@ -56,18 +70,72 @@ function _MainMessageWidget(p: {
);
}, [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 (
<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} />
{/* Separator */}
<Divider orientation="vertical" />
{/* Room selector */}
<RoomSelector
{...p}
rooms={spaceRooms}
currRoom={room}
onChange={setRoom}
currRoom={currentRoom}
onChange={setCurrentRoom}
/>
{/* Separator */}
<Divider orientation="vertical" />
{room === undefined && (
{/* If no room is selected */}
{currentRoom === undefined && (
<div
style={{
display: "flex",
@@ -79,7 +147,19 @@ function _MainMessageWidget(p: {
No room selected.
</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>
);
}

View File

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

View File

@@ -38,7 +38,7 @@ import { AccountIcon } from "./AccountIcon";
export function RoomMessagesList(p: {
room: Room;
users: UsersMap;
mgr: RoomEventsManager;
manager: RoomEventsManager;
}): React.ReactElement {
const messagesEndRef = React.createRef<HTMLDivElement>();
@@ -46,7 +46,7 @@ export function RoomMessagesList(p: {
React.useEffect(() => {
if (messagesEndRef)
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
}, [p.mgr.messages.length]);
}, [p.manager.messages.length]);
return (
<div
@@ -58,20 +58,20 @@ export function RoomMessagesList(p: {
paddingLeft: "20px",
}}
>
{p.mgr.messages.map((m, idx) => (
{p.manager.messages.map((m, idx) => (
<RoomMessage
key={m.event_id}
{...p}
message={m}
previousFromSamePerson={
idx > 0 &&
p.mgr.messages[idx - 1].account === m.account &&
m.time_sent - p.mgr.messages[idx - 1].time_sent < 60 * 3 * 1000
p.manager.messages[idx - 1].account === m.account &&
m.time_sent - p.manager.messages[idx - 1].time_sent < 60 * 3 * 1000
}
firstMessageOfDay={
idx === 0 ||
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 { 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";
export function RoomWidget(p: {
room: Room;
users: UsersMap;
manager: RoomEventsManager;
}): 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 (
<AsyncWidget
loadKey={p.room.id}
ready={!!roomMgr}
load={load}
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>
)}
/>
<div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
<RoomMessagesList {...p} />
<SendMessageForm {...p} />
</div>
);
}