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-08-21 19:03:21 +02:00
|
|
|
import {RoomSummary, needsHeroes} from "./RoomSummary.js";
|
2020-04-20 21:26:39 +02:00
|
|
|
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";
|
2020-08-17 10:48:00 +02:00
|
|
|
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";
|
2020-08-21 18:11:07 +02:00
|
|
|
import {Heroes} from "./members/Heroes.js";
|
2018-12-21 14:35:24 +01:00
|
|
|
|
2020-04-20 21:26:39 +02:00
|
|
|
export class Room extends EventEmitter {
|
2019-07-29 10:23:15 +02:00
|
|
|
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) {
|
2019-02-20 23:48:16 +01:00
|
|
|
super();
|
2019-03-08 20:03:18 +01:00
|
|
|
this._roomId = roomId;
|
|
|
|
this._storage = storage;
|
|
|
|
this._hsApi = hsApi;
|
2020-08-21 14:26:51 +02:00
|
|
|
this._summary = new RoomSummary(roomId, user.id);
|
2019-05-12 20:24:06 +02:00
|
|
|
this._fragmentIdComparer = new FragmentIdComparer([]);
|
2020-01-04 20:04:57 +01:00
|
|
|
this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer});
|
2019-02-20 23:48:16 +01:00
|
|
|
this._emitCollectionChange = emitCollectionChange;
|
2019-07-26 22:33:33 +02:00
|
|
|
this._sendQueue = new SendQueue({roomId, storage, sendScheduler, pendingEvents});
|
2019-02-27 22:50:08 +01:00
|
|
|
this._timeline = null;
|
2019-07-29 10:23:15 +02:00
|
|
|
this._user = user;
|
2020-08-19 16:12:49 +02:00
|
|
|
this._changedMembersDuringSync = null;
|
2020-08-31 08:54:27 +02:00
|
|
|
this._memberList = null;
|
2018-12-21 14:35:24 +01:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @package */
|
2020-08-21 13:45:38 +02:00
|
|
|
async writeSync(roomResponse, membership, isInitialSync, txn) {
|
2020-08-21 14:26:51 +02:00
|
|
|
const isTimelineOpen = !!this._timeline;
|
|
|
|
const summaryChanges = this._summary.writeSync(
|
|
|
|
roomResponse,
|
|
|
|
membership,
|
|
|
|
isInitialSync, isTimelineOpen,
|
|
|
|
txn);
|
2020-08-31 09:50:57 +02:00
|
|
|
const {entries, newLiveKey, memberChanges} = await this._syncWriter.writeSync(roomResponse, txn);
|
2020-08-21 18:11:07 +02:00
|
|
|
// fetch new members while we have txn open,
|
|
|
|
// but don't make any in-memory changes yet
|
|
|
|
let heroChanges;
|
2020-08-21 19:03:21 +02:00
|
|
|
if (needsHeroes(summaryChanges)) {
|
|
|
|
// room name disappeared, open heroes
|
|
|
|
if (!this._heroes) {
|
|
|
|
this._heroes = new Heroes(this._roomId);
|
|
|
|
}
|
2020-08-31 09:50:57 +02:00
|
|
|
heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, memberChanges, txn);
|
2020-08-21 18:11:07 +02:00
|
|
|
}
|
2019-07-26 22:33:33 +02:00
|
|
|
let removedPendingEvents;
|
|
|
|
if (roomResponse.timeline && roomResponse.timeline.events) {
|
|
|
|
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn);
|
|
|
|
}
|
2020-08-21 18:11:07 +02:00
|
|
|
return {
|
|
|
|
summaryChanges,
|
|
|
|
newTimelineEntries: entries,
|
|
|
|
newLiveKey,
|
|
|
|
removedPendingEvents,
|
2020-08-31 09:50:57 +02:00
|
|
|
memberChanges,
|
2020-08-21 18:11:07 +02:00
|
|
|
heroChanges
|
|
|
|
};
|
2019-02-27 19:27:45 +01:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @package */
|
2020-08-31 09:50:57 +02:00
|
|
|
afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges}) {
|
2020-03-14 20:49:15 +01:00
|
|
|
this._syncWriter.afterSync(newLiveKey);
|
2020-08-31 09:50:57 +02:00
|
|
|
if (memberChanges.size) {
|
2020-08-19 16:12:49 +02:00
|
|
|
if (this._changedMembersDuringSync) {
|
2020-08-31 09:50:57 +02:00
|
|
|
for (const [userId, memberChange] of memberChanges.entries()) {
|
|
|
|
this._changedMembersDuringSync.set(userId, memberChange.member);
|
2020-08-19 16:12:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this._memberList) {
|
2020-08-31 09:50:57 +02:00
|
|
|
this._memberList.afterSync(memberChanges);
|
2020-08-19 16:12:49 +02:00
|
|
|
}
|
|
|
|
}
|
2020-08-21 18:11:07 +02:00
|
|
|
let emitChange = false;
|
2020-03-14 20:46:49 +01:00
|
|
|
if (summaryChanges) {
|
2020-06-26 23:26:24 +02:00
|
|
|
this._summary.applyChanges(summaryChanges);
|
2020-08-21 19:03:21 +02:00
|
|
|
if (!this._summary.needsHeroes) {
|
2020-08-21 18:11:07 +02:00
|
|
|
this._heroes = null;
|
|
|
|
}
|
|
|
|
emitChange = true;
|
|
|
|
}
|
|
|
|
if (this._heroes && heroChanges) {
|
|
|
|
const oldName = this.name;
|
|
|
|
this._heroes.applyChanges(heroChanges, this._summary);
|
|
|
|
if (oldName !== this.name) {
|
|
|
|
emitChange = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (emitChange) {
|
2019-02-20 23:48:16 +01:00
|
|
|
this.emit("change");
|
2019-02-27 23:22:47 +01:00
|
|
|
this._emitCollectionChange(this);
|
2019-02-20 23:48:16 +01:00
|
|
|
}
|
2019-02-27 22:50:08 +01:00
|
|
|
if (this._timeline) {
|
|
|
|
this._timeline.appendLiveEntries(newTimelineEntries);
|
|
|
|
}
|
2019-07-26 22:33:33 +02:00
|
|
|
if (removedPendingEvents) {
|
|
|
|
this._sendQueue.emitRemovals(removedPendingEvents);
|
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @package */
|
2019-07-26 22:40:39 +02:00
|
|
|
resumeSending() {
|
|
|
|
this._sendQueue.resumeSending();
|
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @package */
|
2020-08-21 18:11:07 +02:00
|
|
|
async load(summary, txn) {
|
2020-08-17 10:48:00 +02:00
|
|
|
try {
|
|
|
|
this._summary.load(summary);
|
2020-08-21 18:11:07 +02:00
|
|
|
// need to load members for name?
|
2020-08-21 19:03:21 +02:00
|
|
|
if (this._summary.needsHeroes) {
|
2020-08-21 18:11:07 +02:00
|
|
|
this._heroes = new Heroes(this._roomId);
|
|
|
|
const changes = await this._heroes.calculateChanges(this._summary.heroes, [], txn);
|
|
|
|
this._heroes.applyChanges(changes, this._summary);
|
|
|
|
}
|
2020-08-17 10:48:00 +02:00
|
|
|
return this._syncWriter.load(txn);
|
|
|
|
} catch (err) {
|
|
|
|
throw new WrappedError(`Could not load room ${this._roomId}`, err);
|
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
}
|
2019-02-26 22:45:58 +01:00
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2019-07-26 22:33:33 +02:00
|
|
|
sendEvent(eventType, content) {
|
2020-03-30 21:33:04 +02:00
|
|
|
return this._sendQueue.enqueueEvent(eventType, content);
|
2019-07-26 22:33:33 +02:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2020-06-26 23:26:24 +02:00
|
|
|
async loadMemberList() {
|
2020-08-19 16:13:47 +02:00
|
|
|
if (this._memberList) {
|
2020-08-31 16:10:18 +02:00
|
|
|
// TODO: also await fetchOrLoadMembers promise here
|
2020-08-19 16:13:47 +02:00
|
|
|
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,
|
|
|
|
// to handle race between /members and /sync
|
|
|
|
setChangedMembersMap: map => this._changedMembersDuringSync = map,
|
|
|
|
});
|
2020-08-19 16:13:47 +02:00
|
|
|
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 */
|
|
|
|
async fillGap(fragmentEntry, amount) {
|
2020-08-19 16:13:47 +02:00
|
|
|
// TODO move some/all of this out of Room
|
2020-08-17 17:41:10 +02:00
|
|
|
if (fragmentEntry.edgeReached) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-21 23:40:40 +01:00
|
|
|
const response = await this._hsApi.messages(this._roomId, {
|
|
|
|
from: fragmentEntry.token,
|
|
|
|
dir: fragmentEntry.direction.asApiString(),
|
|
|
|
limit: amount,
|
2020-08-20 14:39:03 +02:00
|
|
|
filter: {
|
|
|
|
lazy_load_members: true,
|
|
|
|
include_redundant_members: true,
|
|
|
|
}
|
2020-03-21 23:40:40 +01:00
|
|
|
}).response();
|
2020-03-22 00:07:37 +01:00
|
|
|
|
|
|
|
const txn = await this._storage.readWriteTxn([
|
|
|
|
this._storage.storeNames.pendingEvents,
|
|
|
|
this._storage.storeNames.timelineEvents,
|
|
|
|
this._storage.storeNames.timelineFragments,
|
|
|
|
]);
|
|
|
|
let removedPendingEvents;
|
2020-03-30 20:46:52 +02:00
|
|
|
let gapResult;
|
2020-03-22 00:07:37 +01:00
|
|
|
try {
|
|
|
|
// detect remote echos of pending messages in the gap
|
|
|
|
removedPendingEvents = this._sendQueue.removeRemoteEchos(response.chunk, txn);
|
|
|
|
// write new events into gap
|
|
|
|
const gapWriter = new GapWriter({
|
|
|
|
roomId: this._roomId,
|
|
|
|
storage: this._storage,
|
|
|
|
fragmentIdComparer: this._fragmentIdComparer
|
|
|
|
});
|
2020-03-30 20:46:52 +02:00
|
|
|
gapResult = await gapWriter.writeFragmentFill(fragmentEntry, response, txn);
|
2020-03-22 00:07:37 +01:00
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await txn.complete();
|
2020-03-30 20:46:52 +02:00
|
|
|
// once txn is committed, update in-memory state & emit events
|
|
|
|
for (const fragment of gapResult.fragments) {
|
|
|
|
this._fragmentIdComparer.add(fragment);
|
|
|
|
}
|
2020-03-22 00:07:37 +01:00
|
|
|
if (removedPendingEvents) {
|
|
|
|
this._sendQueue.emitRemovals(removedPendingEvents);
|
|
|
|
}
|
2020-03-21 23:40:40 +01:00
|
|
|
if (this._timeline) {
|
2020-03-30 20:46:52 +02:00
|
|
|
this._timeline.addGapEntries(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() {
|
2020-08-21 18:11:07 +02:00
|
|
|
if (this._heroes) {
|
|
|
|
return this._heroes.roomName;
|
|
|
|
}
|
2019-02-26 22:45:58 +01:00
|
|
|
return this._summary.name;
|
|
|
|
}
|
2019-02-26 23:27:06 +01:00
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2019-02-26 23:27:06 +01:00
|
|
|
get id() {
|
|
|
|
return this._roomId;
|
|
|
|
}
|
2019-02-27 22:50:08 +01:00
|
|
|
|
2020-08-20 17:32:55 +02:00
|
|
|
get avatarUrl() {
|
2020-08-21 18:11:07 +02:00
|
|
|
if (this._summary.avatarUrl) {
|
|
|
|
return this._summary.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.lastMessageTimestamp;
|
|
|
|
}
|
|
|
|
|
|
|
|
get isUnread() {
|
|
|
|
return this._summary.isUnread;
|
|
|
|
}
|
|
|
|
|
|
|
|
get notificationCount() {
|
|
|
|
return this._summary.notificationCount;
|
|
|
|
}
|
2020-08-21 15:50:32 +02:00
|
|
|
|
|
|
|
get highlightCount() {
|
|
|
|
return this._summary.highlightCount;
|
|
|
|
}
|
2020-08-21 11:56:45 +02:00
|
|
|
|
2020-08-27 20:52:51 +02:00
|
|
|
get isLowPriority() {
|
|
|
|
const tags = this._summary.tags;
|
|
|
|
return !!(tags && tags['m.lowpriority']);
|
|
|
|
}
|
|
|
|
|
2020-08-28 14:36:00 +02:00
|
|
|
get isEncrypted() {
|
|
|
|
return !!this._summary.encryption;
|
|
|
|
}
|
|
|
|
|
2020-08-31 08:53:47 +02:00
|
|
|
get isTrackingMembers() {
|
|
|
|
return this._summary.isTrackingMembers;
|
|
|
|
}
|
|
|
|
|
2020-08-21 15:16:57 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-21 11:56:10 +02:00
|
|
|
async clearUnread() {
|
2020-08-21 15:16:57 +02:00
|
|
|
if (this.isUnread || this.notificationCount) {
|
2020-08-21 14:11:42 +02:00
|
|
|
const txn = await this._storage.readWriteTxn([
|
|
|
|
this._storage.storeNames.roomSummary,
|
|
|
|
]);
|
|
|
|
let data;
|
|
|
|
try {
|
|
|
|
data = this._summary.writeClearUnread(txn);
|
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await txn.complete();
|
|
|
|
this._summary.applyChanges(data);
|
|
|
|
this.emit("change");
|
|
|
|
this._emitCollectionChange(this);
|
2020-08-21 15:23:25 +02:00
|
|
|
|
|
|
|
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 */
|
2019-02-27 22:50:08 +01:00
|
|
|
async openTimeline() {
|
|
|
|
if (this._timeline) {
|
|
|
|
throw new Error("not dealing with load race here for now");
|
|
|
|
}
|
2020-05-07 19:14:30 +02:00
|
|
|
console.log(`opening the timeline for ${this._roomId}`);
|
2019-02-27 22:50:08 +01:00
|
|
|
this._timeline = new Timeline({
|
|
|
|
roomId: this.id,
|
|
|
|
storage: this._storage,
|
2019-05-12 20:26:03 +02:00
|
|
|
fragmentIdComparer: this._fragmentIdComparer,
|
2019-07-26 22:33:33 +02:00
|
|
|
pendingEvents: this._sendQueue.pendingEvents,
|
2020-05-07 19:14:30 +02:00
|
|
|
closeCallback: () => {
|
|
|
|
console.log(`closing the timeline for ${this._roomId}`);
|
|
|
|
this._timeline = null;
|
|
|
|
},
|
2019-07-29 10:23:15 +02:00
|
|
|
user: this._user,
|
2019-02-27 22:50:08 +01:00
|
|
|
});
|
|
|
|
await this._timeline.load();
|
|
|
|
return this._timeline;
|
|
|
|
}
|
2020-05-09 20:02:08 +02:00
|
|
|
|
2020-08-20 15:40:43 +02:00
|
|
|
get mediaRepository() {
|
|
|
|
return this._hsApi.mediaRepository;
|
2020-05-09 20:02:08 +02:00
|
|
|
}
|
2020-08-31 14:11:08 +02:00
|
|
|
|
|
|
|
/** @package */
|
|
|
|
writeIsTrackingMembers(value, txn) {
|
|
|
|
return this._summary.writeIsTrackingMembers(value, txn);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @package */
|
|
|
|
applyIsTrackingMembersChanges(changes) {
|
|
|
|
this._summary.applyChanges(changes);
|
|
|
|
}
|
2019-02-20 23:48:16 +01:00
|
|
|
}
|
2019-02-27 22:50:08 +01:00
|
|
|
|