diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index f6ab6e7..9064ccb 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -48,7 +48,7 @@ impl APIEvent { pub struct APIEventsList { pub start: String, pub end: Option, - pub messages: Vec, + pub events: Vec, } /// Get messages for a given room @@ -65,7 +65,7 @@ pub(super) async fn get_events( Ok(APIEventsList { start: messages.start, end: messages.end, - messages: stream::iter(messages.chunk) + events: stream::iter(messages.chunk) .then(async |msg| APIEvent::from_evt(msg, room.room_id()).await) .collect::>() .await diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 1a64948..20461e9 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -52,7 +52,7 @@ impl APIRoomInfo { is_space: r.is_space(), parents: parent_spaces, number_unread_messages: r.unread_notification_counts().notification_count, - latest_event: get_events(r, 1, None).await?.messages.into_iter().next(), + latest_event: get_events(r, 1, None).await?.events.into_iter().next(), }) } } diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index ee0dfd6..01c2d9a 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -1,5 +1,70 @@ +import { APIClient } from "../ApiClient"; +import type { Room } from "./MatrixApiRoom"; + +export interface MatrixRoomMessage { + type: "m.room.message"; + content: { + body: string; + msgtype: "m.text" | "m.image" | string; + "m.relates_to"?: { + event_id: string; + rel_type: "m.replace" | string; + }; + file?: { + url: string; + }; + }; +} + +export interface MatrixReaction { + type: "m.reaction"; + content: { + "m.relates_to": { + event_id: string; + key: string; + }; + }; +} + +export interface MatrixRoomRedaction { + type: "m.room.redaction"; + redacts: string; +} + +export type MatrixEventData = + | MatrixRoomMessage + | MatrixReaction + | MatrixRoomRedaction + | { type: "other" }; + export interface MatrixEvent { id: string; time: number; sender: string; + data: MatrixEventData; +} + +export interface MatrixEventsList { + start: string; + end?: string; + events: MatrixEvent[]; +} + +export class MatrixApiEvent { + /** + * Get Matrix room events + */ + static async GetRoomEvents( + room: Room, + from?: string + ): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: + `/matrix/room/${encodeURIComponent(room.id)}/events` + + (from ? `?from=${from}` : ""), + }) + ).data; + } } diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts new file mode 100644 index 0000000..78dcedb --- /dev/null +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -0,0 +1,101 @@ +import type { + MatrixEvent, + MatrixEventsList, +} from "../api/matrix/MatrixApiEvent"; +import type { Room } from "../api/matrix/MatrixApiRoom"; + +export interface MessageReaction { + event_id: string; + account: string; + key: string; +} + +export interface Message { + event_id: string; + sent: number; + modified: boolean; + reactions: MessageReaction[]; + content: string; + image?: string; +} + +export class RoomEventsManager { + readonly room: Room; + private events: MatrixEvent[]; + messages: Message[]; + endToken?: string; + + constructor(room: Room, initialMessages: MatrixEventsList) { + this.room = room; + this.events = []; + this.messages = []; + + this.processNewEvents(initialMessages); + } + + /** + * Process events given by the API + */ + processNewEvents(evts: MatrixEventsList) { + this.endToken = evts.end; + this.events = [...this.events, ...evts.events]; + this.rebuildMessagesList(); + } + + private rebuildMessagesList() { + // Sorts events list to process oldest events first + this.events.sort((a, b) => a.time - b.time); + + // First, process redactions to skip redacted events + let redacted = new Set( + this.events + .map((e) => + e.data.type === "m.room.redaction" ? e.data.redacts : undefined + ) + .filter((e) => e !== undefined) + ); + + for (const evt of this.events) { + if (redacted.has(evt.id)) continue; + + const data = evt.data; + + // Message + if (data.type === "m.room.message") { + // Check if this message replaces another one + if (data.content["m.relates_to"]) { + const message = this.messages.find( + (m) => m.event_id === data.content["m.relates_to"]?.event_id + ); + if (!message) continue; + message.modified = true; + message.content = data.content.body; + continue; + } + + this.messages.push({ + event_id: evt.id, + modified: false, + reactions: [], + sent: evt.time, + image: data.content.file?.url, + content: data.content.body, + }); + } + + // Reaction + if (data.type === "m.reaction") { + const message = this.messages.find( + (m) => m.event_id === data.content["m.relates_to"].event_id + ); + + if (!message) continue; + message.reactions.push({ + account: evt.sender, + event_id: evt.id, + key: data.content["m.relates_to"].key, + }); + } + } + } +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index be400ad..de26ea7 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -1,9 +1,30 @@ +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 { RoomEventsManager } from "../../utils/RoomEventsManager"; +import { AsyncWidget } from "../AsyncWidget"; export function RoomWidget(p: { room: Room; users: UsersMap; }): React.ReactElement { - return <>room; + const [roomMgr, setRoomMgr] = React.useState(); + + const load = async () => { + setRoomMgr(undefined); + const messages = await MatrixApiEvent.GetRoomEvents(p.room); + const mgr = new RoomEventsManager(p.room, messages); + setRoomMgr(mgr); + }; + + return ( + <>room} + /> + ); }