commit c406e60934da844ac587b96825f87fc7206049c0
parent 7da5e5cf54be3174122867ced89d9eb87ec9cb3c
Author: MTRNord <mtrnord1@gmail.com>
Date: Tue, 2 May 2023 13:03:05 +0200
Fix SYNC, debug INSERT, add spaces list, add TODO for bug in shifting, use Sets, fix room avatar urls, handle somewhat spaces, render roomlist using actual data
Diffstat:
5 files changed, 276 insertions(+), 83 deletions(-)
diff --git a/src/app/sdk/api/apiTypes.ts b/src/app/sdk/api/apiTypes.ts
@@ -228,7 +228,7 @@ export interface SYNC_OP {
}
export interface RoomJson {
- name: string,
+ name?: string,
// List of events
timeline?: IRoomEvent[],
required_state?: IRoomStateEvent[],
@@ -250,7 +250,7 @@ export interface IRoomEvent<Content = any> {
}
export interface IRoomStateEvent<Content = any> extends IRoomEvent<Content> {
- state_key?: string;
+ state_key: string;
}
export interface IRoomMemberContent {
@@ -309,3 +309,26 @@ export interface IRoomAvatarEvent extends IRoomStateEvent<IRoomAvatarContent> {
export function isRoomAvatarEvent(event: IRoomEvent): event is IRoomAvatarEvent {
return event.type === "m.room.avatar";
}
+
+export interface ISpaceChildContent {
+ via: string[];
+ order?: string;
+ suggested?: boolean;
+}
+
+export interface ISpaceChildEvent extends IRoomStateEvent<ISpaceChildContent> { }
+
+export function isSpaceChildEvent(event: IRoomEvent): event is ISpaceChildEvent {
+ return event.type === "m.space.child";
+}
+
+export interface ISpaceParentContent {
+ via: string[];
+ canonical?: boolean;
+}
+
+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
diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts
@@ -6,7 +6,7 @@ import { DBSchema, IDBPDatabase, openDB } from "idb";
export interface MatrixClientEvents {
// Used to notify about changes to the room list
- 'rooms': (rooms: Room[]) => void;
+ 'rooms': (rooms: Set<Room>) => void;
//'delete': (changedCount: number) => void;
}
@@ -149,7 +149,7 @@ export class MatrixClient extends EventEmitter {
instance.roomToRoom.set(room.windowID, roomObj);
}
- instance.emit("rooms", [...instance.roomToRoom.values()]);
+ instance.emit("rooms", new Set(instance.roomToRoom.values()));
}
}
}
@@ -178,8 +178,12 @@ export class MatrixClient extends EventEmitter {
// 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)) { // Checking whether i(current value) present in num(argument)
+ if (!keys.includes(i)) {
+ console.log("Missing:", i)
missing = i;
break
}
@@ -322,6 +326,23 @@ export class MatrixClient extends EventEmitter {
// Sliding Window API
lists: {
+ // TODO: We need a list that fetches all spaces
+ "spaces": {
+ slow_get_all_rooms: true,
+ sort: ["by_name"],
+ required_state: [
+ // needed to build sections
+ ["m.space.child", "*"],
+ ["m.space.parent", "*"],
+ ["m.room.create", ""],
+ // Room Avatar
+ ["m.room.avatar", "*"],
+ ],
+ timeline_limit: timeline_limit,
+ filters: {
+ room_types: ["m.space"]
+ }
+ },
"overview": {
ranges: this.lastRanges["overview"],
sort: ["by_notification_level", "by_recency", "by_name"],
@@ -340,8 +361,8 @@ export class MatrixClient extends EventEmitter {
bump_event_types: ["m.room.message", "m.room.encrypted"],
// Room Subscriptions API
- room_subscriptions: {},
- unsubscribe_rooms: []
+ //room_subscriptions: {},
+ //unsubscribe_rooms: []
})
});
if (!resp.ok) {
@@ -374,7 +395,20 @@ export class MatrixClient extends EventEmitter {
this.roomToRoom.delete(i);
// We start to remember the Room now.
- this.roomToRoom.set(i, new Room(op.room_ids[i], this.hostname!));
+ const newRoom = new Room(op.room_ids[i], this.hostname!);
+ newRoom.setName("Unknown Room");
+ this.roomToRoom.set(i, newRoom);
+ await tx?.store.put({
+ windowID: i,
+ 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 (isInsertOp(op)) {
@@ -395,10 +429,12 @@ export class MatrixClient extends EventEmitter {
const min = Math.min(...this.roomToRoom.keys());
const tx = this.database?.transaction('rooms', 'readwrite');
// If the empty spot is to the right of the position we shift right
- if (emptySpot > position) {
+ if (emptySpot > position && emptySpot <= max) {
console.log("Shifting right");
// Make sure we never shift past 0
for (let i = 0; i < emptySpot && i >= min && i <= max; i++) {
+ console.log(i);
+ // TODO: This fails since we write to the new position but that still isn't moved. meaning we end up with overriding instead of shifting
const room = this.roomToRoom.get(i);
if (room !== undefined) {
await tx?.store.put({
@@ -416,9 +452,10 @@ export class MatrixClient extends EventEmitter {
this.roomToRoom.set(i + 1, room);
}
}
+ console.log("List", this.roomToRoom);
}
// If the empty spot is to the left of the position we shift left
- else if (emptySpot < position) {
+ else if (emptySpot < position && emptySpot <= max) {
console.log("Shifting left");
// Make sure we never shift past 0
for (let i = 0; i > emptySpot && i >= min && i <= max; i--) {
@@ -439,6 +476,7 @@ export class MatrixClient extends EventEmitter {
this.roomToRoom.set(i - 1, room);
}
}
+ console.log("List", this.roomToRoom);
}
console.log("Shifting done");
console.log("Empty spot", this.findNextFreeIndex());
@@ -449,7 +487,23 @@ export class MatrixClient extends EventEmitter {
if (this.roomToRoom.get(position) !== undefined) {
console.error("Shifting failed");
}
- this.roomToRoom.set(position, new Room(op.room_id, this.hostname!));
+ const newRoom = new Room(op.room_id, this.hostname!);
+ console.log(newRoom);
+ newRoom.setName("Unknown Room");
+ this.roomToRoom.set(position, newRoom);
+ const tx = this.database?.transaction('rooms', 'readwrite');
+ await tx?.store.put({
+ windowID: position,
+ roomID: op.room_id,
+ 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 tx = this.database?.transaction('rooms', 'readwrite');
@@ -490,7 +544,9 @@ export class MatrixClient extends EventEmitter {
continue;
}
- roomObj.setName(name);
+ if (name) {
+ roomObj.setName(name);
+ }
roomObj.setNotificationCount(notification_count);
roomObj.setNotificationHighlightCount(notification_highlight_count);
roomObj.setJoinedCount(joined_count);
@@ -519,7 +575,7 @@ export class MatrixClient extends EventEmitter {
}
await tx?.done
if (json.rooms && Object.keys(json.rooms).length > 0) {
- this.emit("rooms", [...this.roomToRoom.values()]);
+ this.emit("rooms", new Set(this.roomToRoom.values()));
}
}
@@ -541,10 +597,59 @@ export class MatrixClient extends EventEmitter {
this.roomsInView = this.roomsInView.filter(room => room !== roomID);
}
- public getRooms(): Room[] {
- return [...this.roomToRoom.values()];
+ public getRooms(): Set<Room> {
+ return new Set(this.roomToRoom.values());
}
+ private getSpaces(): Room[] {
+ return [...this.roomToRoom.values()].filter(room => room.isSpace());
+ }
+
+ public getSpacesWithRooms(): Set<{
+ spaceRoom: Room, children: Set<Room>
+ }> {
+ const spaces = this.getSpaces();
+ const result: Set<{
+ spaceRoom: Room, children: Set<Room>
+ }> = new Set();
+ // Find children of spaces
+ for (const space of spaces) {
+ const childrenIDs = space.getSpaceChildrenIDs();
+
+ const children = new Set([...this.getRooms()].filter(room => childrenIDs.includes(room.roomID)));
+
+ result.add({
+ spaceRoom: space,
+ children: children,
+ });
+ }
+ // 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()) {
+ const parents = room.getSpaceParentIDs();
+ for (const parent of parents) {
+ const parentObj = [...this.getRooms()].find(room => room.roomID === parent.roomID);
+ if (!parentObj) {
+ continue;
+ }
+ const alreadyAddedSpace = [...result].find(space => space.spaceRoom.roomID === parentObj.roomID);
+ if (alreadyAddedSpace) {
+ // Check if room in children
+ if (![...alreadyAddedSpace.children].find(child => child.roomID === room.roomID)) {
+ alreadyAddedSpace.children.add(room);
+ }
+ continue;
+ }
+ // If space not added yet, add it
+ result.add({
+ spaceRoom: parentObj,
+ children: new Set([room]),
+ });
+ }
+ }
+
+ return result;
+ }
private async getLoginFlows(): Promise<ILoginFlows> {
if (!this.hostname) {
@@ -723,11 +828,11 @@ export const MatrixContext = createContext<MatrixClient>(defaultMatrixClient);
// List of rooms
export function useRooms() {
const client = useContext(MatrixContext);
- const [rooms, setRooms] = useState<Room[]>(client.getRooms());
+ const [rooms, setRooms] = useState<Set<Room>>(client.getRooms());
useEffect(() => {
// Listen for room updates
- const listenForRooms = (rooms: Room[]) => {
+ const listenForRooms = (rooms: Set<Room>) => {
setRooms(rooms);
};
client.on("rooms", listenForRooms);
@@ -740,6 +845,28 @@ export function useRooms() {
return rooms;
}
+export function useSpaces() {
+ const client = useContext(MatrixContext);
+ const [spacesWithRooms, setSpacesWithRooms] = useState<Set<{
+ spaceRoom: Room, children: Set<Room>
+ }>>(client.getSpacesWithRooms());
+
+ useEffect(() => {
+ // Listen for room updates
+ const listenForRooms = (_rooms: Set<Room>) => {
+ setSpacesWithRooms(client.getSpacesWithRooms());
+ };
+ client.on("rooms", listenForRooms);
+ // This is a no-op if there is already a sync
+ client.startSync();
+ return () => {
+ client.removeListener("rooms", listenForRooms);
+ }
+ }, [])
+ return spacesWithRooms;
+}
+
+
export function useProfile() {
const client = useContext(MatrixContext);
const [profile, setProfile] = useState<IProfileInfo>({
diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts
@@ -1,4 +1,4 @@
-import { IRoomEvent, IRoomStateEvent, isRoomAvatarEvent, isRoomCreateEvent } from "./api/apiTypes";
+import { IRoomEvent, IRoomStateEvent, isRoomAvatarEvent, isRoomCreateEvent, isSpaceChildEvent, isSpaceParentEvent } from "./api/apiTypes";
export class Room {
private events: IRoomEvent[] = [];
@@ -27,7 +27,7 @@ export class Room {
if (isRoomAvatarEvent(event)) {
const rawAvatarURL = event.content.url;
if (rawAvatarURL?.startsWith("mxc://")) {
- avatarURL = `https://${this.hostname}/_matrix/media/r0/download/${rawAvatarURL.substring(6)}`;
+ avatarURL = `${this.hostname}/_matrix/media/r0/download/${rawAvatarURL.substring(6)}`;
}
}
});
@@ -81,4 +81,34 @@ export class Room {
public getInvitedCount(): number {
return this.invited_count;
}
+
+ public getSpaceChildrenIDs(): string[] {
+ const children: string[] = [];
+ this.stateEvents.forEach((event) => {
+ if (isSpaceChildEvent(event)) {
+ children.push(event.state_key);
+ }
+ });
+ return children;
+ }
+
+ public getSpaceParentIDs(): { roomID: string, canonical: boolean }[] {
+ const parents: { roomID: string, canonical: boolean }[] = [];
+ this.stateEvents.forEach((event) => {
+ if (isSpaceParentEvent(event)) {
+ parents.push({ roomID: event.state_key, canonical: event.content.canonical || false });
+ }
+ });
+ return parents;
+ }
+
+ public isDM(): boolean {
+ // TODO: Implement this
+ return false;
+ }
+
+ public isOnline(): boolean {
+ // TODO: Implement this
+ return false;
+ }
}
\ No newline at end of file
diff --git a/src/components/roomList/roomList.tsx b/src/components/roomList/roomList.tsx
@@ -26,7 +26,7 @@ type Room = {
roomID: string
};
-type Section = {
+export type Section = {
/**
* Section Name. Can be a Space or a Tag
*/
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx
@@ -1,73 +1,85 @@
import { Settings } from 'lucide-react';
import Avatar from '../components/avatar/avatar';
import ChatInput from '../components/input/chat/input';
-import RoomList from '../components/roomList/roomList';
+import RoomList, { Section } from '../components/roomList/roomList';
import './MainPage.scss';
-import { useProfile, useRooms } from '../app/sdk/client';
+import { useProfile, useRooms, useSpaces } from '../app/sdk/client';
+import { Room } from '../app/sdk/room';
export default function MainPage() {
const profile = useProfile();
- const matrixRooms = useRooms();
- console.log(matrixRooms.length);
- const sections = [
- {
- sectionName: "Test Section",
- rooms: [
- {
- roomID: "2",
- displayname: "test1",
- dm: false,
- online: false,
- },
- {
- roomID: "3",
- displayname: "test2",
- dm: false,
- online: false,
- }
- ],
- roomID: "12",
- subsections: [
- {
- sectionName: "Test Subsection",
- rooms: [
- {
- roomID: "1",
- displayname: "test",
- avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg",
- dm: true,
- online: true,
- }
- ],
- roomID: "14",
- subsections: []
+ const spacesWithRooms = useSpaces();
+ const rooms = useRooms();
+
+ // Generate a list of sections.
+ // Each section apart from special toplevel ones is a space.
+ // Each space has a list of rooms and subsections.
+ // Each subsection has a list of rooms and subsections.
+ // Subsections can nest infinitely.
+ // Rooms are always within a section.
+ // A section represents a space.
+ // If a room is not within a space it is in the toplevel section "Other" which is at the end of the list.
+ // The toplevel section "Other" is always present.
+ // The toplevel section "Other" is always the last section.
+ const sections = [...spacesWithRooms].map(space => {
+ const rooms = [...space.children].filter(room => !room.isSpace()).map(room => {
+ return {
+ roomID: room.roomID,
+ displayname: room.getName(),
+ avatarUrl: room.getAvatarURL(),
+ dm: room.isDM(),
+ online: room.isOnline(),
+ }
+ });
+
+ const generateSubsections = (subspace: Room): Section | undefined => {
+ const subspaceMeta = [...spacesWithRooms].find(space => space.spaceRoom.roomID === subspace.roomID);
+ if (subspaceMeta) {
+ const rooms = [...subspaceMeta?.children].map(room => {
+ return {
+ roomID: room.roomID,
+ displayname: room.getName(),
+ avatarUrl: room.getAvatarURL(),
+ dm: room.isDM(),
+ online: room.isOnline(),
+ }
+ });
+
+ return {
+ sectionName: subspace.getName(),
+ rooms: rooms,
+ roomID: subspace.roomID,
+ subsections: [...subspaceMeta?.children]
+ .filter(room => room.isSpace()).map(generateSubsections)
+ .filter(section => section !== undefined) as Section[],
}
- ]
+ }
}
- ];
- const rooms = [
- {
- roomID: "1",
- displayname: "test1234144222222",
- avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg",
- dm: true,
- online: true,
- },
- {
- roomID: "2",
- displayname: "test1",
- dm: false,
- online: false,
- },
- {
- roomID: "3",
- displayname: "test2",
- dm: false,
- online: false,
- },
- ];
+ // Its a little weird sicne there are no children attached to the room object. Only to spacesWithRooms.
+ // Each subsection can have further subsections and rooms.
+ return {
+ sectionName: space.spaceRoom.getName(),
+ rooms: rooms,
+ roomID: space.spaceRoom.roomID,
+ subsections: [...space.children]
+ .filter(room => room.isSpace())
+ .map(generateSubsections),
+ } as Section;
+ });
+
+ // Add the toplevel section "Other" to the end of the list.
+ const otherRooms = [...rooms].filter(room => !room.isSpace()).map(room => {
+ return {
+ roomID: room.roomID,
+ displayname: room.getName(),
+ avatarUrl: room.getAvatarURL(),
+ dm: room.isDM(),
+ online: room.isOnline(),
+ }
+ });
+
return <div className='flex flex-row w-full gap-2 min-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-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'>
{profile?.avatar_url && <Avatar displayname='Test' avatarUrl={profile?.avatar_url} dm={false} online={false} />}
<div className='flex flex-row justify-between items-center w-full'>
@@ -75,12 +87,12 @@ export default function MainPage() {
<Settings size={28} stroke='unset' className='stroke-slate-600 rounded-full hover:bg-slate-300 p-1 cursor-pointer' />
</div>
</div>
- <RoomList sections={sections} rooms={rooms} />
- </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>
- </div>
+ </div >
}
\ No newline at end of file