diff --git a/src/client.js b/src/client.js index b75fcf917..105606478 100644 --- a/src/client.js +++ b/src/client.js @@ -757,6 +757,20 @@ MatrixClient.prototype.getRoom = function(roomId) { return this.store.getRoom(roomId); }; +MatrixClient.prototype._loadMembers = async function(room) { + const roomId = room.roomId; + let rawMembersEvents = await this.store.getOutOfBandMembers(roomId); + if (rawMembersEvents.length == 0) { + const lastEventId = room.getLastEventId(); + const response = await this.members(roomId, "join", "leave", lastEventId); + rawMembersEvents = response.chunk; + // TODO don't block on writing + await this.store.setOutOfBandMembers(roomId, rawMembersEvents); + } + const memberEvents = rawMembersEvents.map(this.getEventMapper()); + return memberEvents; +}; + /** * Preloads the member list for the given room id, * in case lazy loading of memberships is in use. @@ -767,13 +781,8 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { if (!room || !room.needsOutOfBandMembers()) { return; } - - const lastEventId = room.getLastEventId(); - const responsePromise = this.members(roomId, "join", "leave", lastEventId); - const eventsPromise = responsePromise.then((response) => { - return response.chunk.map(this.getEventMapper()); - }); - await room.loadOutOfBandMembers(eventsPromise); + const membersPromise = this._loadMembers(room); + await room.loadOutOfBandMembers(membersPromise); }; /** diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index a928e1aa6..7d64c4d6d 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -19,7 +19,7 @@ import Promise from 'bluebird'; import SyncAccumulator from "../sync-accumulator"; import utils from "../utils"; -const VERSION = 1; +const VERSION = 2; function createDatabase(db) { // Make user store, clobber based on user ID. (userId property of User objects) @@ -33,6 +33,11 @@ function createDatabase(db) { db.createObjectStore("sync", { keyPath: ["clobber"] }); } +function upgradeSchemaV2(db) { + const llMembersStore = db.createObjectStore("lazy_loaded_members", { keyPath: ["roomId", "userId"]}); + llMembersStore.createIndex("room", "roomId"); +} + /** * Helper method to collect results from a Cursor and promiseify it. * @param {ObjectStore|Index} store The store to perform openCursor on. @@ -136,6 +141,9 @@ LocalIndexedDBStoreBackend.prototype = { if (oldVersion < 1) { // The database did not previously exist. createDatabase(db); } + if (oldVersion < 2) { + upgradeSchemaV2(db); + } // Expand as needed. }; @@ -187,6 +195,56 @@ LocalIndexedDBStoreBackend.prototype = { }); }, + getOutOfBandMembers: function(roomId) { + return new Promise((resolve, reject) =>{ + const tx = this.db.transaction(["lazy_loaded_members"], "readonly"); + const store = tx.objectStore("lazy_loaded_members"); + const roomIndex = store.index("room"); + const range = IDBKeyRange.only(roomId); + const request = roomIndex.openCursor(range); + + const members = []; + request.onsuccess = (event) => { + const cursor = event.target.result; + if (!cursor) { + return resolve(members); + } + const record = cursor.value; + members.push({ + userId: record.userId, + displayName: record.displayName, + membership: record.membership, + avatarUrl: record.avatarUrl, + }); + cursor.continue(); + }; + request.onerror = (err) => { + reject(err); + }; + }); + }, + + setOutOfBandMembers: function(roomId, members) { + function ignoreResult() {}; + // run everything in a promise so anything that throws will reject + return new Promise((resolve) =>{ + const tx = this.db.transaction(["lazy_loaded_members"], "readwrite"); + const store = tx.objectStore("lazy_loaded_members"); + const puts = members.map((m) => { + const record = { + roomId: roomId, + userId: m.userId, + displayName: m.displayName, + avatarUrl: m.avatarUrl, + membership: m.membership + }; + let putPromise = promiseifyRequest(store.put(record)); + return putPromise.then(ignoreResult); + }); + resolve(Promise.all(puts).then(ignoreResult)); + }); + }, + /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 2221633e4..1594eef26 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -87,6 +87,13 @@ RemoteIndexedDBStoreBackend.prototype = { return this._doCmd('syncToDatabase', [users]); }, + getOutOfBandMembers: function(roomId) { + return this._doCmd('getOutOfBandMembers', [roomId]); + }, + + setOutOfBandMembers: function(roomId, members) { + return this._doCmd('setOutOfBandMembers', [roomId, members]); + }, /** * Load all user presence events from the database. This is not cached. diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index d32df7274..021156afc 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -92,6 +92,12 @@ class IndexedDBStoreWorker { case 'getNextBatchToken': prom = this.backend.getNextBatchToken(); break; + case 'getOutOfBandMembers': + prom = this.backend.getOutOfBandMembers(msg.args[0]); + break; + case 'setOutOfBandMembers': + prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]); + break; } if (prom === undefined) { diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index d47e35544..0086eb531 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -219,4 +219,12 @@ IndexedDBStore.prototype.setSyncData = function(syncData) { return this.backend.setSyncData(syncData); }; +IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) { + return this.backend.getOutOfBandMembers(roomId); +}; + +IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, members) { + return this.backend.setOutOfBandMembers(roomId, members); +}; + module.exports.IndexedDBStore = IndexedDBStore;