commit 02a035f8b25f0132d39a8c3a3e83c82d5c0100fe parent 2eeaa66acd93bc6611360a94a9d468d7194d6eb6 Author: MTRNord <mtrnord1@gmail.com> Date: Thu, 4 May 2023 18:06:45 +0200 Fix various issues and implement basic timeline Diffstat:
132 files changed, 661 insertions(+), 63 deletions(-)
diff --git a/public/font-files/Inter-Black.otf b/public/font-files/Inter-Black.otf Binary files differ. diff --git a/public/font-files/Inter-Black.woff b/public/font-files/Inter-Black.woff Binary files differ. diff --git a/public/font-files/Inter-Black.woff2 b/public/font-files/Inter-Black.woff2 Binary files differ. diff --git a/public/font-files/Inter-BlackItalic.otf b/public/font-files/Inter-BlackItalic.otf Binary files differ. diff --git a/public/font-files/Inter-BlackItalic.woff b/public/font-files/Inter-BlackItalic.woff Binary files differ. diff --git a/public/font-files/Inter-BlackItalic.woff2 b/public/font-files/Inter-BlackItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-Bold.otf b/public/font-files/Inter-Bold.otf Binary files differ. diff --git a/public/font-files/Inter-Bold.woff b/public/font-files/Inter-Bold.woff Binary files differ. diff --git a/public/font-files/Inter-Bold.woff2 b/public/font-files/Inter-Bold.woff2 Binary files differ. diff --git a/public/font-files/Inter-BoldItalic.otf b/public/font-files/Inter-BoldItalic.otf Binary files differ. diff --git a/public/font-files/Inter-BoldItalic.woff b/public/font-files/Inter-BoldItalic.woff Binary files differ. diff --git a/public/font-files/Inter-BoldItalic.woff2 b/public/font-files/Inter-BoldItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-ExtraBold.otf b/public/font-files/Inter-ExtraBold.otf Binary files differ. diff --git a/public/font-files/Inter-ExtraBold.woff b/public/font-files/Inter-ExtraBold.woff Binary files differ. diff --git a/public/font-files/Inter-ExtraBold.woff2 b/public/font-files/Inter-ExtraBold.woff2 Binary files differ. diff --git a/public/font-files/Inter-ExtraBoldItalic.otf b/public/font-files/Inter-ExtraBoldItalic.otf Binary files differ. diff --git a/public/font-files/Inter-ExtraBoldItalic.woff b/public/font-files/Inter-ExtraBoldItalic.woff Binary files differ. diff --git a/public/font-files/Inter-ExtraBoldItalic.woff2 b/public/font-files/Inter-ExtraBoldItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-ExtraLight.otf b/public/font-files/Inter-ExtraLight.otf Binary files differ. diff --git a/public/font-files/Inter-ExtraLight.woff b/public/font-files/Inter-ExtraLight.woff Binary files differ. diff --git a/public/font-files/Inter-ExtraLight.woff2 b/public/font-files/Inter-ExtraLight.woff2 Binary files differ. diff --git a/public/font-files/Inter-ExtraLightItalic.otf b/public/font-files/Inter-ExtraLightItalic.otf Binary files differ. diff --git a/public/font-files/Inter-ExtraLightItalic.woff b/public/font-files/Inter-ExtraLightItalic.woff Binary files differ. diff --git a/public/font-files/Inter-ExtraLightItalic.woff2 b/public/font-files/Inter-ExtraLightItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-Italic.otf b/public/font-files/Inter-Italic.otf Binary files differ. diff --git a/public/font-files/Inter-Italic.woff b/public/font-files/Inter-Italic.woff Binary files differ. diff --git a/public/font-files/Inter-Italic.woff2 b/public/font-files/Inter-Italic.woff2 Binary files differ. diff --git a/public/font-files/Inter-Light.otf b/public/font-files/Inter-Light.otf Binary files differ. diff --git a/public/font-files/Inter-Light.woff b/public/font-files/Inter-Light.woff Binary files differ. diff --git a/public/font-files/Inter-Light.woff2 b/public/font-files/Inter-Light.woff2 Binary files differ. diff --git a/public/font-files/Inter-LightItalic.otf b/public/font-files/Inter-LightItalic.otf Binary files differ. diff --git a/public/font-files/Inter-LightItalic.woff b/public/font-files/Inter-LightItalic.woff Binary files differ. diff --git a/public/font-files/Inter-LightItalic.woff2 b/public/font-files/Inter-LightItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-Medium.otf b/public/font-files/Inter-Medium.otf Binary files differ. diff --git a/public/font-files/Inter-Medium.woff b/public/font-files/Inter-Medium.woff Binary files differ. diff --git a/public/font-files/Inter-Medium.woff2 b/public/font-files/Inter-Medium.woff2 Binary files differ. diff --git a/public/font-files/Inter-MediumItalic.otf b/public/font-files/Inter-MediumItalic.otf Binary files differ. diff --git a/public/font-files/Inter-MediumItalic.woff b/public/font-files/Inter-MediumItalic.woff Binary files differ. diff --git a/public/font-files/Inter-MediumItalic.woff2 b/public/font-files/Inter-MediumItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-Regular.otf b/public/font-files/Inter-Regular.otf Binary files differ. diff --git a/public/font-files/Inter-Regular.woff b/public/font-files/Inter-Regular.woff Binary files differ. diff --git a/public/font-files/Inter-Regular.woff2 b/public/font-files/Inter-Regular.woff2 Binary files differ. diff --git a/public/font-files/Inter-SemiBold.otf b/public/font-files/Inter-SemiBold.otf Binary files differ. diff --git a/public/font-files/Inter-SemiBold.woff b/public/font-files/Inter-SemiBold.woff Binary files differ. diff --git a/public/font-files/Inter-SemiBold.woff2 b/public/font-files/Inter-SemiBold.woff2 Binary files differ. diff --git a/public/font-files/Inter-SemiBoldItalic.otf b/public/font-files/Inter-SemiBoldItalic.otf Binary files differ. diff --git a/public/font-files/Inter-SemiBoldItalic.woff b/public/font-files/Inter-SemiBoldItalic.woff Binary files differ. diff --git a/public/font-files/Inter-SemiBoldItalic.woff2 b/public/font-files/Inter-SemiBoldItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-Thin.otf b/public/font-files/Inter-Thin.otf Binary files differ. diff --git a/public/font-files/Inter-Thin.woff b/public/font-files/Inter-Thin.woff Binary files differ. diff --git a/public/font-files/Inter-Thin.woff2 b/public/font-files/Inter-Thin.woff2 Binary files differ. diff --git a/public/font-files/Inter-ThinItalic.otf b/public/font-files/Inter-ThinItalic.otf Binary files differ. diff --git a/public/font-files/Inter-ThinItalic.woff b/public/font-files/Inter-ThinItalic.woff Binary files differ. diff --git a/public/font-files/Inter-ThinItalic.woff2 b/public/font-files/Inter-ThinItalic.woff2 Binary files differ. diff --git a/public/font-files/Inter-italic.var.ttf b/public/font-files/Inter-italic.var.ttf Binary files differ. diff --git a/public/font-files/Inter-italic.var.woff2 b/public/font-files/Inter-italic.var.woff2 Binary files differ. diff --git a/public/font-files/Inter-roman.var.ttf b/public/font-files/Inter-roman.var.ttf Binary files differ. diff --git a/public/font-files/Inter-roman.var.woff2 b/public/font-files/Inter-roman.var.woff2 Binary files differ. diff --git a/public/font-files/Inter.var.ttf b/public/font-files/Inter.var.ttf Binary files differ. diff --git a/public/font-files/Inter.var.woff2 b/public/font-files/Inter.var.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-Black.otf b/public/font-files/InterDisplay-Black.otf Binary files differ. diff --git a/public/font-files/InterDisplay-Black.woff b/public/font-files/InterDisplay-Black.woff Binary files differ. diff --git a/public/font-files/InterDisplay-Black.woff2 b/public/font-files/InterDisplay-Black.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-BlackItalic.otf b/public/font-files/InterDisplay-BlackItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-BlackItalic.woff b/public/font-files/InterDisplay-BlackItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-BlackItalic.woff2 b/public/font-files/InterDisplay-BlackItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-Bold.otf b/public/font-files/InterDisplay-Bold.otf Binary files differ. diff --git a/public/font-files/InterDisplay-Bold.woff b/public/font-files/InterDisplay-Bold.woff Binary files differ. diff --git a/public/font-files/InterDisplay-Bold.woff2 b/public/font-files/InterDisplay-Bold.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-BoldItalic.otf b/public/font-files/InterDisplay-BoldItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-BoldItalic.woff b/public/font-files/InterDisplay-BoldItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-BoldItalic.woff2 b/public/font-files/InterDisplay-BoldItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraBold.otf b/public/font-files/InterDisplay-ExtraBold.otf Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraBold.woff b/public/font-files/InterDisplay-ExtraBold.woff Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraBold.woff2 b/public/font-files/InterDisplay-ExtraBold.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraBoldItalic.otf b/public/font-files/InterDisplay-ExtraBoldItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraBoldItalic.woff b/public/font-files/InterDisplay-ExtraBoldItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraBoldItalic.woff2 b/public/font-files/InterDisplay-ExtraBoldItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraLight.otf b/public/font-files/InterDisplay-ExtraLight.otf Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraLight.woff b/public/font-files/InterDisplay-ExtraLight.woff Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraLight.woff2 b/public/font-files/InterDisplay-ExtraLight.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraLightItalic.otf b/public/font-files/InterDisplay-ExtraLightItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraLightItalic.woff b/public/font-files/InterDisplay-ExtraLightItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-ExtraLightItalic.woff2 b/public/font-files/InterDisplay-ExtraLightItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-Italic.otf b/public/font-files/InterDisplay-Italic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-Italic.woff b/public/font-files/InterDisplay-Italic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-Italic.woff2 b/public/font-files/InterDisplay-Italic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-Light.otf b/public/font-files/InterDisplay-Light.otf Binary files differ. diff --git a/public/font-files/InterDisplay-Light.woff b/public/font-files/InterDisplay-Light.woff Binary files differ. diff --git a/public/font-files/InterDisplay-Light.woff2 b/public/font-files/InterDisplay-Light.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-LightItalic.otf b/public/font-files/InterDisplay-LightItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-LightItalic.woff b/public/font-files/InterDisplay-LightItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-LightItalic.woff2 b/public/font-files/InterDisplay-LightItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-Medium.otf b/public/font-files/InterDisplay-Medium.otf Binary files differ. diff --git a/public/font-files/InterDisplay-Medium.woff b/public/font-files/InterDisplay-Medium.woff Binary files differ. diff --git a/public/font-files/InterDisplay-Medium.woff2 b/public/font-files/InterDisplay-Medium.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-MediumItalic.otf b/public/font-files/InterDisplay-MediumItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-MediumItalic.woff b/public/font-files/InterDisplay-MediumItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-MediumItalic.woff2 b/public/font-files/InterDisplay-MediumItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-Regular.otf b/public/font-files/InterDisplay-Regular.otf Binary files differ. diff --git a/public/font-files/InterDisplay-Regular.woff b/public/font-files/InterDisplay-Regular.woff Binary files differ. diff --git a/public/font-files/InterDisplay-Regular.woff2 b/public/font-files/InterDisplay-Regular.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-SemiBold.otf b/public/font-files/InterDisplay-SemiBold.otf Binary files differ. diff --git a/public/font-files/InterDisplay-SemiBold.woff b/public/font-files/InterDisplay-SemiBold.woff Binary files differ. diff --git a/public/font-files/InterDisplay-SemiBold.woff2 b/public/font-files/InterDisplay-SemiBold.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-SemiBoldItalic.otf b/public/font-files/InterDisplay-SemiBoldItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-SemiBoldItalic.woff b/public/font-files/InterDisplay-SemiBoldItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-SemiBoldItalic.woff2 b/public/font-files/InterDisplay-SemiBoldItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-Thin.otf b/public/font-files/InterDisplay-Thin.otf Binary files differ. diff --git a/public/font-files/InterDisplay-Thin.woff b/public/font-files/InterDisplay-Thin.woff Binary files differ. diff --git a/public/font-files/InterDisplay-Thin.woff2 b/public/font-files/InterDisplay-Thin.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-ThinItalic.otf b/public/font-files/InterDisplay-ThinItalic.otf Binary files differ. diff --git a/public/font-files/InterDisplay-ThinItalic.woff b/public/font-files/InterDisplay-ThinItalic.woff Binary files differ. diff --git a/public/font-files/InterDisplay-ThinItalic.woff2 b/public/font-files/InterDisplay-ThinItalic.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-italic.var.ttf b/public/font-files/InterDisplay-italic.var.ttf Binary files differ. diff --git a/public/font-files/InterDisplay-italic.var.woff2 b/public/font-files/InterDisplay-italic.var.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay-roman.var.ttf b/public/font-files/InterDisplay-roman.var.ttf Binary files differ. diff --git a/public/font-files/InterDisplay-roman.var.woff2 b/public/font-files/InterDisplay-roman.var.woff2 Binary files differ. diff --git a/public/font-files/InterDisplay.var.ttf b/public/font-files/InterDisplay.var.ttf Binary files differ. diff --git a/public/font-files/InterDisplay.var.woff2 b/public/font-files/InterDisplay.var.woff2 Binary files differ. diff --git a/src/app/sdk/api/apiTypes.ts b/src/app/sdk/api/apiTypes.ts @@ -238,6 +238,7 @@ export interface RoomJson { joined_count: number, invited_count: number, prev_batch: string, + is_dm?: boolean, } export interface IRoomEvent<Content = any> { @@ -247,12 +248,39 @@ export interface IRoomEvent<Content = any> { sender: string; type: string; unsigned?: any; + [key: string]: any; +} + +export interface IRoomMessageContent<MsgType = any> { + body: string; + msgtype: MsgType; +} + +export interface IRoomMessageTextContent extends IRoomMessageContent<"m.text"> { + format?: string; + formatted_body?: string; +} + +export interface IRoomMessageEvent<Content = any> extends IRoomEvent<Content> { } + +export interface IRoomMessageTextEvent extends IRoomMessageEvent<IRoomMessageTextContent> { } + +export function isRoomMessageTextEvent(event: IRoomEvent): event is IRoomMessageTextEvent { + return event.type === "m.room.message" && event.content.msgtype === "m.text"; +} + +export function isRoomMessageEvent(event: IRoomEvent): event is IRoomMessageEvent { + return event.type === "m.room.message"; } export interface IRoomStateEvent<Content = any> extends IRoomEvent<Content> { state_key: string; } +export function isRoomStateEvent(event: IRoomEvent): event is IRoomStateEvent { + return event.state_key !== undefined; +} + export interface IRoomMemberContent { avatar_url?: string; displayname?: string; @@ -331,4 +359,14 @@ export interface ISpaceParentEvent extends IRoomStateEvent<ISpaceParentContent> export function isSpaceParentEvent(event: IRoomEvent): event is ISpaceParentEvent { return event.type === "m.space.parent"; -} -\ No newline at end of file +} + +export interface IRoomTopicContent { + topic: string; +} + +export interface IRoomTopicEvent extends IRoomStateEvent<IRoomTopicContent> { } + +export function isRoomTopicEvent(event: IRoomEvent): event is IRoomTopicEvent { + return event.type === "m.room.topic"; +} diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts @@ -17,6 +17,7 @@ import { isDeleteOp, isInsertOp, isInvalidateOp, + isRoomStateEvent, isSyncOp } from "./api/apiTypes"; import { Room } from "./room"; @@ -61,6 +62,7 @@ interface MatrixDB extends DBSchema { stateEvents?: IRoomStateEvent[]; avatarUrl?: string; isSpace: boolean; + isDM?: boolean; }; }; loginInfo: { @@ -101,6 +103,8 @@ export class MatrixClient extends EventEmitter { private slidingSyncHostname?: string; private syncing = false; private roomsInView: string[] = []; + // TODO: Use to filter rooms by spaces visible. Eventually even make lists per space dynamically and use that to filter + private spaceOpen: string[] = []; private rooms: Set<Room> = new Set(); private syncPos?: string; private initialSync = true; @@ -168,6 +172,9 @@ export class MatrixClient extends EventEmitter { if (room.stateEvents) { roomObj.addStateEvents(room.stateEvents); } + if (room.isDM) { + roomObj.setDM(room.isDM); + } return roomObj; })) instance.emit("rooms", instance.rooms); @@ -285,7 +292,7 @@ export class MatrixClient extends EventEmitter { } - private removeEntry(listKey: string, ranges: number[][], index: number) { + private async removeEntry(listKey: string, ranges: number[][], index: number) { // work out the max index let max = -1; const indexes = [...this.rooms].map(room => room.windowPos[listKey]); @@ -294,6 +301,13 @@ export class MatrixClient extends EventEmitter { max = Number(n); } } + const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === index); + if (roomObj) { + const tx = this.database?.transaction('rooms', 'readwrite'); + await tx?.store.delete(roomObj.roomID); + await tx?.done; + this.rooms.delete(roomObj) + } if (max < 0 || index > max) { return; } @@ -341,17 +355,23 @@ export class MatrixClient extends EventEmitter { timeline_limit = 10; // Calculate overlap between this.roomsInView and this.roomToRoomID and then // calculate the ranges for each list - const rawRangeInView = [...this.rooms] + const rawRangeInView = new Set([...this.rooms] .filter(room => this.roomsInView.includes(room.roomID)) - .map(room => room.windowPos[list]) - .sort(); + .map(room => room.windowPos[list]).sort()) + + + if (rawRangeInView.size === 0) { + // If there are no rooms in view, we can skip this list + continue; + } // Increment range by 1 to make sure we always get a little more than we need // [1,2,3,4,7,8,9,10,11] -> [2,3,4,5,8,9,10,11,12] - rawRangeInView.push(rawRangeInView[rawRangeInView.length - 1] + 1); + rawRangeInView.add([...rawRangeInView][rawRangeInView.size - 1] + 1); + // Turn an input like [1,2,3,4,7,8,9,10,11] to [[1,4], [7,11]] - const rangesInView = rawRangeInView.reduce((acc, cur, i, arr) => { + const rangesInView = [...rawRangeInView].reduce((acc, cur, i, arr) => { if (i === 0) { // [1,2,3,4] -> [[1,1]] acc.push([cur, cur]); @@ -368,18 +388,35 @@ export class MatrixClient extends EventEmitter { return acc; }, [] as [number, number][]); - lists_ranges[list] = rangesInView; + // Sort by the first element of each range and add to the object + const sorted = rangesInView.sort((a, b) => a[0] - b[0]); + + // Deduplicate ranges + const deduped = sorted.reduce((acc, cur, i, arr) => { + if (i === 0) { + acc.push(cur); + return acc; + } + if (cur[0] === arr[i - 1][0] && cur[1] === arr[i - 1][1]) { + return acc; + } + acc.push(cur); + return acc; + }, [] as [number, number][]); + + lists_ranges[list] = deduped; } } if (this.lastRanges && Object.entries(lists_ranges).toString() !== Object.entries(this.lastRanges).toString()) { - console.log("Ranges changed, resetting sync txn_id") + console.log("Ranges changed, resetting sync txn_id", lists_ranges) this.lastRanges = lists_ranges; this.lastTxnID = Date.now().toString(); } if (!this.lastRanges) { + console.log(lists_ranges) this.lastRanges = lists_ranges; this.lastTxnID = Date.now().toString(); } @@ -419,6 +456,12 @@ export class MatrixClient extends EventEmitter { ["m.room.create", ""], // Room Avatar ["m.room.avatar", "*"], + // Room Topic + ["m.room.topic", "*"], + // Request only the m.room.member events required to render events in the timeline. + // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for + // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members". + ["m.room.member", "$LAZY"], ], timeline_limit: timeline_limit, filters: { @@ -435,6 +478,12 @@ export class MatrixClient extends EventEmitter { ["m.room.create", ""], // Room Avatar ["m.room.avatar", "*"], + // Room Topic + ["m.room.topic", "*"], + // Request only the m.room.member events required to render events in the timeline. + // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for + // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members". + ["m.room.member", "$LAZY"], ], timeline_limit: timeline_limit, filters: {} @@ -463,7 +512,7 @@ export class MatrixClient extends EventEmitter { } } console.error(resp); - throw Error("Error syncing. See console for error."); + console.error("Error syncing. See console for error."); } const json = await resp.json() as ISlidingSyncResp; this.syncPos = json.pos; @@ -500,8 +549,8 @@ export class MatrixClient extends EventEmitter { continue; } - // We start to remember the Room now. const newRoom = new Room(roomID, this.hostname!); + // We start to remember the Room now. newRoom.setName(roomID); newRoom.windowPos[listKey] = i; @@ -516,6 +565,9 @@ export class MatrixClient extends EventEmitter { invited_count: newRoom.getInvitedCount(), avatarUrl: newRoom.getAvatarURL(), isSpace: newRoom.isSpace(), + isDM: newRoom.isDM(), + stateEvents: newRoom.getStateEvents(), + events: newRoom.getEvents(), }); } await tx?.done; @@ -557,11 +609,25 @@ export class MatrixClient extends EventEmitter { invited_count: foundRoom.getInvitedCount(), avatarUrl: foundRoom.getAvatarURL(), isSpace: foundRoom.isSpace(), + isDM: foundRoom.isDM(), + stateEvents: foundRoom.getStateEvents(), + events: foundRoom.getEvents(), }); } else { - const newRoom = new Room(op.room_id, this.hostname!); + const roomFromDB = await tx?.store.get(op.room_id); + let newRoom = new Room(op.room_id, this.hostname!); newRoom.setName(op.room_id); newRoom.windowPos[listKey] = op.index; + if (roomFromDB) { + console.warn("Room in db but not in obj list.", op.room_id, "Updating obj list."); + newRoom = new Room(op.room_id, this.hostname!) + newRoom.setName(roomFromDB.name); + newRoom.setNotificationCount(roomFromDB.notification_count); + newRoom.setNotificationHighlightCount(roomFromDB.highlight_count); + newRoom.setJoinedCount(roomFromDB.joined_count); + newRoom.setInvitedCount(roomFromDB.invited_count); + newRoom.setDM(roomFromDB.isDM || false); + } this.rooms.add(newRoom); await tx?.store.put({ windowPos: newRoom.windowPos, @@ -573,6 +639,9 @@ export class MatrixClient extends EventEmitter { invited_count: newRoom.getInvitedCount(), avatarUrl: newRoom.getAvatarURL(), isSpace: newRoom.isSpace(), + isDM: newRoom.isDM(), + stateEvents: newRoom.getStateEvents(), + events: newRoom.getEvents(), }); } @@ -586,16 +655,9 @@ export class MatrixClient extends EventEmitter { } else if (isDeleteOp(op)) { console.log("Got DELETE OP", op); - const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === op.index); - if (roomObj) { - const tx = this.database?.transaction('rooms', 'readwrite'); - await tx?.store.delete(roomObj.roomID); - await tx?.done; - this.rooms.delete(roomObj) - } if (gapIndex !== -1) { // we already have a DELETE operation to process, so process it. - this.removeEntry(listKey, this.lastRanges[listKey], gapIndex); + await this.removeEntry(listKey, this.lastRanges[listKey], gapIndex); } gapIndex = op.index; } else if (isInvalidateOp(op)) { @@ -614,7 +676,7 @@ export class MatrixClient extends EventEmitter { if (gapIndex !== -1) { // we already have a DELETE operation to process, so process it // Everything higher than the gap needs to be shifted left. - this.removeEntry(listKey, this.lastRanges[listKey], gapIndex); + await this.removeEntry(listKey, this.lastRanges[listKey], gapIndex); } } } @@ -627,7 +689,10 @@ export class MatrixClient extends EventEmitter { const joined_count = room.joined_count; const invited_count = room.invited_count; const events = room.timeline; + const state_events = events?.filter(event => isRoomStateEvent(event)).map(event => event as IRoomStateEvent); + const normal_events = events?.filter(event => !isRoomStateEvent(event)).map(event => event as IRoomEvent); const required_state = room.required_state; + const is_dm = room.is_dm; const roomObj = [...this.rooms].find(room => room.roomID === roomID); if (!roomObj) { @@ -643,12 +708,18 @@ export class MatrixClient extends EventEmitter { roomObj.setNotificationHighlightCount(notification_highlight_count); roomObj.setJoinedCount(joined_count); roomObj.setInvitedCount(invited_count); - if (events) { - roomObj.addEvents(events); + if (normal_events) { + roomObj.addEvents(normal_events); } if (required_state) { roomObj.addStateEvents(required_state); } + if (state_events) { + roomObj.addStateEvents(state_events); + } + if (is_dm) { + roomObj.setDM(is_dm); + } // Write to database await tx?.store.put({ @@ -659,13 +730,19 @@ export class MatrixClient extends EventEmitter { highlight_count: roomObj.getNotificationHighlightCount(), joined_count: roomObj.getJoinedCount(), invited_count: roomObj.getInvitedCount(), - events: events, - stateEvents: required_state, + events: roomObj.getEvents(), + stateEvents: roomObj.getStateEvents(), avatarUrl: roomObj.getAvatarURL(), isSpace: roomObj.isSpace(), + isDM: roomObj.isDM(), }); } await tx?.done + + if (this.initialSync) { + this.initialSync = false; + console.log("initialSyncComplete"); + } if (json.rooms && Object.keys(json.rooms).length > 0) { this.emit("rooms", this.rooms); } @@ -689,6 +766,14 @@ export class MatrixClient extends EventEmitter { this.roomsInView = this.roomsInView.filter(room => room !== roomID); } + public addSpaceOpen(roomID: string) { + this.spaceOpen.push(roomID); + } + + public removeSpaceOpen(roomID: string) { + this.spaceOpen = this.spaceOpen.filter(room => room !== roomID); + } + public getRooms(): Set<Room> { return this.rooms; } @@ -944,6 +1029,11 @@ export function useRooms() { return rooms; } +export function useRoom(roomID?: string): Room | undefined { + const client = useContext(MatrixContext); + return [...client.getRooms()].find(room => room.roomID === roomID); +} + export function useSpaces() { const client = useContext(MatrixContext); const [spacesWithRooms, setSpacesWithRooms] = useState<Set<{ diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts @@ -1,4 +1,4 @@ -import { IRoomEvent, IRoomStateEvent, isRoomAvatarEvent, isRoomCreateEvent, isSpaceChildEvent, isSpaceParentEvent } from "./api/apiTypes"; +import { IRoomEvent, IRoomStateEvent, isRoomAvatarEvent, isRoomCreateEvent, isRoomTopicEvent, isSpaceChildEvent, isSpaceParentEvent } from "./api/apiTypes"; export class Room { private events: IRoomEvent[] = []; @@ -9,6 +9,7 @@ export class Room { private notification_highlight_count: number = 0; private joined_count: number = 0; private invited_count: number = 0; + private is_dm: boolean = false; public windowPos: { [list: string]: number @@ -18,11 +19,31 @@ export class Room { constructor(public roomID: string, private hostname: string) { } public addEvents(events: IRoomEvent[]) { - this.events.push(...events); + // if the event id is already known then we update the event instead of pushing it on to the Array + events.forEach((newEvent) => { + const index = this.events.findIndex((oldEvent) => oldEvent.event_id === newEvent.event_id); + if (index !== -1) { + this.events[index] = newEvent; + } else { + this.events.push(newEvent); + } + }); } public addStateEvents(state: IRoomStateEvent[]) { - this.stateEvents.push(...state); + // if the state event id is already known then we update the event instead of pushing it on to the Array + state.forEach((newEvent) => { + const index = this.stateEvents.findIndex((oldEvent) => oldEvent.event_id === newEvent.event_id); + if (index !== -1) { + this.stateEvents[index] = newEvent; + } else { + this.stateEvents.push(newEvent); + } + }); + } + + public getStateEvents(): IRoomStateEvent[] { + return this.stateEvents; } public getAvatarURL(): string | undefined { @@ -59,6 +80,16 @@ export class Room { return this.name; } + public getTopic(): string | undefined { + let topic: string | undefined = undefined; + this.stateEvents.forEach((event) => { + if (isRoomTopicEvent(event)) { + topic = event.content.topic; + } + }); + return topic; + } + public setNotificationCount(count: number) { this.notification_count = count; } @@ -111,13 +142,47 @@ export class Room { return parents; } + public setDM(isDM: boolean) { + this.is_dm = isDM; + } + public isDM(): boolean { - // TODO: Implement this - return false; + return this.is_dm; } public isOnline(): boolean { // TODO: Implement this return false; } + + public getEvents(): IRoomEvent[] { + return this.events; + } + + public getMemberName(userID: string): string { + let name: string = userID; + this.stateEvents.forEach((event) => { + if (event.type === "m.room.member") { + if (event.state_key === userID && event.content.membership == "join") { + name = event.content.displayname; + } + } + }); + return name; + } + + public getMemberAvatar(userID: string): string | undefined { + let avatarURL: string | undefined = undefined; + this.stateEvents.forEach((event) => { + if (event.type === "m.room.member") { + if (event.state_key === userID && event.content.membership == "join") { + const rawAvatarURL = event.content.avatar_url; + if (rawAvatarURL?.startsWith("mxc://")) { + avatarURL = `${this.hostname}/_matrix/media/r0/download/${rawAvatarURL.substring(6)}`; + } + } + } + }); + return avatarURL; + } } \ No newline at end of file diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx @@ -37,8 +37,8 @@ const Avatar: FC<AvatarProps> = memo(({ avatarUrl, displayname, dm = false, onli ); } return ( - <div className="flex relative min-w-[2rem] min-h-[2rem] bg-orange-500 justify-center items-center m-0 mr-3 text-xl rounded-full text-white"> - {displayname[0].toUpperCase()} + <div className="flex relative min-w-[2rem] min-h-[2rem] w-[2rem] h-[2rem] bg-orange-500 justify-center items-center m-0 mr-3 text-xl rounded-full text-white"> + {displayname.replace('@', '')[0].toUpperCase()} { dm ? ( diff --git a/src/components/events/memberEvent.tsx b/src/components/events/memberEvent.tsx @@ -0,0 +1,20 @@ +import { memo } from "react"; +import { IRoomEvent } from "../../app/sdk/api/apiTypes"; +import { FC } from "react"; + +type MemberEventProps = { + /** + * The event to render + */ + event: IRoomEvent; +}; + +const MemberEvent: FC<MemberEventProps> = memo(({ event }) => { + const renderCorrectMessage = (event: IRoomEvent) => { + return <></> + } + + return renderCorrectMessage(event); +}); + +export default MemberEvent; +\ No newline at end of file diff --git a/src/components/events/messageEvent.tsx b/src/components/events/messageEvent.tsx @@ -0,0 +1,73 @@ +import { memo } from "react"; +import { IRoomEvent, isRoomMessageTextEvent } from "../../app/sdk/api/apiTypes"; +import { FC } from "react"; +import Avatar from "../avatar/avatar"; +import { useRoom } from "../../app/sdk/client"; +import Linkify from "linkify-react"; + +type MessageEventProps = { + /** + * The event to render + */ + event: IRoomEvent; + /** + * The roomID of the event to display + */ + roomID?: string; +}; + +const linkifyOptions = { + defaultProtocol: "https", + rel: "noopener", + target: "_blank", +} + +const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID }) => { + const room = useRoom(roomID); + + const renderCorrectMessage = (event: IRoomEvent) => { + if (isRoomMessageTextEvent(event)) { + if (event.content.format === "org.matrix.custom.html") { + // TODO: Sanitize HTML + return ( + <div className="flex flex-row gap-4 p-2 hover:bg-gray-300 rounded-md duration-200 ease-in-out"> + <Avatar + displayname={room?.getMemberName(event.sender) || ""} + avatarUrl={room?.getMemberAvatar(event.sender)} + online={room?.isOnline() || false} + dm={room?.isDM() || false} + /> + <div className="flex flex-col gap-1"> + <h2 className="text-sm font-medium text-red-500 whitespace-normal">{room?.getMemberName(event.sender)}</h2> + {/* TODO: Fixme */} + <p className="whitespace-normal" dangerouslySetInnerHTML={{ __html: event.content.formatted_body! }}></p> + </div> + </div> + ) + } else { + return ( + <div className="flex flex-row gap-4 p-2 hover:bg-gray-300 rounded-md duration-200 ease-in-out"> + <Avatar + displayname={room?.getMemberName(event.sender) || ""} + avatarUrl={room?.getMemberAvatar(event.sender)} + online={room?.isOnline() || false} + dm={room?.isDM() || false} + /> + <div className="flex flex-col gap-2"> + <h2 className="text-sm font-medium text-red-500 whitespace-normal">{room?.getMemberName(event.sender)}</h2> + <Linkify options={linkifyOptions} as='p' className="whitespace-normal">{event.content.body}</Linkify> + </div> + </div> + ) + } + } else { + return ( + <p>{event.content.body}</p> + ) + } + } + + return renderCorrectMessage(event); +}); + +export default MessageEvent; +\ No newline at end of file diff --git a/src/components/events/unknownEvent.tsx b/src/components/events/unknownEvent.tsx @@ -0,0 +1,16 @@ +import { memo } from "react"; +import { IRoomEvent } from "../../app/sdk/api/apiTypes"; +import { FC } from "react"; + +type UnknownEventProps = { + /** + * The event to render + */ + event: IRoomEvent; +}; + +const UnknownEvent: FC<UnknownEventProps> = memo(({ event }) => { + return <p className="whitespace-break-spaces">{JSON.stringify(event, null, 4)}</p>; +}); + +export default UnknownEvent; +\ No newline at end of file diff --git a/src/components/roomList/roomList.tsx b/src/components/roomList/roomList.tsx @@ -1,8 +1,9 @@ -import { FC, memo, useState } from "react"; +import { FC, memo, useContext, useState } from "react"; import RoomListItem from "./roomListItem/roomListItem"; import { ChevronDown, ChevronRight } from "lucide-react"; import './roomList.scss'; import { useNavigate } from "react-router-dom"; +import { MatrixContext } from "../../app/sdk/client"; type Room = { /** @@ -106,11 +107,17 @@ const RoomListRooms: FC<RoomListRoomsProps> = memo(({ sectionID, rooms, onClick, const RoomSection: FC<{ section: Section, onRoomClick: (roomID: string) => void, activeRoom: string | undefined }> = memo(({ section, onRoomClick, activeRoom }: { section: Section, onRoomClick: (roomID: string) => void, activeRoom: string | undefined }) => { const [hidden, setHidden] = useState<boolean>(true); + const matrixClient = useContext(MatrixContext); + if (hidden) { + matrixClient.removeSpaceOpen(section.roomID); + } else { + matrixClient.addSpaceOpen(section.roomID); + } return ( - <div key={section.roomID} className="flex flex-col gap-1 pl-4"> - <div className="flex flex-row gap-2 py-1 items-center justify-start cursor-pointer h-8 text-slate-600" onClick={() => setHidden(prev => !prev)}> + <div key={section.roomID} className="flex flex-col gap-1 pl-4 select-none"> + <div className="flex flex-row gap-2 py-1 items-center justify-start cursor-pointer h-8 text-slate-600" onClick={() => setHidden(prev => !prev)}> {hidden ? <ChevronRight size={14} /> : <ChevronDown size={14} />} - <span className='font-normal text-sm capitalize max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{section.sectionName}</span> + <span className='font-normal text-base capitalize max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{section.sectionName}</span> </div > {!hidden && (<RoomListRooms hidden={hidden} @@ -140,7 +147,7 @@ const RoomList: FC<RoomListProps> = memo(({ sections, rooms }: RoomListProps) => const navigate = useNavigate(); return ( - <div className="flex flex-col gap-1 flex-1 p-2 min-w-[33ch] h-full overflow-y-auto overflow-x-hidden scrollbarSmall"> + <div className="flex flex-col gap-1 flex-1 p-2 min-w-[30ch] overflow-y-auto overflow-x-hidden scrollbarSmall max-w-[33ch]"> { sections.map(section => { return ( @@ -149,7 +156,7 @@ const RoomList: FC<RoomListProps> = memo(({ sections, rooms }: RoomListProps) => section={section} onRoomClick={(roomID: string) => { setActiveRoom(roomID); - navigate(`/${encodeURI(roomID)}`); + navigate(`/${encodeURIComponent(roomID)}`); }} activeRoom={activeRoom} /> @@ -165,7 +172,7 @@ const RoomList: FC<RoomListProps> = memo(({ sections, rooms }: RoomListProps) => }} onRoomClick={(roomID: string) => { setActiveRoom(roomID); - navigate(`/${encodeURI(roomID)}`); + navigate(`/${encodeURIComponent(roomID)}`); }} activeRoom={activeRoom} /> diff --git a/src/components/roomList/roomListItem/roomListItem.tsx b/src/components/roomList/roomListItem/roomListItem.tsx @@ -1,6 +1,6 @@ import { useInView } from "react-intersection-observer"; import Avatar from "../../avatar/avatar"; -import { FC, memo, useContext, useEffect } from "react"; +import { FC, memo, useContext } from "react"; import { MatrixContext } from "../../../app/sdk/client"; type RoomListItemProps = { @@ -39,31 +39,31 @@ type RoomListItemProps = { }; const RoomListItem: FC<RoomListItemProps> = memo(({ roomId, avatarUrl, displayname, dm = false, online = false, active = false, onClick, hidden }: RoomListItemProps) => { + const matrixClient = useContext(MatrixContext); const { ref, inView } = useInView({ triggerOnce: true, rootMargin: '200px 0px', skip: hidden, + onChange(inView) { + if (inView) { + matrixClient.addInViewRoom(roomId) + } else { + matrixClient.removeInViewRoom(roomId) + } + }, }); - const matrixClient = useContext(MatrixContext); - useEffect(() => { - if (inView) { - matrixClient.addInViewRoom(roomId) - } else { - matrixClient.removeInViewRoom(roomId) - } - }, [inView]) return ( <div ref={ref} onClick={onClick} className="w-full cursor-pointer"> { inView && (active ? ( <div className="flex flex-row gap-2 p-1 bg-gray-300 hover:bg-gray-400 rounded-lg duration-200 ease-in-out items-center"> <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} /> - <span title={displayname} className='text-slate-900 font-normal text-base capitalize max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{displayname}</span> + <span title={displayname} className='text-slate-900 font-normal text-base max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{displayname}</span> </div> ) : ( <div className="flex flex-row gap-2 p-1 hover:bg-gray-300 rounded-lg duration-200 ease-in-out items-center"> <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} /> - <span title={displayname} className='text-slate-900 font-normal text-base capitalize max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{displayname}</span> + <span title={displayname} className='text-slate-900 font-normal text-base max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{displayname}</span> </div> )) } diff --git a/src/index.scss b/src/index.scss @@ -1,4 +1,215 @@ body, html { margin: 0; height: 100%; -} -\ No newline at end of file +} + +a { + @apply text-blue-500; +} + + +/* +Font families defined by this CSS: + +- "Inter" static "traditional" fonts for older web browsers +- "Inter var" single-axis variable fonts for all modern browsers +- "Inter var experimental" multi-axis variable fonts for some modern web browsers + +Use like this in your CSS: + + :root { font-family: 'Inter', sans-serif; } + @supports (font-variation-settings: normal) { + :root { font-family: 'Inter var', sans-serif; } + } + +------------------------- static ------------------------- */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url("font-files/Inter-Thin.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url("font-files/Inter-ThinItalic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url("font-files/Inter-ExtraLight.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 200; + font-display: swap; + src: url("font-files/Inter-ExtraLightItalic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url("font-files/Inter-Light.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 300; + font-display: swap; + src: url("font-files/Inter-LightItalic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("font-files/Inter-Regular.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url("font-files/Inter-Italic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("font-files/Inter-Medium.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url("font-files/Inter-MediumItalic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("font-files/Inter-SemiBold.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url("font-files/Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("font-files/Inter-Bold.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url("font-files/Inter-BoldItalic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url("font-files/Inter-ExtraBold.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 800; + font-display: swap; + src: url("font-files/Inter-ExtraBoldItalic.woff2?v=3.19") format("woff2"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("font-files/Inter-Black.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url("font-files/Inter-BlackItalic.woff2?v=3.19") format("woff2"); +} + + +/* ----------------------- variable ----------------------- */ + +@font-face { + font-family: 'Inter var'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('font-files/Inter-roman.var.woff2?v=3.19') format('woff2'); + font-named-instance: 'Regular'; +} + +@font-face { + font-family: 'Inter var'; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('font-files/Inter-italic.var.woff2?v=3.19') format('woff2'); + font-named-instance: 'Italic'; +} + +/* ----------- experimental multi-axis variable ----------- + +Slant axis is not yet widely supported (as of February 2019) and thus this +multi-axis single-file variable font is opt-in rather than the default. +When using this, you will likely need to set font-variation-settings explicitly, e.g: + * { font-variation-settings: "slnt" 0deg } + .italic { font-variation-settings: "slnt" 10deg } +*/ + +@font-face { + font-family: 'Inter var experimental'; + font-style: oblique 0deg 10deg; + font-weight: 100 900; + font-display: swap; + src: url('font-files/Inter.var.woff2?v=3.19') format('woff2'); +} + +/* Legacy name (became legacy on Feb 2, 2019) */ +@font-face { + font-family: 'Inter var alt'; + font-weight: 100 900; + font-style: normal; + font-named-instance: 'Regular'; + font-display: swap; + src: url("font-files/Inter-roman.var.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter var alt'; + font-weight: 100 900; + font-style: italic; + font-named-instance: 'Italic'; + font-display: swap; + src: url("font-files/Inter-italic.var.woff2?v=3.19") format("woff2"); +} diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx @@ -3,16 +3,67 @@ import Avatar from '../components/avatar/avatar'; import ChatInput from '../components/input/chat/input'; import RoomList, { Section } from '../components/roomList/roomList'; import './MainPage.scss'; -import { useProfile, useRooms, useSpaces } from '../app/sdk/client'; +import { useProfile, useRoom, useRooms, useSpaces } from '../app/sdk/client'; import { Room } from '../app/sdk/room'; -import { memo, useContext } from 'react'; +import { FC, memo, useContext, useState } from 'react'; import { MatrixContext } from '../app/sdk/client'; +import { useParams } from 'react-router-dom'; +import { $generateHtmlFromNodes } from '@lexical/html'; +import { $convertToMarkdownString, TRANSFORMERS } from '@lexical/markdown'; +import MessageEvent from '../components/events/messageEvent'; +import UnknownEvent from '../components/events/unknownEvent'; +import MemberEvent from '../components/events/memberEvent'; +import { IRoomEvent } from '../app/sdk/api/apiTypes'; + +type ChatViewProps = { + /** + * The roomID of the room to display + * If the roomID is empty, the ChatView will be empty + * If the roomID is invalid, the ChatView will be empty + * If the roomID is valid, the ChatView will display the room + * If the roomID is valid, but the room is not joined, the ChatView will display a placeholder + * If the roomID is valid, but the room is not loaded, the ChatView will display a placeholder + */ + roomID?: string +}; + +const ChatView: FC<ChatViewProps> = memo(({ roomID }) => { + const room = useRoom(decodeURIComponent(roomID || "")); + const events = room?.getEvents(); + + // Render events based on the event type and content + const renderEvent = (event: IRoomEvent) => { + switch (event.type) { + case "m.room.message": + return <MessageEvent event={event} roomID={roomID} key={event.event_id} /> + case "m.room.member": + return <MemberEvent event={event} key={event.event_id} /> + case "m.room.redaction": + return <></>; + default: + return <UnknownEvent event={event} key={event.event_id} /> + } + } + + const dedupedEvents = events?.filter((event, index, self) => { + return self.findIndex(e => e.event_id === event.event_id) === index; + }); + + const renderedEvents = dedupedEvents?.map(event => renderEvent(event)); + return <div className='max-w-[130ch] flex flex-col gap-2'>{renderedEvents}</div>; +}); + const MainPage = memo(() => { const profile = useProfile(); const spacesWithRooms = useSpaces(); const rooms = useRooms(); const client = useContext(MatrixContext); + let params = useParams(); + const room = useRoom(decodeURIComponent(params.roomIdOrAlias || "")); + + const [htmlMessage, setHtmlMessage] = useState<string>(""); + const [plainMessage, setPlainMessage] = useState<string>(""); // Filter toplevel spaces. // A toplevel space is a space that is not a child of another space. @@ -113,8 +164,8 @@ const MainPage = memo(() => { console.error('otherRooms has duplicates', otherRoomsDuplicates); } - return <div className='flex flex-row w-full gap-2 min-h-screen h-screen'> - < div className='flex flex-col bg-gradient-to-br from-slate-100 via-gray-200 to-orange-200 border-r-[1px] border-slate-300' > + return <div className='flex flex-row gap-2 min-h-screen h-screen'> + <div className='flex flex-col bg-gradient-to-br from-slate-100 via-gray-200 to-orange-200 border-r-[1px] border-slate-300'> <div className='flex flex-row gap-2 m-2 p-1 items-center border-b-2'> <Avatar displayname={profile.displayname || client.mxid!} avatarUrl={profile?.avatar_url} dm={false} online={false} /> <div className='flex flex-row justify-between items-center w-full'> @@ -123,12 +174,33 @@ const MainPage = memo(() => { </div> </div> <RoomList sections={sections} rooms={otherRooms} /> - </div > - <div className='flex-1 flex flex-col'> - <div className='flex-1'> - </div> - <ChatInput namespace='Editor' onChange={() => { }} onError={(e) => console.error(e)} /> </div> + {room && <div className='flex-1 flex flex-col'> + <div className='pb-2 flex flex-row items-center border-b-2 mt-4 ml-2'> + <Avatar displayname={room.getName()} avatarUrl={room.getAvatarURL()} dm={room.isDM()} online={room.isOnline()} /> + <div className='flex flex-row items-center'> + <h1 className='text-black font-semibold text-lg flex-shrink-0'>{room.getName()}</h1> + <p className='ml-4 text-slate-700 font-normal text-base'>{room.getTopic()}</p> + </div> + </div> + <div className='overflow-y-auto overflow-x-hidden scrollbarSmall mr-2 my-1 flex-1 w-full'> + <ChatView roomID={params.roomIdOrAlias} /> + </div> + <ChatInput namespace='Editor' onChange={(editorState, editor) => { + // Convert editor state to both html and markdown. + // If there is no formatting then just use the plain text. + editorState.read(() => { + const html = $generateHtmlFromNodes(editor); + // TODO: Make sure that we strip any non matrix stuff + setHtmlMessage(html); + console.log(html); + const markdown = $convertToMarkdownString(TRANSFORMERS); + setPlainMessage(markdown); + console.log(markdown); + }); + // TODO: we need some send button + }} onError={(e) => console.error(e)} /> + </div>} </div > }) diff --git a/tailwind.config.js b/tailwind.config.js @@ -1,4 +1,5 @@ /** @type {import('tailwindcss').Config} */ +const defaultTheme = require('tailwindcss/defaultTheme') export default { content: [ "./index.html", @@ -6,7 +7,11 @@ export default { ], darkMode: ['class', '[data-mode="dark"]'], theme: { - extend: {}, + extend: { + fontFamily: { + sans: ['Inter var', ...defaultTheme.fontFamily.sans], + }, + }, }, plugins: [ require('@tailwindcss/forms'),