vector-im-hydrogen-web/src/matrix/room/Room.js

808 lines
31 KiB
JavaScript
Raw Normal View History

2020-08-05 18:38:55 +02:00
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
2020-04-20 21:35:53 +02:00
import {EventEmitter} from "../../utils/EventEmitter.js";
2020-09-23 17:59:42 +02:00
import {RoomSummary} from "./RoomSummary.js";
import {SyncWriter} from "./timeline/persistence/SyncWriter.js";
import {GapWriter} from "./timeline/persistence/GapWriter.js";
import {Timeline} from "./timeline/Timeline.js";
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
import {SendQueue} from "./sending/SendQueue.js";
import {WrappedError} from "../error.js"
2020-08-19 16:58:19 +02:00
import {fetchOrLoadMembers} from "./members/load.js";
2020-08-19 16:29:54 +02:00
import {MemberList} from "./members/MemberList.js";
import {Heroes} from "./members/Heroes.js";
import {EventEntry} from "./timeline/entries/EventEntry.js";
import {ObservedEventMap} from "./ObservedEventMap.js";
import {AttachmentUpload} from "./AttachmentUpload.js";
import {DecryptionSource} from "../e2ee/common.js";
import {ensureLogItem} from "../../logging/utils.js";
const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
2018-12-21 14:35:24 +01:00
export class Room extends EventEmitter {
constructor({roomId, storage, hsApi, mediaRepository, emitCollectionChange, pendingEvents, user, createRoomEncryption, getSyncToken, platform}) {
2019-02-20 23:48:16 +01:00
super();
this._roomId = roomId;
this._storage = storage;
this._hsApi = hsApi;
this._mediaRepository = mediaRepository;
2020-09-23 18:22:51 +02:00
this._summary = new RoomSummary(roomId);
this._fragmentIdComparer = new FragmentIdComparer([]);
2020-09-23 18:22:51 +02:00
this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer});
2019-02-20 23:48:16 +01:00
this._emitCollectionChange = emitCollectionChange;
this._sendQueue = new SendQueue({roomId, storage, hsApi, pendingEvents});
this._timeline = null;
this._user = user;
this._changedMembersDuringSync = null;
2020-08-31 08:54:27 +02:00
this._memberList = null;
2020-09-03 15:36:17 +02:00
this._createRoomEncryption = createRoomEncryption;
this._roomEncryption = null;
this._getSyncToken = getSyncToken;
this._platform = platform;
this._observedEvents = null;
this._invite = null;
2020-09-23 18:22:51 +02:00
}
2018-12-21 14:35:24 +01:00
async _eventIdsToEntries(eventIds, txn) {
2021-03-02 15:29:35 +01:00
const retryEntries = [];
await Promise.all(eventIds.map(async eventId => {
const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId);
if (storageEntry) {
retryEntries.push(new EventEntry(storageEntry, this._fragmentIdComparer));
2021-03-02 15:29:35 +01:00
}
}));
2021-03-02 15:29:35 +01:00
return retryEntries;
2020-09-22 18:22:37 +02:00
}
_getAdditionalTimelineRetryEntries(otherRetryEntries, roomKeys) {
let retryTimelineEntries = this._roomEncryption.filterUndecryptedEventEntriesForKeys(this._timeline.remoteEntries, roomKeys);
// filter out any entries already in retryEntries so we don't decrypt them twice
const existingIds = otherRetryEntries.reduce((ids, e) => {ids.add(e.id); return ids;}, new Set());
retryTimelineEntries = retryTimelineEntries.filter(e => !existingIds.has(e.id));
return retryTimelineEntries;
}
2021-03-02 15:29:35 +01:00
/**
* Used for retrying decryption from other sources than sync, like key backup.
2021-03-02 15:29:35 +01:00
* @internal
* @param {RoomKey} roomKey
* @param {Array<string>} eventIds any event ids that should be retried. There might be more in the timeline though for this key.
2021-03-02 15:29:35 +01:00
* @return {Promise}
*/
async notifyRoomKey(roomKey, eventIds, log) {
if (!this._roomEncryption) {
return;
}
const txn = await this._storage.readTxn([
this._storage.storeNames.timelineEvents,
this._storage.storeNames.inboundGroupSessions,
2021-03-02 15:29:35 +01:00
]);
let retryEntries = await this._eventIdsToEntries(eventIds, txn);
if (this._timeline) {
const retryTimelineEntries = this._getAdditionalTimelineRetryEntries(retryEntries, [roomKey]);
retryEntries = retryEntries.concat(retryTimelineEntries);
}
2021-03-02 15:29:35 +01:00
if (retryEntries.length) {
const decryptRequest = this._decryptEntries(DecryptionSource.Retry, retryEntries, txn, log);
// this will close txn while awaiting decryption
await decryptRequest.complete();
this._timeline?.replaceEntries(retryEntries);
// we would ideally write the room summary in the same txn as the groupSessionDecryptions in the
// _decryptEntries entries and could even know which events have been decrypted for the first
// time from DecryptionChanges.write and only pass those to the summary. As timeline changes
// are not essential to the room summary, it's fine to write this in a separate txn for now.
const changes = this._summary.data.applyTimelineEntries(retryEntries, false, false);
if (await this._summary.writeAndApplyData(changes, this._storage)) {
this._emitUpdate();
}
}
}
_setEncryption(roomEncryption) {
if (roomEncryption && !this._roomEncryption) {
this._roomEncryption = roomEncryption;
this._sendQueue.enableEncryption(this._roomEncryption);
2020-09-04 16:27:39 +02:00
if (this._timeline) {
this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline));
2020-09-04 16:27:39 +02:00
}
}
}
/**
* Used for decrypting when loading/filling the timeline, and retrying decryption,
* not during sync, where it is split up during the multiple phases.
*/
2021-03-15 15:23:35 +01:00
_decryptEntries(source, entries, inboundSessionTxn, log = null) {
const request = new DecryptionRequest(async (r, log) => {
2020-09-10 16:40:30 +02:00
if (!inboundSessionTxn) {
inboundSessionTxn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]);
2020-09-10 16:40:30 +02:00
}
if (r.cancelled) return;
const events = entries.filter(entry => {
return entry.eventType === EVENT_ENCRYPTED_TYPE;
}).map(entry => entry.event);
r.preparation = await this._roomEncryption.prepareDecryptAll(events, null, source, inboundSessionTxn);
2020-09-10 16:40:30 +02:00
if (r.cancelled) return;
const changes = await r.preparation.decrypt();
r.preparation = null;
if (r.cancelled) return;
const stores = [this._storage.storeNames.groupSessionDecryptions];
const isTimelineOpen = this._isTimelineOpen;
2020-09-10 16:40:30 +02:00
if (isTimelineOpen) {
// read to fetch devices if timeline is open
stores.push(this._storage.storeNames.deviceIdentities);
}
const writeTxn = await this._storage.readWriteTxn(stores);
2020-09-10 16:40:30 +02:00
let decryption;
try {
decryption = await changes.write(writeTxn, log);
if (isTimelineOpen) {
await decryption.verifySenders(writeTxn);
}
2020-09-10 16:40:30 +02:00
} catch (err) {
writeTxn.abort();
throw err;
}
await writeTxn.complete();
2021-02-23 19:22:25 +01:00
// TODO: log decryption errors here
2020-09-10 16:40:30 +02:00
decryption.applyToEntries(entries);
if (this._observedEvents) {
this._observedEvents.updateEvents(entries);
}
2021-03-15 15:23:46 +01:00
}, ensureLogItem(log));
2020-09-10 16:40:30 +02:00
return request;
}
async _getSyncRetryDecryptEntries(newKeys, roomEncryption, txn) {
const entriesPerKey = await Promise.all(newKeys.map(async key => {
const retryEventIds = await roomEncryption.getEventIdsForMissingKey(key, txn);
if (retryEventIds) {
return this._eventIdsToEntries(retryEventIds, txn);
}
}));
let retryEntries = entriesPerKey.reduce((allEntries, entries) => entries ? allEntries.concat(entries) : allEntries, []);
// If we have the timeline open, see if there are more entries for the new keys
// as we only store missing session information for synced events, not backfilled.
// We want to decrypt all events we can though if the user is looking
// at them when the timeline is open
if (this._timeline) {
const retryTimelineEntries = this._getAdditionalTimelineRetryEntries(retryEntries, newKeys);
// make copies so we don't modify the original entry in writeSync, before the afterSync stage
const retryTimelineEntriesCopies = retryTimelineEntries.map(e => e.clone());
// add to other retry entries
retryEntries = retryEntries.concat(retryTimelineEntriesCopies);
}
return retryEntries;
}
async prepareSync(roomResponse, membership, invite, newKeys, txn, log) {
2021-02-17 18:45:04 +01:00
log.set("id", this.id);
if (newKeys) {
log.set("newKeys", newKeys.length);
}
2021-05-04 13:33:44 +02:00
let summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership, this._user.id);
2021-04-23 18:05:46 +02:00
if (membership === "join" && invite) {
summaryChanges = summaryChanges.applyInvite(invite);
}
let roomEncryption = this._roomEncryption;
// encryption is enabled in this sync
if (!roomEncryption && summaryChanges.encryption) {
2021-02-17 18:45:04 +01:00
log.set("enableEncryption", true);
roomEncryption = this._createRoomEncryption(this, summaryChanges.encryption);
}
let retryEntries;
let decryptPreparation;
if (roomEncryption) {
let eventsToDecrypt = roomResponse?.timeline?.events || [];
// when new keys arrive, also see if any older events can now be retried to decrypt
if (newKeys) {
retryEntries = await this._getSyncRetryDecryptEntries(newKeys, roomEncryption, txn);
if (retryEntries.length) {
log.set("retry", retryEntries.length);
eventsToDecrypt = eventsToDecrypt.concat(retryEntries.map(entry => entry.event));
}
}
eventsToDecrypt = eventsToDecrypt.filter(event => {
return event?.type === EVENT_ENCRYPTED_TYPE;
});
if (eventsToDecrypt.length) {
decryptPreparation = await roomEncryption.prepareDecryptAll(
eventsToDecrypt, newKeys, DecryptionSource.Sync, txn);
}
}
return {
roomEncryption,
summaryChanges,
decryptPreparation,
decryptChanges: null,
retryEntries
};
}
2021-02-17 18:45:04 +01:00
async afterPrepareSync(preparation, parentLog) {
if (preparation.decryptPreparation) {
2021-03-02 15:29:35 +01:00
await parentLog.wrap("decrypt", async log => {
2021-02-17 18:45:04 +01:00
log.set("id", this.id);
preparation.decryptChanges = await preparation.decryptPreparation.decrypt();
preparation.decryptPreparation = null;
}, parentLog.level.Detail);
}
}
2020-08-19 16:58:28 +02:00
/** @package */
async writeSync(roomResponse, isInitialSync, {summaryChanges, decryptChanges, roomEncryption, retryEntries}, txn, log) {
2021-02-17 18:45:04 +01:00
log.set("id", this.id);
const isRejoin = summaryChanges.membership === "join" && this._summary.data.membership === "leave";
if (isRejoin) {
this._summary.tryRemoveArchive(txn);
}
const {entries: newEntries, newLiveKey, memberChanges} =
await log.wrap("syncWriter", log => this._syncWriter.writeSync(roomResponse, isRejoin, txn, log), log.level.Detail);
let allEntries = newEntries;
if (decryptChanges) {
const decryption = await log.wrap("decryptChanges", log => decryptChanges.write(txn, log));
2021-03-02 21:29:32 +01:00
log.set("decryptionResults", decryption.results.size);
log.set("decryptionErrors", decryption.errors.size);
if (this._isTimelineOpen) {
await decryption.verifySenders(txn);
}
decryption.applyToEntries(newEntries);
if (retryEntries?.length) {
decryption.applyToEntries(retryEntries);
allEntries = retryEntries.concat(allEntries);
}
}
log.set("allEntries", allEntries.length);
let shouldFlushKeyShares = false;
// pass member changes to device tracker
if (roomEncryption && this.isTrackingMembers && memberChanges?.size) {
shouldFlushKeyShares = await roomEncryption.writeMemberChanges(memberChanges, txn, log);
log.set("shouldFlushKeyShares", shouldFlushKeyShares);
}
// also apply (decrypted) timeline entries to the summary changes
summaryChanges = summaryChanges.applyTimelineEntries(
allEntries, isInitialSync, !this._isTimelineOpen, this._user.id);
// only archive a room if we had previously joined it
if (summaryChanges.membership === "leave" && this.membership === "join") {
summaryChanges = this._summary.removeAndWriteArchive(summaryChanges, txn);
} else {
// write summary changes, and unset if nothing was actually changed
summaryChanges = this._summary.writeData(summaryChanges, txn);
}
2021-03-02 21:29:32 +01:00
if (summaryChanges) {
log.set("summaryChanges", summaryChanges.diff(this._summary.data));
}
// fetch new members while we have txn open,
// but don't make any in-memory changes yet
let heroChanges;
// if any hero changes their display name, the summary in the room response
// is also updated, which will trigger a RoomSummary update
// and make summaryChanges non-falsy here
if (summaryChanges?.needsHeroes) {
// room name disappeared, open heroes
if (!this._heroes) {
this._heroes = new Heroes(this._roomId);
}
heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, memberChanges, txn);
}
let removedPendingEvents;
if (Array.isArray(roomResponse.timeline?.events)) {
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn, log);
}
return {
summaryChanges,
roomEncryption,
newEntries,
updatedEntries: retryEntries || [],
newLiveKey,
removedPendingEvents,
memberChanges,
heroChanges,
shouldFlushKeyShares,
};
}
2020-09-08 14:39:33 +02:00
/**
* @package
* Called with the changes returned from `writeSync` to apply them and emit changes.
* No storage or network operations should be done here.
*/
afterSync(changes, log) {
const {
summaryChanges, newEntries, updatedEntries, newLiveKey,
removedPendingEvents, memberChanges,
heroChanges, roomEncryption
} = changes;
2021-02-17 18:45:04 +01:00
log.set("id", this.id);
2020-03-14 20:49:15 +01:00
this._syncWriter.afterSync(newLiveKey);
this._setEncryption(roomEncryption);
if (memberChanges.size) {
if (this._changedMembersDuringSync) {
for (const [userId, memberChange] of memberChanges.entries()) {
this._changedMembersDuringSync.set(userId, memberChange.member);
}
}
if (this._memberList) {
this._memberList.afterSync(memberChanges);
}
if (this._timeline) {
for (const [userId, memberChange] of memberChanges.entries()) {
if (userId === this._user.id) {
this._timeline.updateOwnMember(memberChange.member);
break;
}
}
}
}
let emitChange = false;
if (summaryChanges) {
// if we joined the room, we can't have an invite anymore
2021-05-05 17:03:52 +02:00
if (summaryChanges.membership === "join" && this.membership !== "join") {
this._invite = null;
}
2020-06-26 23:26:24 +02:00
this._summary.applyChanges(summaryChanges);
if (!this._summary.data.needsHeroes) {
this._heroes = null;
}
emitChange = true;
}
if (this._heroes && heroChanges) {
const oldName = this.name;
this._heroes.applyChanges(heroChanges, this._summary.data);
if (oldName !== this.name) {
emitChange = true;
}
}
if (emitChange) {
2020-09-14 16:33:43 +02:00
this._emitUpdate();
2019-02-20 23:48:16 +01:00
}
if (this._timeline) {
// these should not be added if not already there
this._timeline.replaceEntries(updatedEntries);
this._timeline.addOrReplaceEntries(newEntries);
}
if (this._observedEvents) {
this._observedEvents.updateEvents(updatedEntries);
this._observedEvents.updateEvents(newEntries);
}
if (removedPendingEvents) {
this._sendQueue.emitRemovals(removedPendingEvents);
}
2020-09-23 18:22:51 +02:00
}
2018-12-21 14:35:24 +01:00
needsAfterSyncCompleted({shouldFlushKeyShares}) {
return shouldFlushKeyShares;
}
/**
* Only called if the result of writeSync had `needsAfterSyncCompleted` set.
* Can be used to do longer running operations that resulted from the last sync,
* like network operations.
*/
2021-04-20 18:03:27 +02:00
async afterSyncCompleted(changes, log) {
2021-02-17 18:45:04 +01:00
log.set("id", this.id);
if (this._roomEncryption) {
2021-02-23 19:22:25 +01:00
await this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, null, log);
}
}
2020-08-19 16:58:28 +02:00
/** @package */
2021-02-23 19:22:25 +01:00
start(pendingOperations, parentLog) {
if (this._roomEncryption) {
2021-02-23 19:22:25 +01:00
const roomKeyShares = pendingOperations?.get("share_room_key");
if (roomKeyShares) {
// if we got interrupted last time sending keys to newly joined members
parentLog.wrapDetached("flush room keys", log => {
log.set("id", this.id);
return this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, roomKeyShares, log);
});
}
}
2021-02-23 19:22:25 +01:00
this._sendQueue.resumeSending(parentLog);
2019-07-26 22:40:39 +02:00
}
2020-08-19 16:58:28 +02:00
/** @package */
2021-02-23 19:22:25 +01:00
async load(summary, txn, log) {
log.set("id", this.id);
try {
this._summary.load(summary);
if (this._summary.data.encryption) {
const roomEncryption = this._createRoomEncryption(this, this._summary.data.encryption);
this._setEncryption(roomEncryption);
2020-09-03 15:36:17 +02:00
}
// need to load members for name?
if (this._summary.data.needsHeroes) {
this._heroes = new Heroes(this._roomId);
const changes = await this._heroes.calculateChanges(this._summary.data.heroes, [], txn);
this._heroes.applyChanges(changes, this._summary.data);
}
2021-02-23 19:22:25 +01:00
return this._syncWriter.load(txn, log);
} catch (err) {
throw new WrappedError(`Could not load room ${this._roomId}`, err);
}
2020-09-23 18:22:51 +02:00
}
2019-02-26 22:45:58 +01:00
/** @internal */
setInvite(invite) {
// called when an invite comes in for this room
// (e.g. when we're in membership leave and haven't been archived or forgotten yet)
this._invite = invite;
this._emitUpdate();
}
2020-08-19 16:58:28 +02:00
/** @public */
2021-02-23 19:22:59 +01:00
sendEvent(eventType, content, attachments, log = null) {
this._platform.logger.wrapOrRun(log, "send", log => {
log.set("id", this.id);
return this._sendQueue.enqueueEvent(eventType, content, attachments, log);
});
}
/** @public */
2021-02-23 19:22:59 +01:00
async ensureMessageKeyIsShared(log = null) {
if (!this._roomEncryption) {
return;
}
return this._platform.logger.wrapOrRun(log, "ensureMessageKeyIsShared", log => {
log.set("id", this.id);
return this._roomEncryption.ensureMessageKeyIsShared(this._hsApi, log);
});
2020-11-06 23:43:02 +01:00
}
2020-08-19 16:58:28 +02:00
/** @public */
2021-02-23 19:22:59 +01:00
async loadMemberList(log = null) {
if (this._memberList) {
2020-08-31 16:10:18 +02:00
// TODO: also await fetchOrLoadMembers promise here
this._memberList.retain();
return this._memberList;
} else {
2020-08-19 16:58:19 +02:00
const members = await fetchOrLoadMembers({
2020-08-19 16:44:09 +02:00
summary: this._summary,
roomId: this._roomId,
hsApi: this._hsApi,
storage: this._storage,
syncToken: this._getSyncToken(),
2020-08-19 16:44:09 +02:00
// to handle race between /members and /sync
setChangedMembersMap: map => this._changedMembersDuringSync = map,
2021-02-23 19:22:59 +01:00
log,
}, this._platform.logger);
this._memberList = new MemberList({
members,
closeCallback: () => { this._memberList = null; }
});
return this._memberList;
2020-06-26 23:26:24 +02:00
}
}
2020-03-21 23:40:40 +01:00
/** @public */
fillGap(fragmentEntry, amount, log = null) {
// TODO move some/all of this out of Room
return this._platform.logger.wrapOrRun(log, "fillGap", async log => {
2021-02-24 11:22:07 +01:00
log.set("id", this.id);
log.set("fragment", fragmentEntry.fragmentId);
log.set("dir", fragmentEntry.direction.asApiString());
if (fragmentEntry.edgeReached) {
2021-02-24 11:22:07 +01:00
log.set("edgeReached", true);
return;
}
const response = await this._hsApi.messages(this._roomId, {
from: fragmentEntry.token,
dir: fragmentEntry.direction.asApiString(),
limit: amount,
filter: {
lazy_load_members: true,
include_redundant_members: true,
}
}, {log}).response();
const txn = await this._storage.readWriteTxn([
this._storage.storeNames.pendingEvents,
this._storage.storeNames.timelineEvents,
this._storage.storeNames.timelineFragments,
]);
let removedPendingEvents;
let gapResult;
try {
// detect remote echos of pending messages in the gap
removedPendingEvents = this._sendQueue.removeRemoteEchos(response.chunk, txn, log);
// write new events into gap
const gapWriter = new GapWriter({
roomId: this._roomId,
storage: this._storage,
fragmentIdComparer: this._fragmentIdComparer,
});
gapResult = await gapWriter.writeFragmentFill(fragmentEntry, response, txn, log);
} catch (err) {
txn.abort();
throw err;
}
await txn.complete();
if (this._roomEncryption) {
2021-03-15 15:23:35 +01:00
const decryptRequest = this._decryptEntries(DecryptionSource.Timeline, gapResult.entries, null, log);
await decryptRequest.complete();
}
// once txn is committed, update in-memory state & emit events
for (const fragment of gapResult.fragments) {
this._fragmentIdComparer.add(fragment);
}
if (removedPendingEvents) {
this._sendQueue.emitRemovals(removedPendingEvents);
}
if (this._timeline) {
this._timeline.addOrReplaceEntries(gapResult.entries);
}
});
2020-03-21 23:40:40 +01:00
}
2020-08-19 16:58:28 +02:00
/** @public */
2019-02-26 22:45:58 +01:00
get name() {
if (this._heroes) {
return this._heroes.roomName;
}
const summaryData = this._summary.data;
if (summaryData.name) {
return summaryData.name;
}
if (summaryData.canonicalAlias) {
return summaryData.canonicalAlias;
}
return null;
2019-02-26 22:45:58 +01:00
}
2020-08-19 16:58:28 +02:00
/** @public */
get id() {
return this._roomId;
}
2020-08-20 17:32:55 +02:00
get avatarUrl() {
if (this._summary.data.avatarUrl) {
return this._summary.data.avatarUrl;
} else if (this._heroes) {
return this._heroes.roomAvatarUrl;
}
return null;
2020-08-20 17:32:55 +02:00
}
2020-08-21 11:56:45 +02:00
get lastMessageTimestamp() {
return this._summary.data.lastMessageTimestamp;
2020-08-21 11:56:45 +02:00
}
get isUnread() {
return this._summary.data.isUnread;
2020-08-21 11:56:45 +02:00
}
get notificationCount() {
return this._summary.data.notificationCount;
2020-08-21 11:56:45 +02:00
}
2020-08-21 15:50:32 +02:00
get highlightCount() {
return this._summary.data.highlightCount;
2020-08-21 15:50:32 +02:00
}
2020-08-21 11:56:45 +02:00
2020-08-27 20:52:51 +02:00
get isLowPriority() {
const tags = this._summary.data.tags;
2020-08-27 20:52:51 +02:00
return !!(tags && tags['m.lowpriority']);
}
get isEncrypted() {
return !!this._summary.data.encryption;
}
get membership() {
return this._summary.data.membership;
}
/**
* The invite for this room, if any.
* This will only be set if you've left a room, and
* don't archive or forget it, and then receive an invite
* for it again
* @return {Invite?}
*/
get invite() {
return this._invite;
}
enableSessionBackup(sessionBackup) {
this._roomEncryption?.enableSessionBackup(sessionBackup);
2021-03-02 21:30:00 +01:00
// TODO: do we really want to do this every time you open the app?
2021-03-02 15:29:35 +01:00
if (this._timeline) {
this._platform.logger.run("enableSessionBackup", log => {
return this._roomEncryption.restoreMissingSessionsFromBackup(this._timeline.remoteEntries, log);
});
2021-03-02 15:29:35 +01:00
}
}
get isTrackingMembers() {
return this._summary.data.isTrackingMembers;
}
async _getLastEventId() {
const lastKey = this._syncWriter.lastMessageKey;
if (lastKey) {
const txn = await this._storage.readTxn([
this._storage.storeNames.timelineEvents,
]);
const eventEntry = await txn.timelineEvents.get(this._roomId, lastKey);
return eventEntry?.event?.event_id;
}
}
get _isTimelineOpen() {
return !!this._timeline;
}
2020-09-14 16:33:43 +02:00
_emitUpdate() {
// once for event emitter listeners
this.emit("change");
// and once for collection listeners
this._emitCollectionChange(this);
}
2021-02-24 11:22:19 +01:00
async clearUnread(log = null) {
if (this.isUnread || this.notificationCount) {
2021-02-24 11:22:19 +01:00
return await this._platform.logger.wrapOrRun(log, "clearUnread", async log => {
log.set("id", this.id);
const txn = await this._storage.readWriteTxn([
2021-02-24 11:22:19 +01:00
this._storage.storeNames.roomSummary,
]);
let data;
try {
data = this._summary.writeClearUnread(txn);
} catch (err) {
txn.abort();
throw err;
}
2021-02-24 11:22:19 +01:00
await txn.complete();
this._summary.applyChanges(data);
this._emitUpdate();
try {
const lastEventId = await this._getLastEventId();
if (lastEventId) {
await this._hsApi.receipt(this._roomId, "m.read", lastEventId);
}
} catch (err) {
// ignore ConnectionError
if (err.name !== "ConnectionError") {
throw err;
}
}
});
2020-08-21 11:56:10 +02:00
}
}
2020-08-19 16:58:28 +02:00
/** @public */
openTimeline(log = null) {
return this._platform.logger.wrapOrRun(log, "open timeline", async log => {
log.set("id", this.id);
if (this._timeline) {
throw new Error("not dealing with load race here for now");
}
this._timeline = new Timeline({
roomId: this.id,
storage: this._storage,
fragmentIdComparer: this._fragmentIdComparer,
pendingEvents: this._sendQueue.pendingEvents,
closeCallback: () => {
this._timeline = null;
if (this._roomEncryption) {
this._roomEncryption.notifyTimelineClosed();
}
},
clock: this._platform.clock,
logger: this._platform.logger,
});
if (this._roomEncryption) {
this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline));
}
await this._timeline.load(this._user, this._summary.data.membership, log);
return this._timeline;
});
}
2020-05-09 20:02:08 +02:00
get mediaRepository() {
return this._mediaRepository;
2020-05-09 20:02:08 +02:00
}
/** @package */
writeIsTrackingMembers(value, txn) {
return this._summary.writeIsTrackingMembers(value, txn);
}
/** @package */
applyIsTrackingMembersChanges(changes) {
this._summary.applyChanges(changes);
}
2020-09-18 13:08:18 +02:00
observeEvent(eventId) {
if (!this._observedEvents) {
this._observedEvents = new ObservedEventMap(() => {
this._observedEvents = null;
});
}
let entry = null;
if (this._timeline) {
entry = this._timeline.getByEventId(eventId);
}
const observable = this._observedEvents.observe(eventId, entry);
if (!entry) {
// update in the background
this._readEventById(eventId).then(entry => {
observable.update(entry);
}).catch(err => {
console.warn(`could not load event ${eventId} from storage`, err);
});
}
return observable;
}
async _readEventById(eventId) {
let stores = [this._storage.storeNames.timelineEvents];
if (this.isEncrypted) {
stores.push(this._storage.storeNames.inboundGroupSessions);
}
const txn = await this._storage.readTxn(stores);
const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId);
if (storageEntry) {
const entry = new EventEntry(storageEntry, this._fragmentIdComparer);
if (entry.eventType === EVENT_ENCRYPTED_TYPE) {
const request = this._decryptEntries(DecryptionSource.Timeline, [entry], txn);
await request.complete();
}
return entry;
}
}
2020-11-13 17:19:19 +01:00
createAttachment(blob, filename) {
return new AttachmentUpload({blob, filename, platform: this._platform});
}
2020-09-18 13:08:18 +02:00
dispose() {
this._roomEncryption?.dispose();
this._timeline?.dispose();
this._sendQueue.dispose();
2020-09-18 13:08:18 +02:00
}
2019-02-20 23:48:16 +01:00
}
2020-09-10 16:40:30 +02:00
class DecryptionRequest {
2021-03-15 15:23:46 +01:00
constructor(decryptFn, log) {
2020-09-10 16:40:30 +02:00
this._cancelled = false;
this.preparation = null;
2021-03-15 15:23:46 +01:00
this._promise = log.wrap("decryptEntries", log => decryptFn(this, log));
2020-09-10 16:40:30 +02:00
}
complete() {
return this._promise;
}
get cancelled() {
return this._cancelled;
}
dispose() {
this._cancelled = true;
if (this.preparation) {
this.preparation.dispose();
}
}
}