Fix rooms with no messages appearing at the top of the room list (#31798)

* Extract and move timestamp function

* Use new function in BaseRecencySorter

* Remove deprecated method usage

* Add jsdoc

* Avoid unnecessary exports

* Fix tests
This commit is contained in:
R Midhun Suresh
2026-01-27 21:38:35 +05:30
committed by GitHub
parent 617722018c
commit dda87ff1ed
6 changed files with 275 additions and 6 deletions

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Sorter, SortingAlgorithm } from ".";
import { getLastTs } from "../../../room-list/algorithms/tag-sorting/RecentAlgorithm";
import { getLastTimestamp } from "./utils/getLastTimestamp";
export abstract class BaseRecencySorter implements Sorter {
public constructor(protected myUserId: string) {}
@@ -29,7 +29,7 @@ export abstract class BaseRecencySorter implements Sorter {
}
private getTs(room: Room, cache?: { [roomId: string]: number }): number {
const ts = cache?.[room.roomId] ?? getLastTs(room, this.myUserId);
const ts = cache?.[room.roomId] ?? getLastTimestamp(room, this.myUserId);
if (cache) {
cache[room.roomId] = ts;
}

View File

@@ -0,0 +1,84 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { EventTimeline, EventType, type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
import { EffectiveMembership, getEffectiveMembership } from "../../../../../utils/membership";
import * as Unread from "../../../../../Unread";
function shouldCauseReorder(event: MatrixEvent): boolean {
const type = event.getType();
const content = event.getContent();
const prevContent = event.getPrevContent();
// Never ignore membership changes
if (type === EventType.RoomMember && prevContent.membership !== content.membership) return true;
// Ignore display name changes
if (type === EventType.RoomMember && prevContent.displayname !== content.displayname) return false;
// Ignore avatar changes
if (type === EventType.RoomMember && prevContent.avatar_url !== content.avatar_url) return false;
return true;
}
/**
* For a given room, this function returns a timestamp that can be used for recency sorting.
* @param r room for which the timestamp is calculated
* @param userId mxId of the current user
* @returns timestamp
*/
export const getLastTimestamp = (r: Room, userId: string): number => {
const mainTimelineLastTs = ((): number => {
const timeline = r.getLiveTimeline().getEvents();
// MSC4186: Simplified Sliding Sync sets this.
// If it's present, sort by it.
const bumpStamp = r.getBumpStamp();
if (bumpStamp) {
return bumpStamp;
}
// If the room hasn't been joined yet, it probably won't have a timeline to
// parse. We'll still fall back to the timeline if this fails, but chances
// are we'll at least have our own membership event to go off of.
const effectiveMembership = getEffectiveMembership(r.getMyMembership());
if (effectiveMembership !== EffectiveMembership.Join) {
const membershipEvent = r
.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.getStateEvents(EventType.RoomMember, userId);
if (membershipEvent && !Array.isArray(membershipEvent)) {
return membershipEvent.getTs();
}
}
for (let i = timeline.length - 1; i >= 0; --i) {
const ev = timeline[i];
if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
if (
(ev.getSender() === userId && shouldCauseReorder(ev)) ||
Unread.eventTriggersUnreadCount(r.client, ev)
) {
return ev.getTs();
}
}
// we might only have events that don't trigger the unread indicator,
// in which case use the oldest event even if normally it wouldn't count.
// This is better than just assuming the last event was forever ago.
return timeline[0]?.getTs() ?? 0;
})();
const threadLastEventTimestamps = r.getThreads().map((thread) => {
const event = thread.replyToEvent ?? thread.rootEvent;
return event?.getTs() ?? 0;
});
return Math.max(mainTimelineLastTs, ...threadLastEventTimestamps);
};