diff --git a/package.json b/package.json index 2034f6b9..3935c174 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.0.32", + "version": "0.0.33", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "main": "index.js", "directories": { diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 879c5e26..152b77ac 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -57,24 +57,20 @@ export class SessionViewModel extends ViewModel { } _closeCurrentRoom() { - if (this._currentRoomViewModel) { - this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); - this.emitChange("currentRoom"); - } + this._currentRoomTileViewModel?.close(); + this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); } _openRoom(room, roomTileVM) { - if (this._currentRoomTileViewModel) { - this._currentRoomTileViewModel.close(); - } + this._closeCurrentRoom(); this._currentRoomTileViewModel = roomTileVM; - if (this._currentRoomViewModel) { - this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); - } this._currentRoomViewModel = this.track(new RoomViewModel(this.childOptions({ room, ownUserId: this._session.user.id, - closeCallback: () => this._closeCurrentRoom(), + closeCallback: () => { + this._closeCurrentRoom(); + this.emitChange("currentRoom"); + }, }))); this._currentRoomViewModel.load(); this.emitChange("currentRoom"); diff --git a/src/domain/session/roomlist/RoomTileViewModel.js b/src/domain/session/roomlist/RoomTileViewModel.js index ec344f79..e586bbd1 100644 --- a/src/domain/session/roomlist/RoomTileViewModel.js +++ b/src/domain/session/roomlist/RoomTileViewModel.js @@ -49,34 +49,61 @@ export class RoomTileViewModel extends ViewModel { } compare(other) { + /* + put unread rooms first + then put rooms with a timestamp first, and sort by name + then sort by name for rooms without a timestamp + */ const myRoom = this._room; const theirRoom = other._room; + let buf = ""; + function log(...args) { + buf = buf + args.map(a => a+"").join(" ") + "\n"; + } + function logResult(result) { + if (result === 0) { + log("rooms are equal (should not happen)", result); + } else if (result > 0) { + log(`${theirRoom.name || theirRoom.id} comes first`, result); + } else { + log(`${myRoom.name || myRoom.id} comes first`, result); + } + console.info(buf); + return result; + } + log(`comparing ${myRoom.name || theirRoom.id} and ${theirRoom.name || theirRoom.id} ...`); + log("comparing isUnread..."); if (isSortedAsUnread(this) !== isSortedAsUnread(other)) { if (isSortedAsUnread(this)) { - return -1; + return logResult(-1); } - return 1; + return logResult(1); } const myTimestamp = myRoom.lastMessageTimestamp; const theirTimestamp = theirRoom.lastMessageTimestamp; - // rooms with a timestamp come before rooms without one - if ((myTimestamp === null) !== (theirTimestamp === null)) { - if (theirTimestamp === null) { - return -1; + const myTimestampValid = Number.isSafeInteger(myTimestamp); + const theirTimestampValid = Number.isSafeInteger(theirTimestamp); + // if either does not have a timestamp, put the one with a timestamp first + if (myTimestampValid !== theirTimestampValid) { + log("checking if either does not have lastMessageTimestamp ...", myTimestamp, theirTimestamp); + if (!theirTimestampValid) { + return logResult(-1); } - return 1; + return logResult(1); } const timeDiff = theirTimestamp - myTimestamp; - if (timeDiff === 0) { + if (timeDiff === 0 || !theirTimestampValid || !myTimestampValid) { + log("checking name ...", myTimestamp, theirTimestamp); // sort alphabetically const nameCmp = this.name.localeCompare(other.name); if (nameCmp === 0) { - return this._room.id.localeCompare(other._room.id); + return logResult(this._room.id.localeCompare(other._room.id)); } - return nameCmp; + return logResult(nameCmp); } - return timeDiff; + log("checking timestamp ..."); + return logResult(timeDiff); } get isOpen() { diff --git a/src/matrix/room/members/Heroes.js b/src/matrix/room/members/Heroes.js index 79902579..f7ccb2df 100644 --- a/src/matrix/room/members/Heroes.js +++ b/src/matrix/room/members/Heroes.js @@ -22,12 +22,12 @@ function calculateRoomName(sortedMembers, summary) { 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; + return firstMembers.map(m => m.name).join(", ") + " and " + lastMember.name; } else { - return sortedMembers[0].displayName; + return sortedMembers[0].name; } } else if (sortedMembers.length < countWithoutMe) { - return sortedMembers.map(m => m.displayName).join(", ") + ` and ${countWithoutMe} others`; + return sortedMembers.map(m => m.name).join(", ") + ` and ${countWithoutMe} others`; } else { // Empty Room return null; @@ -81,7 +81,7 @@ export class Heroes { 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)); + const sortedMembers = Array.from(this._members.values()).sort((a, b) => a.name.localeCompare(b.name)); this._roomName = calculateRoomName(sortedMembers, summary); } @@ -92,7 +92,6 @@ export class Heroes { get roomAvatarUrl() { if (this._members.size === 1) { for (const member of this._members.values()) { - console.log("roomAvatarUrl", member, member.avatarUrl); return member.avatarUrl; } } diff --git a/src/matrix/room/members/RoomMember.js b/src/matrix/room/members/RoomMember.js index e6303fe4..02c3c292 100644 --- a/src/matrix/room/members/RoomMember.js +++ b/src/matrix/room/members/RoomMember.js @@ -27,21 +27,33 @@ export class RoomMember { if (typeof userId !== "string") { return; } - return this._fromMemberEventContent(roomId, userId, memberEvent.content); + const content = memberEvent.content; + const prevContent = memberEvent.unsigned?.prev_content; + const membership = content?.membership; + // fall back to prev_content for these as synapse doesn't (always?) + // put them on content for "leave" memberships + const displayName = content?.displayname || prevContent?.displayname; + const avatarUrl = content?.avatar_url || prevContent?.avatar_url; + return this._validateAndCreateMember(roomId, userId, membership, displayName, avatarUrl); } - + /** + * Creates a (historical) member from a member event that is the next member event + * after the point in time where we need a member for. This will use `prev_content`. + */ static fromReplacingMemberEvent(roomId, memberEvent) { const userId = memberEvent && memberEvent.state_key; if (typeof userId !== "string") { return; } - return this._fromMemberEventContent(roomId, userId, memberEvent.prev_content); + const content = memberEvent.unsigned?.prev_content + return this._validateAndCreateMember(roomId, userId, + content?.membership, + content?.displayname, + content?.avatar_url + ); } - static _fromMemberEventContent(roomId, userId, content) { - const membership = content?.membership; - const avatarUrl = content?.avatar_url; - const displayName = content?.displayname; + static _validateAndCreateMember(roomId, userId, membership, displayName, avatarUrl) { if (typeof membership !== "string") { return; } @@ -54,10 +66,23 @@ export class RoomMember { }); } + /** + * @return {String?} the display name, if any + */ get displayName() { return this._data.displayName; } + /** + * @return {String} the display name or userId + */ + get name() { + return this._data.displayName || this._data.userId; + } + + /** + * @return {String?} the avatar mxc url, if any + */ get avatarUrl() { return this._data.avatarUrl; } diff --git a/src/matrix/room/timeline/persistence/GapWriter.js b/src/matrix/room/timeline/persistence/GapWriter.js index 834239f7..e9abded6 100644 --- a/src/matrix/room/timeline/persistence/GapWriter.js +++ b/src/matrix/room/timeline/persistence/GapWriter.js @@ -142,8 +142,9 @@ export class GapWriter { return RoomMember.fromReplacingMemberEvent(this._roomId, event)?.serialize(); } } - // assuming the member hasn't changed within the chunk, just take it from state if it's there - const stateMemberEvent = state.find(isOurUser); + // assuming the member hasn't changed within the chunk, just take it from state if it's there. + // Don't assume state is set though, as it can be empty at the top of the timeline in some circumstances + const stateMemberEvent = state?.find(isOurUser); if (stateMemberEvent) { return RoomMember.fromMemberEvent(this._roomId, stateMemberEvent)?.serialize(); }