slidingSync.ts (34296B)
1 import { MatrixClient } from "./client"; 2 import { ISlidingSyncReq, ISlidingSyncResp, isDeleteOp, isInsertOp, isInvalidateOp, isSyncOp } from './api/slidingSync'; 3 import EventEmitter from "events"; 4 import { Room } from "./room"; 5 import { OwnUser } from "./ownUser"; 6 import { DeviceLists, UserId } from "@mtrnord/matrix-sdk-crypto-js"; 7 import { IRoomEvent, IRoomStateEvent, isRoomStateEvent } from "./api/events"; 8 import { HostnameMissingError, NotLogeedInError, SDKError, SyncError } from "./utils"; 9 10 export interface MatrixSlidingSyncEvents { 11 // Used to notify about changes to the room list 12 'rooms': (rooms: Set<Room>) => void; 13 //'delete': (changedCount: number) => void; 14 } 15 16 export declare interface MatrixSlidingSync { 17 on<U extends keyof MatrixSlidingSyncEvents>( 18 event: U, listener: MatrixSlidingSyncEvents[U] 19 ): this; 20 21 emit<U extends keyof MatrixSlidingSyncEvents>( 22 event: U, ...args: Parameters<MatrixSlidingSyncEvents[U]> 23 ): boolean; 24 } 25 26 export class MatrixSlidingSync extends EventEmitter { 27 private syncing = false; 28 private syncPos?: string; 29 private initialSync = true; 30 private lastRanges?: { [key: string]: number[][] }; 31 private lastTxnID?: string; 32 private to_device_since?: string; 33 public mustUpdateTxnID = true; 34 public rooms: Set<Room> = new Set(); 35 private abortController = new AbortController(); 36 37 constructor(private client: MatrixClient, private user: OwnUser) { super() } 38 39 public applyStoredSyncInfo(syncInfo: { 40 userId: string; 41 syncPos?: string; 42 initialSync: boolean; 43 lastRanges?: { 44 [key: string]: number[][]; 45 }; 46 lastTxnID?: string; 47 to_device_since?: string; 48 }) { 49 this.syncPos = syncInfo.syncPos; 50 this.initialSync = syncInfo.initialSync; 51 this.lastRanges = syncInfo.lastRanges; 52 this.lastTxnID = syncInfo.lastTxnID; 53 this.to_device_since = syncInfo.to_device_since; 54 } 55 56 public logout() { 57 this.stopSync(); 58 this.abortController.abort(); 59 this.rooms = new Set(); 60 this.initialSync = true; 61 this.syncPos = undefined; 62 this.to_device_since = undefined; 63 } 64 65 public resetAbortController() { 66 this.abortController = new AbortController(); 67 } 68 69 public async startSync(): Promise<SDKError | void> { 70 // @ts-ignore 71 if (globalThis.IS_STORYBOOK) { 72 await new Promise(r => setTimeout(r, 5000)); 73 } 74 if (!this.client.isLoggedIn) { 75 return new NotLogeedInError(); 76 } 77 if (!this.client.database) { 78 await this.client.createDatabase(); 79 } 80 if (this.syncing) { 81 return; 82 } 83 this.syncing = true; 84 let retries = 0; 85 while (this.syncing) { 86 try { 87 await this.sync(); 88 } catch (e) { 89 console.error(`Error: ${e}`); 90 if (retries > 5) { 91 console.error("Too many retries, giving up"); 92 this.stopSync(); 93 return; 94 } 95 // Sleep for 30 seconds 96 await new Promise(resolve => setTimeout(resolve, 30000)); 97 console.warn("Retrying sync"); 98 retries++; 99 } 100 } 101 } 102 103 public stopSync() { 104 this.syncing = false; 105 } 106 107 private isIndexInRange(index: number, ranges: number[][]): boolean { 108 for (const r of ranges) { 109 if (r[0] < index && index <= r[1]) { 110 return true 111 } 112 } 113 return false 114 } 115 116 private shiftRight(listKey: string, ranges: number[][], hi: number, low: number) { 117 // l h 118 // 0,1,2,3,4 <- before 119 // 0,1,2,2,3 <- after, hi is deleted and low is duplicated 120 for (let i = hi - 1; i > low - 1; i--) { 121 if (this.isIndexInRange(i, ranges)) { 122 const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i + 1); 123 if (roomObj) { 124 roomObj.windowPos[listKey] = (i); 125 } 126 } 127 } 128 } 129 130 private shiftLeft(listKey: string, ranges: number[][], hi: number, low: number) { 131 // l h 132 // 0,1,2,3,4 <- before 133 // 0,1,3,4,4 <- after, low is deleted and hi is duplicated 134 for (let i = low + 1; i < hi + 1; i++) { 135 if (this.isIndexInRange(i, ranges)) { 136 const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i - 1); 137 if (roomObj) { 138 roomObj.windowPos[listKey] = (i); 139 } 140 } 141 } 142 143 } 144 145 private async removeEntry(listKey: string, ranges: number[][], index: number) { 146 // work out the max index 147 let max = -1; 148 const indexes = [...this.rooms].map(room => room.windowPos[listKey]); 149 for (const n in indexes) { 150 if (Number(n) > max) { 151 max = Number(n); 152 } 153 } 154 // TODO: Unclear if this is needed or working. Probably wrong? 155 // const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === index); 156 // if (roomObj) { 157 // const tx = this.database?.transaction('rooms', 'readwrite'); 158 // await tx?.store.delete(roomObj.roomID); 159 // await tx?.done; 160 // this.rooms.delete(roomObj) 161 // } 162 if (max < 0 || index > max) { 163 return; 164 } 165 // Everything higher than the gap needs to be shifted left. 166 this.shiftLeft(listKey, ranges, max, index); 167 } 168 169 private addEntry(listKey: string, ranges: number[][], index: number): void { 170 // work out the max index 171 let max = -1; 172 const indexes = [...this.rooms].map(room => room.windowPos[listKey]); 173 for (const n in indexes) { 174 if (Number(n) > max) { 175 max = Number(n); 176 } 177 } 178 if (max < 0 || index > max) { 179 return; 180 } 181 // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element 182 this.shiftRight(listKey, ranges, max + 1, index); 183 } 184 185 private async sync(): Promise<SDKError | void> { 186 if (!this.client.isLoggedIn) { 187 return new NotLogeedInError(); 188 } 189 if (!this.user.slidingSyncHostname) { 190 return new HostnameMissingError(); 191 } 192 193 // TODO: This might cause future issues 194 Promise.all([this.user.e2ee.sendIdentifyAndOneTimeKeys()]).catch(e => { 195 console.error("Error sending identify and one time keys", e); 196 }); 197 198 // This is the initial sync case for each list 199 const lists_ranges: { 200 "overview": number[][]; 201 "spaces": number[][]; 202 [key: string]: number[][]; 203 } = { 204 "overview": [[0, 20]], 205 "spaces": [[0, 20]] 206 }; 207 for (const space of this.client.spaceOpen) { 208 if (space === "other") { continue } 209 if (space === "dm") { continue } 210 lists_ranges[space] = [[0, 20]]; 211 } 212 213 let timeline_limit = 1; 214 let subscription_limit = 10; 215 if (!this.initialSync) { 216 for (const list in lists_ranges) { 217 // Set higher timeline limit for subsequent syncs 218 timeline_limit = 10; 219 subscription_limit = 50; 220 // Calculate overlap between this.roomsInView and this.roomToRoomID and then 221 // calculate the ranges for each list 222 let rawRangeInView = new Set([...this.rooms] 223 .filter(room => this.client.roomsInView.includes(room.roomID)) 224 .map(room => room.windowPos[list]).sort().filter(x => x !== undefined && x !== null)) 225 226 if (this.client.getSpaces().find(r => r.roomID === list)) { 227 // If we are syncing the spaces list, we need to use the spaceInView list instead 228 rawRangeInView = new Set([...this.rooms] 229 .filter(room => this.client.spacesInView.includes(room.roomID)) 230 .map(room => room.windowPos[list]).sort().filter(x => x !== undefined && x !== null)) 231 } 232 233 if (rawRangeInView.size !== 0) { 234 const minimum = Math.min(...rawRangeInView); 235 const maximum = Math.max(...rawRangeInView); 236 237 lists_ranges[list] = [[Math.max(minimum - 10, 0), maximum + 10]]; 238 } 239 } 240 lists_ranges["e2ee"] = lists_ranges["overview"]; 241 } 242 243 244 if (this.lastRanges && Object.entries(lists_ranges).toString() !== Object.entries(this.lastRanges).toString()) { 245 console.log("Ranges changed, resetting sync txn_id", lists_ranges) 246 this.lastRanges = lists_ranges; 247 this.lastTxnID = Date.now().toString(); 248 } 249 250 if (!this.lastRanges) { 251 this.lastRanges = lists_ranges; 252 this.lastTxnID = Date.now().toString(); 253 } 254 255 if (this.mustUpdateTxnID) { 256 this.lastTxnID = Date.now().toString(); 257 this.mustUpdateTxnID = false; 258 } 259 260 261 let url = `${this.user.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=5000`; 262 if (this.syncPos) { 263 url = `${this.user.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=5000&pos=${this.syncPos}` 264 } 265 266 const body: ISlidingSyncReq = { 267 // allows clients to know what request params reached the server, 268 // functionally similar to txn IDs on /send for events. 269 txn_id: this.lastTxnID, 270 271 // a delta token to remember information between sessions. 272 // See "Bandwidth optimisations for persistent clients" for more information. 273 // TODO: This isnt implemented anywhere yet 274 //delta_token: "opaque-server-provided-string", 275 276 // Sliding Window API 277 lists: { 278 "spaces": { 279 ranges: this.lastRanges["spaces"], 280 // slow_get_all_rooms: true, 281 sort: ["by_name"], 282 required_state: [ 283 // needed to build sections 284 ["m.space.child", "*"], 285 ["m.space.parent", "*"], 286 ["m.room.create", ""], 287 ["m.room.tombstone", ""], 288 // Room Avatar 289 ["m.room.avatar", "*"], 290 // Room Topic 291 ["m.room.topic", "*"], 292 // Request only the m.room.member events required to render events in the timeline. 293 // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for 294 // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members". 295 ["m.room.member", "$LAZY"], 296 // E2EE 297 ["m.room.encryption", ""], 298 ["m.room.history_visibility", ""], 299 ], 300 timeline_limit: 0, 301 filters: { 302 room_types: ["m.space"] 303 } 304 }, 305 "overview": { 306 ranges: this.lastRanges["overview"], 307 sort: ["by_notification_level", "by_recency", "by_name"], 308 required_state: [ 309 // needed to build sections 310 ["m.space.child", "*"], 311 ["m.space.parent", "*"], 312 ["m.room.create", ""], 313 ["m.room.tombstone", ""], 314 // Room Avatar 315 ["m.room.avatar", "*"], 316 // Room Topic 317 ["m.room.topic", "*"], 318 // Request only the m.room.member events required to render events in the timeline. 319 // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for 320 // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members". 321 ["m.room.member", "$LAZY"], 322 // E2EE 323 ["m.room.encryption", ""], 324 ["m.room.history_visibility", ""], 325 ], 326 timeline_limit: timeline_limit, 327 filters: { 328 not_room_types: ["m.space"], 329 } 330 }, 331 "e2ee": { 332 ranges: this.lastRanges["overview"], 333 sort: ["by_notification_level", "by_recency", "by_name"], 334 required_state: [ 335 // needed to build sections 336 ["m.space.child", "*"], 337 ["m.space.parent", "*"], 338 ["m.room.create", ""], 339 ["m.room.tombstone", ""], 340 // Room Avatar 341 ["m.room.avatar", "*"], 342 // Room Topic 343 ["m.room.topic", "*"], 344 ["m.room.member", "*"], 345 // E2EE 346 ["m.room.encryption", ""], 347 ["m.room.history_visibility", ""], 348 ], 349 timeline_limit: timeline_limit, 350 filters: { 351 not_room_types: ["m.space"], 352 is_encrypted: true, 353 } 354 }, 355 }, 356 bump_event_types: ["m.room.message", "m.room.encrypted"], 357 358 extensions: { 359 e2ee: { 360 enabled: true, 361 }, 362 to_device: { 363 enabled: true, 364 since: this.to_device_since 365 }, 366 typing: { 367 enabled: true, 368 lists: ["overview", "e2ee"], 369 }, 370 receipts: { 371 enabled: true, 372 lists: ["overview", "e2ee"], 373 } 374 }, 375 }; 376 377 for (const space of this.client.spaceOpen) { 378 if (space === "other") { continue } 379 if (space === "dm") { continue } 380 if (!body.lists) { 381 body.lists = {}; 382 } 383 body.lists[space] = { 384 slow_get_all_rooms: true, 385 ranges: this.lastRanges[space], 386 sort: ["by_notification_level", "by_recency", "by_name"], 387 required_state: [ 388 // needed to build sections 389 ["m.space.child", "*"], 390 ["m.space.parent", "*"], 391 ["m.room.create", ""], 392 ["m.room.tombstone", ""], 393 // Room Avatar 394 ["m.room.avatar", "*"], 395 // Room Topic 396 ["m.room.topic", "*"], 397 // Request only the m.room.member events required to render events in the timeline. 398 // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for 399 // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members". 400 ["m.room.member", "$LAZY"], 401 // E2EE 402 ["m.room.encryption", ""], 403 ["m.room.history_visibility", ""], 404 ], 405 timeline_limit: timeline_limit, 406 filters: { 407 "spaces": [space] 408 } 409 } 410 } 411 412 if (this.client.currentRoom) { 413 body.room_subscriptions = {}; 414 body.room_subscriptions[this.client.currentRoom] = { 415 sort: ["by_notification_level", "by_recency", "by_name"], 416 required_state: [ 417 // needed to build sections 418 ["m.space.child", "*"], 419 ["m.space.parent", "*"], 420 ["m.room.create", ""], 421 ["m.room.tombstone", ""], 422 // Room Avatar 423 ["m.room.avatar", "*"], 424 // Room Topic 425 ["m.room.topic", "*"], 426 // Request only the m.room.member events required to render events in the timeline. 427 // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for 428 // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members". 429 ["m.room.member", "$LAZY"], 430 // E2EE 431 ["m.room.encryption", ""], 432 ["m.room.history_visibility", ""], 433 ], 434 timeline_limit: subscription_limit, 435 filters: {} 436 } 437 } 438 439 const resp = await fetch(url, { 440 method: "POST", 441 signal: this.abortController.signal, 442 headers: { 443 "Content-Type": "application/json", 444 "Authorization": `Bearer ${this.user.access_token}` 445 }, 446 body: JSON.stringify(body) 447 }); 448 if (!resp.ok) { 449 if (resp.status === 400) { 450 if ((await resp.json()).errcode === "M_UNKNOWN_POS") { 451 this.syncPos = undefined; 452 const syncInfoTX = this.client.database?.transaction('syncInfo', 'readwrite'); 453 await syncInfoTX?.store.put({ 454 userId: this.user.mxid!, 455 syncPos: this.syncPos, 456 initialSync: this.initialSync, 457 lastRanges: this.lastRanges, 458 lastTxnID: this.lastTxnID, 459 }); 460 await syncInfoTX?.done; 461 } 462 return; 463 } else if (resp.status === 401) { 464 await this.logout(); 465 return new SyncError(resp); 466 } else { 467 return new SyncError(resp); 468 } 469 } 470 const json = await resp.json() as ISlidingSyncResp; 471 this.syncPos = json.pos; 472 473 if (json.extensions?.to_device) { 474 await this.user.e2ee.receiveSyncData( 475 JSON.stringify(json.extensions.to_device.events || []), 476 new DeviceLists( 477 json.extensions.e2ee?.device_lists ? json.extensions.e2ee?.device_lists?.changed?.map( 478 user_id => new UserId(user_id) 479 ) : [], 480 json.extensions.e2ee?.device_lists ? json.extensions.e2ee?.device_lists?.left?.map( 481 user_id => new UserId(user_id) 482 ) : [] 483 ), 484 new Map(Object.entries(json.extensions.e2ee?.device_one_time_keys_count || [])), 485 new Set(json.extensions.e2ee?.device_unused_fallback_key_types) 486 ) 487 this.to_device_since = json.extensions.to_device.next_batch; 488 } 489 490 await this.user.e2ee.sendIdentifyAndOneTimeKeys(); 491 492 493 const syncInfoTX = this.client.database?.transaction('syncInfo', 'readwrite'); 494 await syncInfoTX?.store.put({ 495 userId: this.user.mxid!, 496 syncPos: this.syncPos, 497 initialSync: this.initialSync, 498 lastRanges: this.lastRanges, 499 lastTxnID: this.lastTxnID, 500 to_device_since: this.to_device_since, 501 }); 502 await syncInfoTX?.done; 503 504 let gapIndex = -1; 505 for (const listKey in json.lists) { 506 const list = json.lists[listKey]; 507 if (list.ops) { 508 for (const op of list.ops) { 509 if (isSyncOp(op)) { 510 const tx = this.client.database?.transaction('rooms', 'readwrite'); 511 for (let i = op.range[0]; i <= op.range[1]; i++) { 512 const roomID = op.room_ids[i - op.range[0]]; 513 if (!roomID) { 514 break; // we are at the end of list 515 } 516 517 // Check if we already know this room and skip if we do. This is needed since we have 2 lists. 518 // The db would already do this but the obj list doesn't (even though its a Set. Thats a mystery yet to solve) 519 const roomObj = [...this.rooms].find(room => room.roomID === roomID); 520 if (roomObj) { 521 roomObj.windowPos[listKey] = i; 522 continue; 523 } 524 525 const newRoom = new Room(roomID, this.user.hostname!, this.client, this.user.e2ee); 526 // We start to remember the Room now. 527 newRoom.setName(roomID); 528 newRoom.windowPos[listKey] = i; 529 530 this.rooms.add(newRoom); 531 await tx?.store.put({ 532 windowPos: newRoom.windowPos, 533 roomID: newRoom.roomID, 534 name: newRoom.getName(), 535 notification_count: newRoom.getNotificationCount(), 536 highlight_count: newRoom.getNotificationHighlightCount(), 537 joined_count: newRoom.getJoinedCount(), 538 invited_count: newRoom.getInvitedCount(), 539 avatarUrl: newRoom.getAvatarURL(), 540 isSpace: newRoom.isSpace(), 541 isDM: newRoom.isDM(), 542 stateEvents: newRoom.getStateEvents(), 543 events: newRoom.getEvents(), 544 }); 545 } 546 await tx?.done; 547 } else if (isInsertOp(op)) { 548 console.log("Got INSERT OP", op); 549 const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === op.index); 550 if (roomObj) { 551 if (gapIndex < 0) { 552 // we haven't been told where to shift from, so make way for a new room entry. 553 this.addEntry(listKey, this.lastRanges[listKey], op.index); 554 } else if (gapIndex > op.index) { 555 // the gap is further down the list, shift every element to the right 556 // starting at the gap so we can just shift each element in turn: 557 // [A,B,C,_] gapIndex=3, op.index=0 558 // [A,B,C,C] i=3 559 // [A,B,B,C] i=2 560 // [A,A,B,C] i=1 561 // Terminate. We'll assign into op.index next. 562 this.shiftRight(listKey, this.lastRanges[listKey], gapIndex, op.index); 563 } else if (gapIndex < op.index) { 564 // the gap is further up the list, shift every element to the left 565 // starting at the gap so we can just shift each element in turn 566 this.shiftLeft(listKey, this.lastRanges[listKey], op.index, gapIndex); 567 } 568 } 569 gapIndex = -1; 570 const tx = this.client.database?.transaction('rooms', 'readwrite'); 571 // We start to remember the Room now. 572 const foundRoom = [...this.rooms].find(room => room.roomID === op.room_id); 573 if (foundRoom) { 574 foundRoom.windowPos[listKey] = op.index; 575 await tx?.store.put({ 576 windowPos: foundRoom.windowPos, 577 roomID: foundRoom.roomID, 578 name: foundRoom.getName(), 579 notification_count: foundRoom.getNotificationCount(), 580 highlight_count: foundRoom.getNotificationHighlightCount(), 581 joined_count: foundRoom.getJoinedCount(), 582 invited_count: foundRoom.getInvitedCount(), 583 avatarUrl: foundRoom.getAvatarURL(), 584 isSpace: foundRoom.isSpace(), 585 isDM: foundRoom.isDM(), 586 stateEvents: foundRoom.getStateEvents(), 587 events: foundRoom.getEvents(), 588 }); 589 } else { 590 const roomFromDB = await tx?.store.get(op.room_id); 591 let newRoom = new Room(op.room_id, this.user.hostname!, this.client, this.user.e2ee); 592 newRoom.setName(op.room_id); 593 newRoom.windowPos[listKey] = op.index; 594 if (roomFromDB) { 595 console.warn("Room in db but not in obj list.", op.room_id, "Updating obj list."); 596 newRoom = new Room(op.room_id, this.user.hostname!, this.client, this.user.e2ee) 597 newRoom.setName(roomFromDB.name); 598 newRoom.setNotificationCount(roomFromDB.notification_count); 599 newRoom.setNotificationHighlightCount(roomFromDB.highlight_count); 600 newRoom.setJoinedCount(roomFromDB.joined_count); 601 newRoom.setInvitedCount(roomFromDB.invited_count); 602 newRoom.setDM(roomFromDB.isDM || false); 603 } 604 this.rooms.add(newRoom); 605 await tx?.store.put({ 606 windowPos: newRoom.windowPos, 607 roomID: newRoom.roomID, 608 name: newRoom.getName(), 609 notification_count: newRoom.getNotificationCount(), 610 highlight_count: newRoom.getNotificationHighlightCount(), 611 joined_count: newRoom.getJoinedCount(), 612 invited_count: newRoom.getInvitedCount(), 613 avatarUrl: newRoom.getAvatarURL(), 614 isSpace: newRoom.isSpace(), 615 isDM: newRoom.isDM(), 616 stateEvents: newRoom.getStateEvents(), 617 events: newRoom.getEvents(), 618 }); 619 } 620 621 const roomIDs2 = [...this.rooms].map(room => room.roomID); 622 // Check if we generated any duplicates and log them. 623 const duplicates = roomIDs2.filter((item, index) => roomIDs2.indexOf(item) != index); 624 if (duplicates.length > 0) { 625 console.error("Duplicates found", duplicates); 626 } 627 await tx?.done; 628 } else if (isDeleteOp(op)) { 629 console.log("Got DELETE OP", op); 630 631 if (gapIndex !== -1) { 632 // we already have a DELETE operation to process, so process it. 633 await this.removeEntry(listKey, this.lastRanges[listKey], gapIndex); 634 } 635 gapIndex = op.index; 636 } else if (isInvalidateOp(op)) { 637 // TODO: Figure out if this is needed in reality 638 // const tx = this.database?.transaction('rooms', 'readwrite'); 639 // for (let i = op.range[0]; i <= op.range[1]; i++) { 640 // // We shall first forget about these and "startover" 641 // const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i); 642 // if (roomObj) { 643 // await tx?.store.delete(roomObj.roomID); 644 // this.rooms.delete(roomObj) 645 // } 646 // } 647 // await tx?.done; 648 } 649 } 650 if (gapIndex !== -1) { 651 // we already have a DELETE operation to process, so process it 652 // Everything higher than the gap needs to be shifted left. 653 await this.removeEntry(listKey, this.lastRanges[listKey], gapIndex); 654 } 655 } 656 } 657 for (const roomID in json.rooms) { 658 const room = json.rooms[roomID]; 659 const name = room.name; 660 const notification_count = room.notification_count; 661 const notification_highlight_count = room.highlight_count; 662 const joined_count = room.joined_count; 663 const invited_count = room.invited_count; 664 const events = room.timeline; 665 const state_events = events?.filter(event => isRoomStateEvent(event)).map(event => event as IRoomStateEvent); 666 const normal_events = events?.filter(event => !isRoomStateEvent(event)).map(event => event as IRoomEvent); 667 const required_state = room.required_state; 668 const is_dm = room.is_dm; 669 670 let roomObj = [...this.rooms].find(room => room.roomID === roomID); 671 if (!roomObj) { 672 // Warn, check in the db and if that fails, create a new one. 673 console.warn("Could not find roomObj for roomID:", roomID); 674 675 const tx = this.client.database?.transaction('rooms', 'readwrite'); 676 const roomFromDB = await tx?.store.get(roomID); 677 await tx?.done; 678 679 if (roomFromDB) { 680 console.warn("Room in db but not in obj list.", roomID, "Updating obj list."); 681 682 roomObj = new Room(roomID, this.user.hostname!, this.client, this.user.e2ee); 683 roomObj.setName(roomFromDB.name); 684 roomObj.setNotificationCount(roomFromDB.notification_count); 685 roomObj.setNotificationHighlightCount(roomFromDB.highlight_count); 686 roomObj.setJoinedCount(roomFromDB.joined_count); 687 roomObj.setInvitedCount(roomFromDB.invited_count); 688 roomObj.setDM(roomFromDB.isDM || false); 689 if (roomFromDB.events) { 690 roomObj.addEvents(roomFromDB.events); 691 } 692 if (roomFromDB.stateEvents) { 693 roomObj.addStateEvents(roomFromDB.stateEvents); 694 } 695 roomObj.windowPos = roomFromDB.windowPos; 696 } else { 697 console.warn("Could not find room in db. Creating new one."); 698 roomObj = new Room(roomID, this.user.hostname!, this.client, this.user.e2ee); 699 this.rooms.add(roomObj); 700 } 701 } 702 703 if (name) { 704 roomObj.setName(name); 705 } 706 roomObj.setNotificationCount(notification_count); 707 roomObj.setNotificationHighlightCount(notification_highlight_count); 708 roomObj.setJoinedCount(joined_count); 709 roomObj.setInvitedCount(invited_count); 710 if (normal_events) { 711 roomObj.addEvents(normal_events); 712 } 713 if (required_state) { 714 roomObj.addStateEvents(required_state); 715 } 716 if (state_events) { 717 roomObj.addStateEvents(state_events); 718 } 719 if (required_state || state_events) { 720 if (roomObj.isEncrypted() && roomObj.isJoined()) { 721 const joinEvents = [...(required_state || []), ...(state_events || [])] 722 .filter(event => event.type === "m.room.member" && event.content.membership === "join"); 723 const memberIds = joinEvents.map(event => new UserId(event.state_key)); 724 await this.user.e2ee.updateTrackedUsers(memberIds); 725 } 726 } 727 if (is_dm) { 728 roomObj.setDM(is_dm); 729 } 730 731 732 const tx = this.client.database?.transaction('rooms', 'readwrite'); 733 // Write to database 734 await tx?.store.put({ 735 windowPos: roomObj.windowPos, 736 roomID: roomObj.roomID, 737 name: roomObj.getName(), 738 notification_count: roomObj.getNotificationCount(), 739 highlight_count: roomObj.getNotificationHighlightCount(), 740 joined_count: roomObj.getJoinedCount(), 741 invited_count: roomObj.getInvitedCount(), 742 events: roomObj.getPureEvents(), 743 stateEvents: roomObj.getStateEvents(), 744 avatarUrl: roomObj.getAvatarURL(), 745 isSpace: roomObj.isSpace(), 746 isDM: roomObj.isDM(), 747 }); 748 await tx?.done 749 } 750 751 if (this.initialSync) { 752 this.initialSync = false; 753 console.log("initialSyncComplete"); 754 } 755 if (json.rooms && Object.keys(json.rooms).length > 0) { 756 this.emit("rooms", this.rooms); 757 } 758 } 759 }