commit 1090504f8ca810d401283b6ddc338f6cd4b34956
parent ab20ae968aea0d2a10a00fe9153dffbc46d3a22f
Author: MTRNord <mtrnord1@gmail.com>
Date: Mon, 1 May 2023 22:58:16 +0200
Add storing data and make sure to correctly resolve the avatarURL
Diffstat:
2 files changed, 223 insertions(+), 30 deletions(-)
diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts
@@ -1,13 +1,15 @@
-import { createContext, useEffect, useState } from "react";
-import { IErrorResp, ILoginFlows, ILoginResponse, IRateLimitError, ISlidingSyncResp, IWellKnown, isDeleteOp, isInsertOp, isInvalidateOp, isSyncOp } from "./api/apiTypes";
+import { createContext, useContext, useEffect, useState } from "react";
+import { IErrorResp, ILoginFlows, ILoginResponse, 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";
export interface MatrixClientEvents {
// Used to notify about changes to the room list
'rooms': (rooms: Room[]) => void;
//'delete': (changedCount: number) => void;
}
+
export declare interface MatrixClient {
on<U extends keyof MatrixClientEvents>(
event: U, listener: MatrixClientEvents[U]
@@ -18,51 +20,171 @@ export declare interface MatrixClient {
): boolean;
}
+interface MatrixDB extends DBSchema {
+ rooms: {
+ // Same as roomToRoom map
+ key: number;
+ value: {
+ windowID: number;
+ roomID: string;
+ name: string;
+ notification_count: number;
+ highlight_count: number;
+ joined_count: number;
+ invited_count: number;
+ events?: IRoomEvent[];
+ stateEvents?: IRoomStateEvent[];
+ avatarUrl?: string;
+ isSpace: boolean;
+ };
+ indexes: {
+ // Get room in window order
+ 'by-windowID': number;
+ };
+ };
+ loginInfo: {
+ // login info
+ value: {
+ userId: string;
+ device_id?: string;
+ hostname?: string;
+ slidingSyncHostname?: string;
+ access_token?: string;
+ };
+ // User ID
+ key: string;
+ };
+ syncInfo: {
+ // sync info
+ value: {
+ userId: string;
+ syncPos: string;
+ initialSync: boolean;
+ };
+ // User ID
+ key: string;
+ }
+}
export class MatrixClient extends EventEmitter {
private static _instance: MatrixClient;
- // TODO: This needs to be stored
private access_token?: string;
- // TODO: This needs to be stored
// @ts-ignore Not used currently but needed
private device_id?: string;
- // TODO: This needs to be stored
// @ts-ignore Not used currently but needed
private mxid?: string;
- // TODO: This needs to be stored
// Hostname including "https://"
private hostname?: string;
- // TODO: This needs to be stored
private slidingSyncHostname?: string;
private syncing = false;
private roomsInView: string[] = [];
- // TODO: This needs to be stored
private roomToRoom: Map<number, Room> = new Map();
- // TODO: This needs to be stored
private syncPos?: string;
- // TODO: This needs to be stored
private initialSync = true;
+ private database?: IDBPDatabase<MatrixDB>;
public get isLoggedIn(): boolean {
return this.access_token !== undefined;
}
- public static get Instance() {
- // Do you need arguments? Make it a regular static method instead.
- return this._instance || (this._instance = new this());
+ public static async Instance() {
+ const instance = this._instance || (this._instance = new this());
+ // Load from database
+ if (!instance.database) {
+ await instance.createDatabase();
+ }
+ const tx = instance.database?.transaction('loginInfo', 'readonly');
+ // We dont know the mxid so we just get all and use the first. In theory this allows for multiple accounts
+ const loginInfo = await tx?.store.getAll();
+ await tx?.done;
+ if (loginInfo && loginInfo.length > 0) {
+ instance.mxid = loginInfo[0].userId;
+ instance.hostname = loginInfo[0].hostname;
+ instance.slidingSyncHostname = loginInfo[0].slidingSyncHostname;
+ instance.access_token = loginInfo[0].access_token;
+ instance.device_id = loginInfo[0].device_id;
+
+ // Load sync info
+ const syncTx = instance.database?.transaction('syncInfo', 'readonly');
+ const syncInfo = await syncTx?.store.get(instance.mxid!);
+ await syncTx?.done;
+
+ if (syncInfo) {
+ instance.syncPos = syncInfo.syncPos;
+ instance.initialSync = syncInfo.initialSync;
+ }
+
+ // Load rooms
+ const roomTx = instance.database?.transaction('rooms', 'readonly');
+ const rooms = await roomTx?.store.getAll();
+ await roomTx?.done;
+
+ if (rooms) {
+ for (const room of rooms) {
+ const roomObj = new Room(room.roomID, instance.hostname!);
+ roomObj.setInvitedCount(room.invited_count);
+ roomObj.setJoinedCount(room.joined_count);
+ roomObj.setNotificationCount(room.notification_count);
+ roomObj.setNotificationHighlightCount(room.highlight_count);
+ roomObj.setName(room.name);
+ if (room.events) {
+ roomObj.addEvents(room.events);
+ }
+ if (room.stateEvents) {
+ roomObj.addStateEvents(room.stateEvents);
+ }
+
+ instance.roomToRoom.set(room.windowID, roomObj);
+ }
+ instance.emit("rooms", [...instance.roomToRoom.values()]);
+ }
+ }
+
+ return instance;
+ }
+
+ private async createDatabase() {
+ this.database = await openDB<MatrixDB>("matrix", 1, {
+ upgrade(db) {
+ const roomStore = db.createObjectStore('rooms', { keyPath: 'windowID' });
+ roomStore.createIndex('by-windowID', 'windowID');
+ db.createObjectStore('loginInfo', { keyPath: 'userId' });
+ db.createObjectStore('syncInfo', { keyPath: 'userId' });
+ }
+ });
}
- private set setHostname(hostname: string) {
+ private async setHostname(hostname: string) {
if (!hostname.startsWith("https://")) {
throw Error("Hostname must start with 'https://'");
}
+ if (!this.database) {
+ await this.createDatabase();
+ }
+
+ // Write to database
+ const tx = this.database?.transaction('loginInfo', 'readwrite');
+ await tx?.store.put({
+ userId: this.mxid!,
+ hostname: hostname,
+ slidingSyncHostname: this.slidingSyncHostname,
+ access_token: this.access_token,
+ device_id: this.device_id,
+ });
+ await tx?.done
+
+ // Set in memory
this.hostname = hostname;
+
}
public async startSync() {
if (!this.isLoggedIn) {
throw Error("Not logged in");
}
+ if (!this.database) {
+ await this.createDatabase();
+ }
if (this.syncing) {
return;
}
@@ -137,8 +259,6 @@ export class MatrixClient extends EventEmitter {
url = `${this.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=30000&pos=${this.syncPos}`
}
-
- console.log(url)
const resp = await fetch(url, {
method: "POST",
headers: {
@@ -185,31 +305,53 @@ export class MatrixClient extends EventEmitter {
}
const json = await resp.json() as ISlidingSyncResp;
this.syncPos = json.pos;
+
+
+ const syncInfoTX = this.database?.transaction('syncInfo', 'readwrite');
+ await syncInfoTX?.store.put({
+ userId: this.mxid!,
+ syncPos: this.syncPos,
+ initialSync: this.initialSync,
+ });
+ await syncInfoTX?.done;
+
for (const listKey in json.lists) {
const list = json.lists[listKey];
if (list.ops) {
for (const op of list.ops) {
if (isSyncOp(op)) {
+ const tx = this.database?.transaction('rooms', 'readwrite');
for (let i = op.range[0]; i <= op.range[1]; i++) {
// We shall first forget about these and "startover"
+ await tx?.store.delete(i);
this.roomToRoom.delete(i);
// We start to remember the Room now.
- this.roomToRoom.set(i, new Room(op.room_ids[i]));
+ this.roomToRoom.set(i, new Room(op.room_ids[i], this.hostname!));
}
+ await tx?.done;
} else if (isInsertOp(op)) {
console.log("Got INSERT OP", op);
+ this.roomToRoom.set(op.index, new Room(op.room_id, this.hostname!));
} else if (isDeleteOp(op)) {
+ console.log("Got DELETE OP", op);
+ const tx = this.database?.transaction('rooms', 'readwrite');
+ await tx?.store.delete(op.index);
+ await tx?.done;
this.roomToRoom.delete(op.index);
} else if (isInvalidateOp(op)) {
+ const tx = this.database?.transaction('rooms', 'readwrite');
for (let i = op.range[0]; i <= op.range[1]; i++) {
// We shall first forget about these and "startover"
+ await tx?.store.delete(i);
this.roomToRoom.delete(i);
}
+ await tx?.done;
}
}
}
}
+ const tx = this.database?.transaction('rooms', 'readwrite');
for (const roomID in json.rooms) {
const room = json.rooms[roomID];
const name = room.name;
@@ -238,10 +380,28 @@ export class MatrixClient extends EventEmitter {
roomObj.setInvitedCount(invited_count);
roomObj.addEvents(events);
if (required_state) {
- roomObj.addRequiredState(required_state);
+ roomObj.addStateEvents(required_state);
}
+
+ // Write to database
+ await tx?.store.put({
+ windowID: [...this.roomToRoom.keys()].find(key => this.roomToRoom.get(key) === roomObj) as number,
+ roomID: roomObj.roomID,
+ name: roomObj.getName(),
+ notification_count: roomObj.getNotificationCount(),
+ highlight_count: roomObj.getNotificationHighlightCount(),
+ joined_count: roomObj.getJoinedCount(),
+ invited_count: roomObj.getInvitedCount(),
+ events: events,
+ stateEvents: required_state,
+ avatarUrl: roomObj.getAvatarURL(),
+ isSpace: roomObj.isSpace(),
+ });
+ }
+ await tx?.done
+ if (json.rooms && Object.keys(json.rooms).length > 0) {
+ this.emit("rooms", [...this.roomToRoom.values()]);
}
- this.emit("rooms", [...this.roomToRoom.values()]);
}
/**
@@ -262,6 +422,10 @@ export class MatrixClient extends EventEmitter {
this.roomsInView = this.roomsInView.filter(room => room !== roomID);
}
+ public getRooms(): Room[] {
+ return [...this.roomToRoom.values()];
+ }
+
private async getLoginFlows(): Promise<ILoginFlows> {
if (!this.hostname) {
@@ -290,14 +454,30 @@ export class MatrixClient extends EventEmitter {
}
public async passwordLogin(username: string, password: string, triesLeft = 5) {
- this.setHostname = `https://${username.split(':')[1]}`;
+ if (!this.database) {
+ await this.createDatabase();
+ }
+ this.mxid = username;
+ await this.setHostname(`https://${username.split(':')[1]}`);
try {
const well_known = await this.getWellKnown();
if (well_known["m.homeserver"]?.base_url) {
- this.setHostname = well_known["m.homeserver"].base_url;
+ await this.setHostname(well_known["m.homeserver"].base_url);
}
if (well_known["org.matrix.msc3575.proxy"]?.url) {
+ // Write to database
+ const tx = this.database?.transaction('loginInfo', 'readwrite');
+ await tx?.store.put({
+ userId: this.mxid!,
+ hostname: this.hostname,
+ slidingSyncHostname: well_known["org.matrix.msc3575.proxy"].url,
+ access_token: this.access_token,
+ device_id: this.device_id,
+ });
+ await tx?.done
+
+ // Set the sliding sync proxy
this.slidingSyncHostname = well_known["org.matrix.msc3575.proxy"].url;
} else {
// TODO: we might want to check for synapse support somehow
@@ -340,6 +520,16 @@ export class MatrixClient extends EventEmitter {
await this.passwordLogin(username, password, triesLeft - 1);
}
if (isLoginResponse(json)) {
+ // Write to database
+ const tx = this.database?.transaction('loginInfo', 'readwrite');
+ await tx?.store.put({
+ userId: json.user_id!,
+ hostname: this.hostname,
+ slidingSyncHostname: this.slidingSyncHostname,
+ access_token: json.access_token,
+ device_id: json.device_id,
+ });
+ await tx?.done
this.access_token = json.access_token;
this.device_id = json.device_id;
this.mxid = json.user_id;
@@ -359,21 +549,21 @@ function isErrorResp(arg: any): arg is IErrorResp {
return arg.errcode !== undefined;
}
-export const defaultMatrixClient = MatrixClient.Instance;
-export const MatrixContext = createContext(defaultMatrixClient);
+export const defaultMatrixClient: MatrixClient = await MatrixClient.Instance();
+export const MatrixContext = createContext<MatrixClient>(defaultMatrixClient);
// List of rooms
export function useRooms() {
- const client = MatrixClient.Instance;
- const [rooms, setRooms] = useState<Room[]>([]);
+ const client = useContext(MatrixContext);
+ const [rooms, setRooms] = useState<Room[]>(client.getRooms());
useEffect(() => {
- // This is a no-op if there is already a sync
- client.startSync();
// Listen for room updates
client.on("rooms", (rooms: Room[]) => {
setRooms(rooms);
});
+ // This is a no-op if there is already a sync
+ client.startSync();
}, [])
return rooms;
}
\ No newline at end of file
diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts
@@ -11,13 +11,13 @@ export class Room {
private invited_count: number = 0;
- constructor(public roomID: string) { }
+ constructor(public roomID: string, private hostname: string) { }
public addEvents(event: IRoomEvent[]) {
this.events.push(...event);
}
- public addRequiredState(state: IRoomStateEvent[]) {
+ public addStateEvents(state: IRoomStateEvent[]) {
this.stateEvents.push(...state);
}
@@ -25,7 +25,10 @@ export class Room {
let avatarURL: string | undefined = undefined;
this.stateEvents.forEach((event) => {
if (isRoomAvatarEvent(event)) {
- avatarURL = event.content.url;
+ const rawAvatarURL = event.content.url;
+ if (rawAvatarURL?.startsWith("mxc://")) {
+ avatarURL = `https://${this.hostname}/_matrix/media/r0/download/${rawAvatarURL.substring(6)}`;
+ }
}
});
return avatarURL;