Merge pull request #62 from vector-im/bwindels/heroes

Implement heroes logic
This commit is contained in:
Bruno Windels 2020-08-21 16:16:04 +00:00 committed by GitHub
commit 16f13a3b4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 173 additions and 28 deletions

View file

@ -84,7 +84,7 @@ export class RoomViewModel extends ViewModel {
} }
get name() { get name() {
return this._room.name; return this._room.name || this.i18n`Empty Room`;
} }
get timelineViewModel() { get timelineViewModel() {
@ -102,7 +102,7 @@ export class RoomViewModel extends ViewModel {
} }
get avatarLetter() { get avatarLetter() {
return avatarInitials(this._room.name); return avatarInitials(this.name);
} }
get avatarColorNumber() { get avatarColorNumber() {

View file

@ -70,7 +70,7 @@ export class RoomTileViewModel extends ViewModel {
const timeDiff = theirTimestamp - myTimestamp; const timeDiff = theirTimestamp - myTimestamp;
if (timeDiff === 0) { if (timeDiff === 0) {
// sort alphabetically // sort alphabetically
const nameCmp = this._room.name.localeCompare(other._room.name); const nameCmp = this.name.localeCompare(other.name);
if (nameCmp === 0) { if (nameCmp === 0) {
return this._room.id.localeCompare(other._room.id); return this._room.id.localeCompare(other._room.id);
} }
@ -88,12 +88,12 @@ export class RoomTileViewModel extends ViewModel {
} }
get name() { get name() {
return this._room.name; return this._room.name || this.i18n`Empty Room`;
} }
// Avatar view model contract // Avatar view model contract
get avatarLetter() { get avatarLetter() {
return avatarInitials(this._room.name); return avatarInitials(this.name);
} }
get avatarColorNumber() { get avatarColorNumber() {

View file

@ -36,7 +36,7 @@ export class Session {
const txn = await this._storage.readTxn([ const txn = await this._storage.readTxn([
this._storage.storeNames.session, this._storage.storeNames.session,
this._storage.storeNames.roomSummary, this._storage.storeNames.roomSummary,
this._storage.storeNames.roomState, this._storage.storeNames.roomMembers,
this._storage.storeNames.timelineEvents, this._storage.storeNames.timelineEvents,
this._storage.storeNames.timelineFragments, this._storage.storeNames.timelineFragments,
this._storage.storeNames.pendingEvents, this._storage.storeNames.pendingEvents,

View file

@ -24,6 +24,7 @@ import {SendQueue} from "./sending/SendQueue.js";
import {WrappedError} from "../error.js" import {WrappedError} from "../error.js"
import {fetchOrLoadMembers} from "./members/load.js"; import {fetchOrLoadMembers} from "./members/load.js";
import {MemberList} from "./members/MemberList.js"; import {MemberList} from "./members/MemberList.js";
import {Heroes} from "./members/Heroes.js";
export class Room extends EventEmitter { export class Room extends EventEmitter {
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) { constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) {
@ -50,15 +51,32 @@ export class Room extends EventEmitter {
isInitialSync, isTimelineOpen, isInitialSync, isTimelineOpen,
txn); txn);
const {entries, newLiveKey, changedMembers} = await this._syncWriter.writeSync(roomResponse, txn); const {entries, newLiveKey, changedMembers} = await this._syncWriter.writeSync(roomResponse, txn);
// room name disappeared, open heroes
if (!summaryChanges.name && summaryChanges.heroes && !this._heroes) {
this._heroes = new Heroes(this._roomId);
}
// fetch new members while we have txn open,
// but don't make any in-memory changes yet
let heroChanges;
if (summaryChanges.heroes && this._heroes) {
heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, changedMembers, txn);
}
let removedPendingEvents; let removedPendingEvents;
if (roomResponse.timeline && roomResponse.timeline.events) { if (roomResponse.timeline && roomResponse.timeline.events) {
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn); removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn);
} }
return {summaryChanges, newTimelineEntries: entries, newLiveKey, removedPendingEvents, changedMembers}; return {
summaryChanges,
newTimelineEntries: entries,
newLiveKey,
removedPendingEvents,
changedMembers,
heroChanges
};
} }
/** @package */ /** @package */
afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, changedMembers}) { afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, changedMembers, heroChanges}) {
this._syncWriter.afterSync(newLiveKey); this._syncWriter.afterSync(newLiveKey);
if (changedMembers.length) { if (changedMembers.length) {
if (this._changedMembersDuringSync) { if (this._changedMembersDuringSync) {
@ -70,8 +88,22 @@ export class Room extends EventEmitter {
this._memberList.afterSync(changedMembers); this._memberList.afterSync(changedMembers);
} }
} }
let emitChange = false;
if (summaryChanges) { if (summaryChanges) {
this._summary.applyChanges(summaryChanges); this._summary.applyChanges(summaryChanges);
if (this._summary.name && this._heroes) {
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) {
this.emit("change"); this.emit("change");
this._emitCollectionChange(this); this._emitCollectionChange(this);
} }
@ -89,9 +121,15 @@ export class Room extends EventEmitter {
} }
/** @package */ /** @package */
load(summary, txn) { async load(summary, txn) {
try { try {
this._summary.load(summary); this._summary.load(summary);
// need to load members for name?
if (!this._summary.name && this._summary.heroes) {
this._heroes = new Heroes(this._roomId);
const changes = await this._heroes.calculateChanges(this._summary.heroes, [], txn);
this._heroes.applyChanges(changes, this._summary);
}
return this._syncWriter.load(txn); return this._syncWriter.load(txn);
} catch (err) { } catch (err) {
throw new WrappedError(`Could not load room ${this._roomId}`, err); throw new WrappedError(`Could not load room ${this._roomId}`, err);
@ -177,6 +215,9 @@ export class Room extends EventEmitter {
/** @public */ /** @public */
get name() { get name() {
if (this._heroes) {
return this._heroes.roomName;
}
return this._summary.name; return this._summary.name;
} }
@ -186,7 +227,12 @@ export class Room extends EventEmitter {
} }
get avatarUrl() { get avatarUrl() {
if (this._summary.avatarUrl) {
return this._summary.avatarUrl; return this._summary.avatarUrl;
} else if (this._heroes) {
return this._heroes.roomAvatarUrl;
}
return null;
} }
get lastMessageTimestamp() { get lastMessageTimestamp() {

View file

@ -73,7 +73,6 @@ function processStateEvent(data, event) {
const content = event.content; const content = event.content;
data = data.cloneIfNeeded(); data = data.cloneIfNeeded();
data.canonicalAlias = content.alias; data.canonicalAlias = content.alias;
data.altAliases = content.alt_aliases;
} }
return data; return data;
} }
@ -97,10 +96,10 @@ function processTimelineEvent(data, event, isInitialSync, isTimelineOpen, ownUse
function updateSummary(data, summary) { function updateSummary(data, summary) {
const heroes = summary["m.heroes"]; const heroes = summary["m.heroes"];
const inviteCount = summary["m.joined_member_count"]; const joinCount = summary["m.joined_member_count"];
const joinCount = summary["m.invited_member_count"]; const inviteCount = summary["m.invited_member_count"];
if (heroes) { if (heroes && Array.isArray(heroes)) {
data = data.cloneIfNeeded(); data = data.cloneIfNeeded();
data.heroes = heroes; data.heroes = heroes;
} }
@ -129,7 +128,6 @@ class SummaryData {
this.joinCount = copy ? copy.joinCount : 0; this.joinCount = copy ? copy.joinCount : 0;
this.heroes = copy ? copy.heroes : null; this.heroes = copy ? copy.heroes : null;
this.canonicalAlias = copy ? copy.canonicalAlias : null; this.canonicalAlias = copy ? copy.canonicalAlias : null;
this.altAliases = copy ? copy.altAliases : null;
this.hasFetchedMembers = copy ? copy.hasFetchedMembers : false; this.hasFetchedMembers = copy ? copy.hasFetchedMembers : false;
this.lastPaginationToken = copy ? copy.lastPaginationToken : null; this.lastPaginationToken = copy ? copy.lastPaginationToken : null;
this.avatarUrl = copy ? copy.avatarUrl : null; this.avatarUrl = copy ? copy.avatarUrl : null;
@ -165,13 +163,11 @@ export class RoomSummary {
if (this._data.canonicalAlias) { if (this._data.canonicalAlias) {
return this._data.canonicalAlias; return this._data.canonicalAlias;
} }
if (Array.isArray(this._data.altAliases) && this._data.altAliases.length !== 0) { return null;
return this._data.altAliases[0];
} }
if (Array.isArray(this._data.heroes) && this._data.heroes.length !== 0) {
return this._data.heroes.join(", "); get heroes() {
} return this._data.heroes;
return this._data.roomId;
} }
get isUnread() { get isUnread() {
@ -240,12 +236,6 @@ export class RoomSummary {
isInitialSync, isTimelineOpen, isInitialSync, isTimelineOpen,
this._ownUserId); this._ownUserId);
if (data !== this._data) { if (data !== this._data) {
// need to think here how we want to persist
// things like unread status (as read marker, or unread count)?
// we could very well load additional things in the load method
// ... the trade-off is between constantly writing the summary
// on every sync, or doing a bit of extra reading on load
// and have in-memory only variables for visualization
txn.roomSummary.set(data.serialize()); txn.roomSummary.set(data.serialize());
return data; return data;
} }

View file

@ -0,0 +1,101 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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.
*/
import {RoomMember} from "./RoomMember.js";
function calculateRoomName(sortedMembers, summary) {
const countWithoutMe = summary.joinCount + summary.inviteCount - 1;
if (sortedMembers.length >= countWithoutMe) {
if (sortedMembers.length > 1) {
const lastMember = sortedMembers[sortedMembers.length - 1];
const firstMembers = sortedMembers.slice(0, sortedMembers.length - 1);
return firstMembers.map(m => m.displayName).join(", ") + " and " + lastMember.displayName;
} else {
return sortedMembers[0].displayName;
}
} else if (sortedMembers.length < countWithoutMe) {
return sortedMembers.map(m => m.displayName).join(", ") + ` and ${countWithoutMe} others`;
} else {
// Empty Room
return null;
}
}
export class Heroes {
constructor(roomId) {
this._roomId = roomId;
this._members = new Map();
}
/**
* @param {string[]} newHeroes array of user ids
* @param {RoomMember[]} changedMembers array of changed members in this sync
* @param {Transaction} txn
* @return {Promise}
*/
async calculateChanges(newHeroes, changedMembers, txn) {
const updatedHeroMembers = new Map();
const removedUserIds = [];
// remove non-present members
for (const existingUserId of this._members.keys()) {
if (newHeroes.indexOf(existingUserId) === -1) {
removedUserIds.push(existingUserId);
}
}
// update heroes with synced member changes
for (const member of changedMembers) {
if (this._members.has(member.userId) || newHeroes.indexOf(member.userId) !== -1) {
updatedHeroMembers.set(member.userId, member);
}
}
// load member for new heroes from storage
for (const userId of newHeroes) {
if (!this._members.has(userId) && !updatedHeroMembers.has(userId)) {
const memberData = await txn.roomMembers.get(this._roomId, userId);
if (memberData) {
const member = new RoomMember(memberData);
updatedHeroMembers.set(member.userId, member);
}
}
}
return {updatedHeroMembers: updatedHeroMembers.values(), removedUserIds};
}
applyChanges({updatedHeroMembers, removedUserIds}, summary) {
for (const userId of removedUserIds) {
this._members.delete(userId);
}
for (const member of updatedHeroMembers) {
this._members.set(member.userId, member);
}
const sortedMembers = Array.from(this._members.values()).sort((a, b) => a.displayName.localeCompare(b.displayName));
this._roomName = calculateRoomName(sortedMembers, summary);
}
get roomName() {
return this._roomName;
}
get roomAvatarUrl() {
if (this._members.size === 1) {
for (const member of this._members.values()) {
console.log("roomAvatarUrl", member, member.avatarUrl);
return member.avatarUrl;
}
}
return null;
}
}

View file

@ -54,6 +54,14 @@ export class RoomMember {
}); });
} }
get displayName() {
return this._data.displayName;
}
get avatarUrl() {
return this._data.avatarUrl;
}
get roomId() { get roomId() {
return this._data.roomId; return this._data.roomId;
} }