cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

commit a78986c734c47f5c704c509f1327954ad451cf69
parent 6f41b5fb53179418dd96adfa381d90a19552a3d2
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sat,  6 May 2023 17:54:02 +0200

Improve loading of roomlist, dont show subspaces as subrooms, prevent showing tombstoned rooms

Diffstat:
Msrc/app/sdk/client.ts | 171+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/app/sdk/room.ts | 10++++++++++
Msrc/components/roomList/roomList.tsx | 43++++++++++++++++++++++++++++++-------------
Msrc/components/roomList/roomListItem/roomListItem.tsx | 2+-
Msrc/pages/MainPage.tsx | 51++++++++++++++++++++++++++++++++++++++++++++++-----
5 files changed, 189 insertions(+), 88 deletions(-)

diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts @@ -110,6 +110,7 @@ export class MatrixClient extends EventEmitter { private slidingSyncHostname?: string; private syncing = false; private roomsInView: string[] = []; + private spacesInView: string[] = []; private spaceOpen: string[] = []; private rooms: Set<Room> = new Set(); private syncPos?: string; @@ -630,11 +631,11 @@ export class MatrixClient extends EventEmitter { // This is the initial sync case for each list let lists_ranges: { "overview": number[][]; + "spaces": number[][]; [key: string]: number[][]; } = { - "overview": [[0, 50]], - // Needed for calcs - "spaces": [[0, Number.MAX_SAFE_INTEGER]] + "overview": [[0, 20]], + "spaces": [[0, 20]] }; for (const space of this.spaceOpen) { if (space === "other") { continue } @@ -650,72 +651,88 @@ export class MatrixClient extends EventEmitter { subscription_limit = 50; // Calculate overlap between this.roomsInView and this.roomToRoomID and then // calculate the ranges for each list - const rawRangeInView = new Set([...this.rooms] + let rawRangeInView = new Set([...this.rooms] .filter(room => this.roomsInView.includes(room.roomID)) - .map(room => room.windowPos[list]).sort()) + .map(room => room.windowPos[list]).sort().filter(x => x !== undefined && x !== null)) - - if (rawRangeInView.size === 0) { - // If there are no rooms in view, we can skip this list - continue; + if (list === "spaces") { + // If we are syncing the spaces list, we need to use the spaceInView list instead + rawRangeInView = new Set([...this.rooms] + .filter(room => this.spacesInView.includes(room.roomID)) + .map(room => room.windowPos[list]).sort().filter(x => x !== undefined && x !== null)) } - // 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,16] - rawRangeInView.add([...rawRangeInView][rawRangeInView.size - 1] + 5); - - // 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) => { - if (i === 0) { - // [1,2,3,4] -> [[1,1]] - acc.push([cur, cur]); - return acc; - } - // Cur = 2, arr = [1,2,3,4], arr[i - 1] + 1 = 2 then - if (cur === arr[i - 1] + 1) { - // [1,2,3,4,7] -> [[1,2]] - acc[acc.length - 1][1] = cur; - return acc; - } - // Else [1,2,3,4,7] -> [[1,2], [7,7]] - acc.push([cur, cur]); - return acc; - }, [] as [number, number][]); - - // Sort by the first element of each range and add to the object - const sorted = rangesInView.sort((a, b) => a[0] - b[0]); - - const deduped = []; - deduped.push(sorted[0]); - for (let i = 1; i < sorted.length; i++) { - let ok; - for (let j = 0; j < i; j++) { - if (deduped[j].length != sorted[i].length) { - continue - } - ok = false; - for (let k = 0; k < sorted[i].length; k++) { - if (sorted[i][k] != sorted[j][k]) { - ok = true - } - }; - if (ok == false) { - break - }; - } - if (ok) { - deduped.push(sorted[i]) - }; + // 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,16] + // for (const [i, v] of rawRangeInView.entries()) { + // if (i % 2 === 0) { + // rawRangeInView.add(v + 5); + // } + // } + + // // 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) => { + // if (i === 0) { + // // [1,2,3,4] -> [[1,1]] + // acc.push([cur, cur]); + // return acc; + // } + // // Cur = 2, arr = [1,2,3,4], arr[i - 1] + 1 = 2 then + // if (cur === arr[i - 1] + 1) { + // // [1,2,3,4,7] -> [[1,2]] + // acc[acc.length - 1][1] = cur; + // return acc; + // } + // // Else [1,2,3,4,7] -> [[1,2], [7,7]] + // acc.push([cur, cur]); + // return acc; + // }, [] as [number, number][]); + + // // Sort by the first element of each range and add to the object + // const sorted = rangesInView.sort((a, b) => a[0] - b[0]); + + // const deduped = []; + // deduped.push(sorted[0]); + // for (let i = 1; i < sorted.length; i++) { + // let ok; + // for (let j = 0; j < i; j++) { + // if (deduped[j].length != sorted[i].length) { + // continue + // } + // ok = false; + // for (let k = 0; k < sorted[i].length; k++) { + // if (sorted[i][k] != sorted[j][k]) { + // ok = true + // } + // }; + // if (ok == false) { + // break + // }; + // } + // if (ok) { + // deduped.push(sorted[i]) + // }; + // } + + + // // Deduplicate ranges + // const deduped_final = deduped + // .filter(subarray => subarray.length === 2) + // .filter(subarray => subarray[0] !== undefined && subarray[1] !== undefined && subarray[0] !== null && subarray[1] !== null) + + if (rawRangeInView.size !== 0) { + console.log("changed:", rawRangeInView) + const minimum = Math.min(...rawRangeInView); + const maximum = Math.max(...rawRangeInView); + + lists_ranges[list] = [[Math.max(minimum - 10, 0), maximum + 10]]; } - - - // Deduplicate ranges - const deduped_final = deduped - .filter(subarray => subarray.length === 2) - .filter(subarray => subarray[0] !== undefined && subarray[1] !== undefined && subarray[0] !== null && subarray[1] !== null) - - lists_ranges[list] = deduped_final; } } @@ -756,13 +773,15 @@ export class MatrixClient extends EventEmitter { // Sliding Window API lists: { "spaces": { - slow_get_all_rooms: true, + ranges: this.lastRanges["spaces"], + // slow_get_all_rooms: true, sort: ["by_name"], required_state: [ // needed to build sections ["m.space.child", "*"], ["m.space.parent", "*"], ["m.room.create", ""], + ["m.room.tombstone", ""], // Room Avatar ["m.room.avatar", "*"], // Room Topic @@ -775,7 +794,7 @@ export class MatrixClient extends EventEmitter { ["m.room.encryption", ""], ["m.room.history_visibility", ""], ], - timeline_limit: timeline_limit, + timeline_limit: 0, filters: { room_types: ["m.space"] } @@ -788,6 +807,7 @@ export class MatrixClient extends EventEmitter { ["m.space.child", "*"], ["m.space.parent", "*"], ["m.room.create", ""], + ["m.room.tombstone", ""], // Room Avatar ["m.room.avatar", "*"], // Room Topic @@ -825,14 +845,15 @@ export class MatrixClient extends EventEmitter { body.lists = {}; } body.lists[space] = { - slow_get_all_rooms: true, - //ranges: this.lastRanges[space], - // sort: ["by_notification_level", "by_recency", "by_name"], + //slow_get_all_rooms: true, + ranges: this.lastRanges[space], + sort: ["by_notification_level", "by_recency", "by_name"], required_state: [ // needed to build sections ["m.space.child", "*"], ["m.space.parent", "*"], ["m.room.create", ""], + ["m.room.tombstone", ""], // Room Avatar ["m.room.avatar", "*"], // Room Topic @@ -861,6 +882,7 @@ export class MatrixClient extends EventEmitter { ["m.space.child", "*"], ["m.space.parent", "*"], ["m.room.create", ""], + ["m.room.tombstone", ""], // Room Avatar ["m.room.avatar", "*"], // Room Topic @@ -1215,6 +1237,14 @@ export class MatrixClient extends EventEmitter { this.roomsInView = this.roomsInView.filter(room => room !== roomID); } + public addInViewSpace(roomID: string) { + this.spacesInView.push(roomID); + } + + public removeInViewSpace(roomID: string) { + this.spacesInView = this.spacesInView.filter(room => room !== roomID); + } + public addSpaceOpen(roomID: string) { if (roomID === "other") { return; @@ -1242,7 +1272,7 @@ export class MatrixClient extends EventEmitter { } private getSpaces(): Room[] { - return [...this.rooms].filter(room => room.isSpace()).sort((a: Room, b: Room) => { + return [...this.rooms].filter(room => room.isSpace() && !room.isTombstoned()).sort((a: Room, b: Room) => { if (a.getName() < b.getName()) { return -1; } @@ -1274,6 +1304,9 @@ export class MatrixClient extends EventEmitter { // Find spaces of parents // Check parents of each room and if we have a parent make sure to add it to the result unless already added for (const room of this.getRooms()) { + if (room.isSpace() || room.isTombstoned()) { + continue; + } const parents = room.getSpaceParentIDs(); for (const parent of parents) { const parentObj = [...this.getRooms()].find(room => room.roomID === parent.roomID); diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts @@ -65,6 +65,16 @@ export class Room extends EventEmitter { return this.stateEvents; } + public isTombstoned(): boolean { + let isTombstoned: boolean = false; + this.stateEvents.forEach((event) => { + if (event.type === "m.room.tombstone") { + isTombstoned = true; + } + }); + return isTombstoned; + } + public getAvatarURL(): string | undefined { let avatarURL: string | undefined = undefined; this.stateEvents.forEach((event) => { diff --git a/src/components/roomList/roomList.tsx b/src/components/roomList/roomList.tsx @@ -1,9 +1,10 @@ -import { FC, memo, useContext, useState } from "react"; +import { FC, memo, useContext, useEffect, 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"; +import { useInView } from "react-intersection-observer"; type Room = { /** @@ -108,25 +109,41 @@ 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); - } + const { ref, inView } = useInView({ + triggerOnce: true, + threshold: 1, + onChange(inView) { + if (section.roomID !== "other") { + if (inView) { + matrixClient.addInViewRoom(section.roomID) + } else { + matrixClient.removeInViewRoom(section.roomID) + } + } + }, + }); + useEffect(() => { + if (hidden) { + matrixClient.removeSpaceOpen(section.roomID); + } else { + matrixClient.addSpaceOpen(section.roomID); + } + }, [hidden]) return ( - <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-base capitalize max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{section.sectionName}</span> - </div > - {!hidden && (<RoomListRooms + <div ref={ref} key={section.roomID} className="flex flex-col gap-1 pl-4 select-none"> + { + inView && <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-base capitalize max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{section.sectionName}</span> + </div >} + {!hidden && inView && (<RoomListRooms hidden={hidden} sectionID={section.roomID} rooms={section.rooms} onClick={onRoomClick} activeRoom={activeRoom} />)} - {!hidden && ( + {!hidden && inView && ( section.subsections.map(section => { return ( <RoomSection diff --git a/src/components/roomList/roomListItem/roomListItem.tsx b/src/components/roomList/roomListItem/roomListItem.tsx @@ -42,8 +42,8 @@ const RoomListItem: FC<RoomListItemProps> = memo(({ roomId, avatarUrl, displayna const matrixClient = useContext(MatrixContext); const { ref, inView } = useInView({ triggerOnce: true, - rootMargin: '200px 0px', skip: hidden, + threshold: 1, onChange(inView) { if (inView) { matrixClient.addInViewRoom(roomId) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx @@ -161,26 +161,48 @@ const MainPage = memo(() => { // A toplevel space is a space that is not a child of another space. // We can not rely only on the parent. We need to check in both directions. const toplevelSpaces = [...spacesWithRooms].filter(({ spaceRoom }) => { + const not_tombstoned = !spaceRoom.isTombstoned(); const not_a_child = ![...spacesWithRooms].some(({ children: otherChildren }) => { return [...otherChildren].some(room => room.roomID === spaceRoom.roomID); }); // Also check if there are no parents set const no_parents = spaceRoom.getSpaceParentIDs().length === 0; - return not_a_child && no_parents; + return not_a_child && no_parents && not_tombstoned; }); // Filter rooms which are not part of any space and are not a space. // A room is not part of any space if it is not a child of any space. // A room is not a space if it has not any space as parent. const leftOverRooms = [...rooms].filter(room => { + const not_tombstoned = !room.isTombstoned(); const not_a_child = ![...spacesWithRooms].some(({ children }) => { return [...children].some(otherRoom => otherRoom.roomID === room.roomID); }); const no_parents = room.getSpaceParentIDs().length === 0; const not_a_space = !room.isSpace(); - return not_a_child && no_parents && not_a_space; + return not_a_child && no_parents && not_a_space && not_tombstoned; + }).sort((a, b) => { + // Sort rooms by sliding sync list order of overview list, + + // Get the index of the room in the sync list. + const a_index = a.windowPos["overview"]; + const b_index = b.windowPos["overview"]; + + // If the room is not in the sync list, it will be at the end of the list. + // This is the same as the index being -1. + // So we need to check for that. + if (a_index === -1) { + return 1; + } + if (b_index === -1) { + return -1; + } + + // If the room is in the sync list, we can compare the indexes. + return a_index - b_index; }); + // Generate a list of sections. // Each section apart from special toplevel ones is a space. // Each space has a list of rooms and subsections. @@ -192,7 +214,26 @@ const MainPage = memo(() => { // The toplevel section "Other" is always present. // The toplevel section "Other" is always the last section. const sections = toplevelSpaces.map(space => { - const rooms = [...space.children].filter(room => !room.isSpace()).map(room => { + const rooms = [...space.children].filter(room => !room.isSpace() && !room.isTombstoned()).sort((a, b) => { + // Sort rooms by sliding sync list order of the spaces list, + + // Get the index of the room in the sync list. + const a_index = a.windowPos[space.spaceRoom.roomID]; + const b_index = b.windowPos[space.spaceRoom.roomID]; + + // If the room is not in the sync list, it will be at the end of the list. + // This is the same as the index being -1. + // So we need to check for that. + if (a_index === -1) { + return 1; + } + if (b_index === -1) { + return -1; + } + + // If the room is in the sync list, we can compare the indexes. + return a_index - b_index; + }).map(room => { return { roomID: room.roomID, displayname: room.getName(), @@ -205,7 +246,7 @@ const MainPage = memo(() => { const generateSubsections = (subspace: Room): Section | undefined => { const subspaceMeta = [...spacesWithRooms].find(space => space.spaceRoom.roomID === subspace.roomID); if (subspaceMeta) { - const rooms = [...subspaceMeta?.children].map(room => { + const rooms = [...subspaceMeta?.children].filter(room => !room.isSpace() && !room.isTombstoned()).map(room => { return { roomID: room.roomID, displayname: room.getName(), @@ -220,7 +261,7 @@ const MainPage = memo(() => { rooms: rooms, roomID: subspace.roomID, subsections: [...subspaceMeta?.children] - .filter(room => room.isSpace()).map(generateSubsections) + .filter(room => room.isSpace() && !room.isTombstoned()).map(generateSubsections) .filter(section => section !== undefined) as Section[], } }