Skip to content

Prevent phantom notifications from events not in a room's timeline #3942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions spec/integ/matrix-client-unread-notifications.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,69 @@ import {
NotificationCountType,
RelationType,
Room,
fixNotificationCountOnDecryption,
} from "../../src";
import { TestClient } from "../TestClient";
import { ReceiptType } from "../../src/@types/read_receipts";
import { mkThread } from "../test-utils/thread";
import { SyncState } from "../../src/sync";

describe("MatrixClient syncing", () => {
const userA = "@alice:localhost";
const userB = "@bob:localhost";
const userA = "@alice:localhost";
const userB = "@bob:localhost";
const selfUserId = userA;
const selfAccessToken = "aseukfgwef";

const selfUserId = userA;
const selfAccessToken = "aseukfgwef";
function setupTestClient(): [MatrixClient, HttpBackend] {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client, httpBackend];
}

describe("Notification count fixing", () => {
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;

const setupTestClient = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client, httpBackend];
};
beforeEach(() => {
[client] = setupTestClient();
});

it("doesn't increment notification count for events that can't be found in a room", async () => {
const roomId = "!room:localhost";

client!.startClient({ threadSupport: true });
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));

const event = new MatrixEvent({
room_id: roomId,
type: "m.reaction",
event_id: "$foo",
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: "$foo",
key: "x",
},
},
});

jest.spyOn(event, "getPushActions").mockReturnValue({
notify: true,
tweaks: {},
});

fixNotificationCountOnDecryption(client!, event);

expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
});
});

describe("MatrixClient syncing", () => {
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;

beforeEach(() => {
[client, httpBackend] = setupTestClient();
Expand Down
2 changes: 2 additions & 0 deletions spec/unit/notifications.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ describe("fixNotificationCountOnDecryption", () => {
mockClient,
);

room.addLiveEvents([event]);

THREAD_ID = event.getId()!;
threadEvent = mkEvent({
type: EventType.RoomMessage,
Expand Down
13 changes: 13 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9838,6 +9838,19 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
const room = cli.getRoom(event.getRoomId());
if (!room || !ourUserId || !eventId) return;

// Due to threads, we can get relation events (eg. edits & reactions) that never get
// added to a timeline and so cannot be found in their own room (their edit / reaction
// still applies to the event it needs to, so it doesn't matter too much). However, if
// we try to process notification about this event, we'll get very confused because we
// won't be able to find the event in the room, so will assume it must be unread, even
// if it's actually read. We therefore skip anything that isn't in the room. This isn't
// *great*, so if we can fix the homeless events (eg. with MSC4023) then we should probably
// remove this workaround.
if (!room.findEventById(eventId)) {
logger.info("Decrypted event is not in the room: ignoring");
return;
}

const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;

let hasReadEvent;
Expand Down