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-08-28 14:36:00 +02:00
|
|
|
import {MEGOLM_ALGORITHM} from "../e2ee/common.js";
|
|
|
|
|
2020-09-14 16:34:07 +02:00
|
|
|
|
2020-09-23 19:11:11 +02:00
|
|
|
function applyTimelineEntries(data, timelineEntries, isInitialSync, canMarkUnread, ownUserId) {
|
2020-09-14 16:34:07 +02:00
|
|
|
if (timelineEntries.length) {
|
|
|
|
data = timelineEntries.reduce((data, entry) => {
|
|
|
|
return processTimelineEvent(data, entry,
|
2020-09-23 19:11:11 +02:00
|
|
|
isInitialSync, canMarkUnread, ownUserId);
|
2020-09-14 16:34:07 +02:00
|
|
|
}, data);
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applySyncResponse(data, roomResponse, membership) {
|
2020-03-14 20:46:49 +01:00
|
|
|
if (roomResponse.summary) {
|
|
|
|
data = updateSummary(data, roomResponse.summary);
|
|
|
|
}
|
|
|
|
if (membership !== data.membership) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.membership = membership;
|
|
|
|
}
|
2020-08-27 20:52:51 +02:00
|
|
|
if (roomResponse.account_data) {
|
|
|
|
data = roomResponse.account_data.events.reduce(processRoomAccountData, data);
|
|
|
|
}
|
2020-09-23 14:26:14 +02:00
|
|
|
const stateEvents = roomResponse?.state?.events;
|
2020-03-14 20:46:49 +01:00
|
|
|
// state comes before timeline
|
2020-09-23 14:26:14 +02:00
|
|
|
if (Array.isArray(stateEvents)) {
|
|
|
|
data = stateEvents.reduce(processStateEvent, data);
|
2020-03-14 20:46:49 +01:00
|
|
|
}
|
2020-09-23 14:26:14 +02:00
|
|
|
const timelineEvents = roomResponse?.timeline?.events;
|
2020-09-14 16:34:07 +02:00
|
|
|
// process state events in timeline
|
|
|
|
// non-state events are handled by applyTimelineEntries
|
|
|
|
// so decryption is handled properly
|
2020-09-23 14:26:14 +02:00
|
|
|
if (Array.isArray(timelineEvents)) {
|
|
|
|
data = timelineEvents.reduce((data, event) => {
|
2020-09-14 16:34:07 +02:00
|
|
|
if (typeof event.state_key === "string") {
|
|
|
|
return processStateEvent(data, event);
|
2020-08-21 14:35:23 +02:00
|
|
|
}
|
2020-09-14 17:45:13 +02:00
|
|
|
return data;
|
2020-08-21 13:45:38 +02:00
|
|
|
}, data);
|
2020-03-14 20:46:49 +01:00
|
|
|
}
|
2020-08-20 17:07:02 +02:00
|
|
|
const unreadNotifications = roomResponse.unread_notifications;
|
|
|
|
if (unreadNotifications) {
|
2020-09-24 16:45:28 +02:00
|
|
|
const highlightCount = unreadNotifications.highlight_count || 0;
|
|
|
|
if (highlightCount !== data.highlightCount) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.highlightCount = highlightCount;
|
|
|
|
}
|
|
|
|
const notificationCount = unreadNotifications.notification_count;
|
|
|
|
if (notificationCount !== data.notificationCount) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.notificationCount = notificationCount;
|
|
|
|
}
|
2020-08-20 17:07:02 +02:00
|
|
|
}
|
2020-03-14 20:46:49 +01:00
|
|
|
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2020-08-27 20:52:51 +02:00
|
|
|
function processRoomAccountData(data, event) {
|
|
|
|
if (event?.type === "m.tag") {
|
|
|
|
let tags = event?.content?.tags;
|
|
|
|
if (!tags || Array.isArray(tags) || typeof tags !== "object") {
|
|
|
|
tags = null;
|
|
|
|
}
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.tags = tags;
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2020-08-21 14:35:23 +02:00
|
|
|
function processStateEvent(data, event) {
|
2020-03-14 20:46:49 +01:00
|
|
|
if (event.type === "m.room.encryption") {
|
2020-08-28 14:36:00 +02:00
|
|
|
const algorithm = event.content?.algorithm;
|
|
|
|
if (!data.encryption && algorithm === MEGOLM_ALGORITHM) {
|
2020-03-14 20:46:49 +01:00
|
|
|
data = data.cloneIfNeeded();
|
2020-08-28 14:36:00 +02:00
|
|
|
data.encryption = event.content;
|
2020-03-14 20:46:49 +01:00
|
|
|
}
|
2020-08-21 11:55:25 +02:00
|
|
|
} else if (event.type === "m.room.name") {
|
2020-08-20 17:02:51 +02:00
|
|
|
const newName = event.content?.name;
|
2020-03-14 20:46:49 +01:00
|
|
|
if (newName !== data.name) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.name = newName;
|
|
|
|
}
|
2020-08-21 11:55:25 +02:00
|
|
|
} else if (event.type === "m.room.avatar") {
|
2020-08-20 17:02:51 +02:00
|
|
|
const newUrl = event.content?.url;
|
|
|
|
if (newUrl !== data.avatarUrl) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.avatarUrl = newUrl;
|
|
|
|
}
|
2020-08-21 14:35:23 +02:00
|
|
|
} else if (event.type === "m.room.canonical_alias") {
|
|
|
|
const content = event.content;
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.canonicalAlias = content.alias;
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2020-09-23 19:11:11 +02:00
|
|
|
function processTimelineEvent(data, eventEntry, isInitialSync, canMarkUnread, ownUserId) {
|
2020-09-14 14:01:47 +02:00
|
|
|
if (eventEntry.eventType === "m.room.message") {
|
2020-09-14 16:34:07 +02:00
|
|
|
if (!data.lastMessageTimestamp || eventEntry.timestamp > data.lastMessageTimestamp) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.lastMessageTimestamp = eventEntry.timestamp;
|
|
|
|
}
|
2020-09-23 19:11:11 +02:00
|
|
|
if (!isInitialSync && eventEntry.sender !== ownUserId && canMarkUnread) {
|
2020-09-14 16:34:07 +02:00
|
|
|
data = data.cloneIfNeeded();
|
2020-08-21 13:45:38 +02:00
|
|
|
data.isUnread = true;
|
|
|
|
}
|
2020-09-22 18:22:37 +02:00
|
|
|
}
|
2020-03-14 20:46:49 +01:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateSummary(data, summary) {
|
|
|
|
const heroes = summary["m.heroes"];
|
2020-08-21 18:11:26 +02:00
|
|
|
const joinCount = summary["m.joined_member_count"];
|
|
|
|
const inviteCount = summary["m.invited_member_count"];
|
2020-08-31 16:09:38 +02:00
|
|
|
// TODO: we could easily calculate if all members are available here and set hasFetchedMembers?
|
|
|
|
// so we can avoid calling /members...
|
|
|
|
// we'd need to do a count query in the roomMembers store though ...
|
2020-08-21 18:11:07 +02:00
|
|
|
if (heroes && Array.isArray(heroes)) {
|
2020-03-14 20:46:49 +01:00
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.heroes = heroes;
|
|
|
|
}
|
|
|
|
if (Number.isInteger(inviteCount)) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.inviteCount = inviteCount;
|
|
|
|
}
|
|
|
|
if (Number.isInteger(joinCount)) {
|
|
|
|
data = data.cloneIfNeeded();
|
|
|
|
data.joinCount = joinCount;
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
class SummaryData {
|
|
|
|
constructor(copy, roomId) {
|
|
|
|
this.roomId = copy ? copy.roomId : roomId;
|
|
|
|
this.name = copy ? copy.name : null;
|
2020-08-21 11:55:47 +02:00
|
|
|
this.lastMessageTimestamp = copy ? copy.lastMessageTimestamp : null;
|
2020-08-21 14:10:53 +02:00
|
|
|
this.isUnread = copy ? copy.isUnread : false;
|
2020-08-28 14:36:00 +02:00
|
|
|
this.encryption = copy ? copy.encryption : null;
|
2020-03-14 20:46:49 +01:00
|
|
|
this.membership = copy ? copy.membership : null;
|
|
|
|
this.inviteCount = copy ? copy.inviteCount : 0;
|
|
|
|
this.joinCount = copy ? copy.joinCount : 0;
|
|
|
|
this.heroes = copy ? copy.heroes : null;
|
|
|
|
this.canonicalAlias = copy ? copy.canonicalAlias : null;
|
2020-06-26 23:26:24 +02:00
|
|
|
this.hasFetchedMembers = copy ? copy.hasFetchedMembers : false;
|
2020-08-31 08:53:47 +02:00
|
|
|
this.isTrackingMembers = copy ? copy.isTrackingMembers : false;
|
2020-08-20 17:02:51 +02:00
|
|
|
this.avatarUrl = copy ? copy.avatarUrl : null;
|
2020-08-20 17:07:02 +02:00
|
|
|
this.notificationCount = copy ? copy.notificationCount : 0;
|
|
|
|
this.highlightCount = copy ? copy.highlightCount : 0;
|
2020-08-27 20:52:51 +02:00
|
|
|
this.tags = copy ? copy.tags : null;
|
2020-03-14 20:46:49 +01:00
|
|
|
this.cloned = copy ? true : false;
|
|
|
|
}
|
|
|
|
|
2021-03-02 21:29:32 +01:00
|
|
|
diff(other) {
|
|
|
|
const props = Object.getOwnPropertyNames(this);
|
|
|
|
return props.reduce((diff, prop) => {
|
|
|
|
if (prop !== "cloned") {
|
|
|
|
if (this[prop] !== other[prop]) {
|
|
|
|
diff[prop] = this[prop];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return diff;
|
|
|
|
}, {});
|
|
|
|
}
|
|
|
|
|
2020-03-14 20:46:49 +01:00
|
|
|
cloneIfNeeded() {
|
|
|
|
if (this.cloned) {
|
|
|
|
return this;
|
|
|
|
} else {
|
|
|
|
return new SummaryData(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
serialize() {
|
|
|
|
const {cloned, ...serializedProps} = this;
|
|
|
|
return serializedProps;
|
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
|
2020-09-23 19:11:11 +02:00
|
|
|
applyTimelineEntries(timelineEntries, isInitialSync, canMarkUnread, ownUserId) {
|
|
|
|
return applyTimelineEntries(this, timelineEntries, isInitialSync, canMarkUnread, ownUserId);
|
2020-08-21 18:11:07 +02:00
|
|
|
}
|
|
|
|
|
2020-09-23 14:26:14 +02:00
|
|
|
applySyncResponse(roomResponse, membership) {
|
|
|
|
return applySyncResponse(this, roomResponse, membership);
|
2020-08-28 14:36:00 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 19:03:21 +02:00
|
|
|
get needsHeroes() {
|
2020-09-23 14:26:14 +02:00
|
|
|
return !this.name && !this.canonicalAlias && this.heroes && this.heroes.length > 0;
|
2020-08-21 11:55:47 +02:00
|
|
|
}
|
2020-09-23 14:26:14 +02:00
|
|
|
}
|
2020-08-21 11:55:47 +02:00
|
|
|
|
2020-09-23 14:26:14 +02:00
|
|
|
export class RoomSummary {
|
|
|
|
constructor(roomId) {
|
2020-09-24 16:40:51 +02:00
|
|
|
this._data = null;
|
|
|
|
this.applyChanges(new SummaryData(null, roomId));
|
2018-12-21 14:35:24 +01:00
|
|
|
}
|
|
|
|
|
2020-09-23 14:26:14 +02:00
|
|
|
get data() {
|
|
|
|
return this._data;
|
2020-09-22 18:22:37 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 11:56:10 +02:00
|
|
|
writeClearUnread(txn) {
|
|
|
|
const data = new SummaryData(this._data);
|
|
|
|
data.isUnread = false;
|
|
|
|
data.notificationCount = 0;
|
|
|
|
data.highlightCount = 0;
|
|
|
|
txn.roomSummary.set(data.serialize());
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2020-06-26 23:26:24 +02:00
|
|
|
writeHasFetchedMembers(value, txn) {
|
|
|
|
const data = new SummaryData(this._data);
|
|
|
|
data.hasFetchedMembers = value;
|
|
|
|
txn.roomSummary.set(data.serialize());
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2020-08-31 08:53:47 +02:00
|
|
|
writeIsTrackingMembers(value, txn) {
|
|
|
|
const data = new SummaryData(this._data);
|
|
|
|
data.isTrackingMembers = value;
|
|
|
|
txn.roomSummary.set(data.serialize());
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2020-09-23 14:26:14 +02:00
|
|
|
writeData(data, txn) {
|
2020-03-14 20:46:49 +01:00
|
|
|
if (data !== this._data) {
|
|
|
|
txn.roomSummary.set(data.serialize());
|
|
|
|
return data;
|
2018-12-21 14:35:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-23 14:26:14 +02:00
|
|
|
async writeAndApplyData(data, storage) {
|
|
|
|
if (data === this._data) {
|
2020-09-23 19:10:25 +02:00
|
|
|
return false;
|
2020-09-23 14:26:14 +02:00
|
|
|
}
|
2021-03-04 19:47:02 +01:00
|
|
|
const txn = await storage.readWriteTxn([
|
2020-09-14 16:34:07 +02:00
|
|
|
storage.storeNames.roomSummary,
|
|
|
|
]);
|
|
|
|
try {
|
|
|
|
txn.roomSummary.set(data.serialize());
|
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await txn.complete();
|
|
|
|
this.applyChanges(data);
|
2020-09-23 19:10:25 +02:00
|
|
|
return true;
|
2020-09-14 16:34:07 +02:00
|
|
|
}
|
|
|
|
|
2020-06-26 23:26:24 +02:00
|
|
|
applyChanges(data) {
|
2020-03-14 20:46:49 +01:00
|
|
|
this._data = data;
|
2020-09-23 14:26:14 +02:00
|
|
|
// clear cloned flag, so cloneIfNeeded makes a copy and
|
|
|
|
// this._data is not modified if any field is changed.
|
|
|
|
this._data.cloned = false;
|
2020-03-14 20:46:49 +01:00
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
|
2020-03-14 20:46:49 +01:00
|
|
|
async load(summary) {
|
2020-09-24 16:40:51 +02:00
|
|
|
this.applyChanges(new SummaryData(summary));
|
2018-12-21 14:35:24 +01:00
|
|
|
}
|
2020-03-14 20:46:49 +01:00
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
|
2020-03-14 20:46:49 +01:00
|
|
|
export function tests() {
|
|
|
|
return {
|
2020-09-23 18:28:21 +02:00
|
|
|
"membership trigger change": function(assert) {
|
2020-03-14 20:46:49 +01:00
|
|
|
const summary = new RoomSummary("id");
|
2020-03-14 21:38:37 +01:00
|
|
|
let written = false;
|
2020-09-23 18:20:29 +02:00
|
|
|
let changes = summary.data.applySyncResponse({}, "join");
|
|
|
|
const txn = {roomSummary: {set: () => { written = true; }}};
|
|
|
|
changes = summary.writeData(changes, txn);
|
2020-03-14 20:46:49 +01:00
|
|
|
assert(changes);
|
2020-03-14 21:38:37 +01:00
|
|
|
assert(written);
|
|
|
|
assert.equal(changes.membership, "join");
|
2019-10-13 07:48:33 +02:00
|
|
|
}
|
2020-03-14 20:46:49 +01:00
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
}
|