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 d1e71bb6ee766b830c01a337b4ae36def2f310f0
parent d9e74c0346a8c2a1562175cfb1e124b9132c52a1
Author: MTRNord <mtrnord1@gmail.com>
Date:   Tue,  2 May 2023 21:49:37 +0200

make roomlist better readable and fix OPs in Sync

Diffstat:
Msrc/app/sdk/client.ts | 186++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/components/roomList/roomList.tsx | 4++--
2 files changed, 162 insertions(+), 28 deletions(-)

diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts @@ -1,8 +1,31 @@ -import { createContext, useContext, useEffect, useState } from "react"; -import { IErrorResp, ILoginFlows, ILoginResponse, IProfileInfo, IRateLimitError, IRoomEvent, IRoomStateEvent, ISlidingSyncResp, IWellKnown, isDeleteOp, isInsertOp, isInvalidateOp, isSyncOp } from "./api/apiTypes"; +import { + createContext, + useContext, + useEffect, + useState +} from "react"; +import { + IErrorResp, + ILoginFlows, + ILoginResponse, + IProfileInfo, + IRateLimitError, + IRoomEvent, + IRoomStateEvent, + ISlidingSyncResp, + IWellKnown, + isDeleteOp, + isInsertOp, + isInvalidateOp, + isSyncOp +} from "./api/apiTypes"; import { Room } from "./room"; import EventEmitter from "events"; -import { DBSchema, IDBPDatabase, openDB } from "idb"; +import { + DBSchema, + IDBPDatabase, + openDB +} from "idb"; export interface MatrixClientEvents { // Used to notify about changes to the room list @@ -175,26 +198,6 @@ export class MatrixClient extends EventEmitter { }); } - private findNextFreeIndex(list: string): number { - const keys = [...this.rooms].map(room => room.windowPos[list]); - const max = Math.max(...keys); // Will find highest number - const min = 0; - // Basically "append" - let missing = max + 1; - - console.log("Max:", max) - console.log("Min:", min) - console.log("Keys:", keys) - for (let i = min; i <= max; i++) { - if (!keys.includes(i)) { - console.log("Missing:", i) - missing = i; - break - } - } - return missing; - } - private async setHostname(hostname: string) { if (!hostname.startsWith("https://")) { throw Error("Hostname must start with 'https://'"); @@ -244,6 +247,76 @@ export class MatrixClient extends EventEmitter { this.syncing = false; } + private isIndexInRange(index: number, ranges: number[][]): boolean { + for (const r of ranges) { + if (r[0] < index && index <= r[1]) { + return true + } + } + return false + } + + private shiftRight(listKey: string, ranges: number[][], hi: number, low: number) { + // l h + // 0,1,2,3,4 <- before + // 0,1,2,2,3 <- after, hi is deleted and low is duplicated + for (let i = hi - 1; i > low - 1; i--) { + if (this.isIndexInRange(i, ranges)) { + const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i + 1); + if (roomObj) { + roomObj.windowPos[listKey] = (i); + } + } + } + } + + private shiftLeft(listKey: string, ranges: number[][], hi: number, low: number) { + // l h + // 0,1,2,3,4 <- before + // 0,1,3,4,4 <- after, low is deleted and hi is duplicated + for (let i = low + 1; i < hi + 1; i++) { + if (this.isIndexInRange(i, ranges)) { + const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i - 1); + if (roomObj) { + roomObj.windowPos[listKey] = (i); + } + } + } + + } + + private removeEntry(listKey: string, ranges: number[][], index: number) { + // work out the max index + let max = -1; + const indexes = [...this.rooms].map(room => room.windowPos[listKey]); + for (const n in indexes) { + if (Number(n) > max) { + max = Number(n); + } + } + if (max < 0 || index > max) { + return; + } + // Everything higher than the gap needs to be shifted left. + this.shiftLeft(listKey, ranges, max, index); + } + + private addEntry(listKey: string, ranges: number[][], index: number): void { + // work out the max index + let max = -1; + const indexes = [...this.rooms].map(room => room.windowPos[listKey]); + for (const n in indexes) { + if (Number(n) > max) { + max = Number(n); + } + } + if (max < 0 || index > max) { + return; + } + // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element + this.shiftRight(listKey, ranges, max + 1, index); + } + private async sync() { if (!this.isLoggedIn) { throw Error("Not logged in"); @@ -257,7 +330,9 @@ export class MatrixClient extends EventEmitter { "overview": number[][]; [key: string]: number[][]; } = { - "overview": [[0, 10]] + "overview": [[0, 10]], + // Needed for calcs + "spaces": [[0, Number.MAX_SAFE_INTEGER]] }; let timeline_limit = 1; if (!this.initialSync) { @@ -404,6 +479,7 @@ export class MatrixClient extends EventEmitter { }); await syncInfoTX?.done; + let gapIndex = -1; for (const listKey in json.lists) { const list = json.lists[listKey]; if (list.ops) { @@ -437,9 +513,49 @@ export class MatrixClient extends EventEmitter { await tx?.done; } else if (isInsertOp(op)) { console.log("Got INSERT OP", op); - console.warn("INSERT not implemented") + const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === op.index); + if (roomObj) { + if (gapIndex < 0) { + // we haven't been told where to shift from, so make way for a new room entry. + this.addEntry(listKey, this.lastRanges[listKey], op.index); + } else if (gapIndex > op.index) { + // the gap is further down the list, shift every element to the right + // starting at the gap so we can just shift each element in turn: + // [A,B,C,_] gapIndex=3, op.index=0 + // [A,B,C,C] i=3 + // [A,B,B,C] i=2 + // [A,A,B,C] i=1 + // Terminate. We'll assign into op.index next. + this.shiftRight(listKey, this.lastRanges[listKey], gapIndex, op.index); + } else if (gapIndex < op.index) { + // the gap is further up the list, shift every element to the left + // starting at the gap so we can just shift each element in turn + this.shiftLeft(listKey, this.lastRanges[listKey], op.index, gapIndex); + } + } + // TODO: + gapIndex = -1; + const tx = this.database?.transaction('rooms', 'readwrite'); + // We start to remember the Room now. + const newRoom = new Room(op.room_id, this.hostname!); + newRoom.setName("Unknown Room"); + newRoom.windowPos[listKey] = op.index; + this.rooms.add(newRoom); + await tx?.store.put({ + windowPos: newRoom.windowPos, + roomID: newRoom.roomID, + name: newRoom.getName(), + notification_count: newRoom.getNotificationCount(), + highlight_count: newRoom.getNotificationHighlightCount(), + joined_count: newRoom.getJoinedCount(), + invited_count: newRoom.getInvitedCount(), + avatarUrl: newRoom.getAvatarURL(), + isSpace: newRoom.isSpace(), + }); + await tx?.done; } 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'); @@ -447,6 +563,11 @@ export class MatrixClient extends EventEmitter { 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); + } + gapIndex = op.index; } else if (isInvalidateOp(op)) { const tx = this.database?.transaction('rooms', 'readwrite'); for (let i = op.range[0]; i <= op.range[1]; i++) { @@ -460,6 +581,11 @@ export class MatrixClient extends EventEmitter { await tx?.done; } } + 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); + } } } const tx = this.database?.transaction('rooms', 'readwrite'); @@ -538,7 +664,15 @@ export class MatrixClient extends EventEmitter { } private getSpaces(): Room[] { - return [...this.rooms].filter(room => room.isSpace()); + return [...this.rooms].filter(room => room.isSpace()).sort((a: Room, b: Room) => { + if (a.getName() < b.getName()) { + return -1; + } + if (a.getName() > b.getName()) { + return 1; + } + return 0; + }); } public getSpacesWithRooms(): Set<{ diff --git a/src/components/roomList/roomList.tsx b/src/components/roomList/roomList.tsx @@ -103,9 +103,9 @@ const RoomListRooms: FC<RoomListRoomsProps> = ({ sectionID, rooms, onClick, acti } const RoomSection: FC<{ section: Section, onRoomClick: (roomID: string) => void, activeRoom: string | undefined }> = ({ section, onRoomClick, activeRoom }: { section: Section, onRoomClick: (roomID: string) => void, activeRoom: string | undefined }) => { - const [hidden, setHidden] = useState<boolean>(false); + const [hidden, setHidden] = useState<boolean>(true); return ( - <div key={section.roomID} className="flex flex-col gap-1"> + <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)}> {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>