You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Move code for processing our own receipts to Room (#4109)
* Move code for processing our own receipts to Room This is some code to process our own receipts and recalculate our notification counts. There was no reason for this to be in client. Room is still rather large, but at least it makes somewhat more sense there. Moving as a refactor before I start work on it. * Add test for the client-side e2e notifications code * simplify object literal
This commit is contained in:
@ -46,6 +46,7 @@ import * as utils from "../test-utils/test-utils";
|
|||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
|
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
|
||||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||||
|
import { IActionsObject } from "../../src/pushprocessor";
|
||||||
import { KnownMembership } from "../../src/@types/membership";
|
import { KnownMembership } from "../../src/@types/membership";
|
||||||
|
|
||||||
describe("MatrixClient syncing", () => {
|
describe("MatrixClient syncing", () => {
|
||||||
@ -1733,64 +1734,122 @@ describe("MatrixClient syncing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
describe("encrypted notification logic", () => {
|
||||||
const roomId = "!room123:server";
|
let roomId: string;
|
||||||
const syncData = {
|
let syncData: ISyncResponse;
|
||||||
rooms: {
|
|
||||||
join: {
|
beforeEach(() => {
|
||||||
[roomId]: {
|
roomId = "!room123:server";
|
||||||
ephemeral: {
|
syncData = {
|
||||||
events: [],
|
rooms: {
|
||||||
},
|
join: {
|
||||||
timeline: {
|
[roomId]: {
|
||||||
events: [
|
ephemeral: {
|
||||||
utils.mkEvent({
|
events: [],
|
||||||
room: roomId,
|
},
|
||||||
event: true,
|
timeline: {
|
||||||
skey: "",
|
events: [
|
||||||
type: EventType.RoomEncryption,
|
utils.mkEvent({
|
||||||
content: {},
|
room: roomId,
|
||||||
}),
|
event: true,
|
||||||
utils.mkMessage({
|
skey: "",
|
||||||
room: roomId,
|
type: EventType.RoomEncryption,
|
||||||
user: otherUserId,
|
content: {},
|
||||||
msg: "hello",
|
}),
|
||||||
}),
|
utils.mkMessage({
|
||||||
],
|
room: roomId,
|
||||||
},
|
user: otherUserId,
|
||||||
state: {
|
msg: "hello",
|
||||||
events: [
|
}),
|
||||||
utils.mkMembership({
|
],
|
||||||
room: roomId,
|
},
|
||||||
mship: KnownMembership.Join,
|
state: {
|
||||||
user: otherUserId,
|
events: [
|
||||||
}),
|
utils.mkMembership({
|
||||||
utils.mkMembership({
|
room: roomId,
|
||||||
room: roomId,
|
mship: KnownMembership.Join,
|
||||||
mship: KnownMembership.Join,
|
user: otherUserId,
|
||||||
user: selfUserId,
|
}),
|
||||||
}),
|
utils.mkMembership({
|
||||||
utils.mkEvent({
|
room: roomId,
|
||||||
type: "m.room.create",
|
mship: KnownMembership.Join,
|
||||||
room: roomId,
|
user: selfUserId,
|
||||||
user: selfUserId,
|
}),
|
||||||
content: {},
|
utils.mkEvent({
|
||||||
}),
|
type: "m.room.create",
|
||||||
],
|
room: roomId,
|
||||||
|
user: selfUserId,
|
||||||
|
content: {},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
} as unknown as ISyncResponse;
|
||||||
} as unknown as ISyncResponse;
|
});
|
||||||
|
|
||||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
||||||
client!.startClient();
|
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||||
|
client!.startClient();
|
||||||
|
|
||||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||||
|
|
||||||
const room = client!.getRoom(roomId)!;
|
const room = client!.getRoom(roomId)!;
|
||||||
expect(room).toBeInstanceOf(Room);
|
expect(room).toBeInstanceOf(Room);
|
||||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should recalculate highlights on receipt for encrypted rooms", async () => {
|
||||||
|
const myUserId = client!.getUserId()!;
|
||||||
|
|
||||||
|
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
|
||||||
|
|
||||||
|
// add a receipt for the first event in the room (let's say the user has already read that one)
|
||||||
|
syncData.rooms.join[roomId].ephemeral.events = [
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
[firstEventId]: {
|
||||||
|
"m.read": {
|
||||||
|
[myUserId]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "m.receipt",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Now add a highlighting event after that receipt
|
||||||
|
const pingEvent = utils.mkMessage({
|
||||||
|
room: roomId,
|
||||||
|
user: otherUserId,
|
||||||
|
msg: client?.getUserId() + " ping",
|
||||||
|
}) as IRoomEvent;
|
||||||
|
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
|
||||||
|
|
||||||
|
// fudge this to make it a highlight
|
||||||
|
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||||
|
if (ev.getId() === pingEvent.event_id) {
|
||||||
|
return {
|
||||||
|
notify: true,
|
||||||
|
tweaks: {
|
||||||
|
highlight: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||||
|
client!.startClient();
|
||||||
|
|
||||||
|
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||||
|
|
||||||
|
const room = client!.getRoom(roomId)!;
|
||||||
|
expect(room).toBeInstanceOf(Room);
|
||||||
|
// the room should now have one highlight since our receipt was before the ping message
|
||||||
|
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1446,55 +1446,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
fixNotificationCountOnDecryption(this, event);
|
fixNotificationCountOnDecryption(this, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Like above, we have to listen for read receipts from ourselves in order to
|
|
||||||
// correctly handle notification counts on encrypted rooms.
|
|
||||||
// This fixes https://github.com/vector-im/element-web/issues/9421
|
|
||||||
this.on(RoomEvent.Receipt, (event, room) => {
|
|
||||||
if (room?.hasEncryptionStateEvent()) {
|
|
||||||
// Figure out if we've read something or if it's just informational
|
|
||||||
const content = event.getContent();
|
|
||||||
const isSelf =
|
|
||||||
Object.keys(content).filter((eid) => {
|
|
||||||
for (const [key, value] of Object.entries(content[eid])) {
|
|
||||||
if (!utils.isSupportedReceiptType(key)) continue;
|
|
||||||
if (!value) continue;
|
|
||||||
|
|
||||||
if (Object.keys(value).includes(this.getUserId()!)) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}).length > 0;
|
|
||||||
|
|
||||||
if (!isSelf) return;
|
|
||||||
|
|
||||||
// Work backwards to determine how many events are unread. We also set
|
|
||||||
// a limit for how back we'll look to avoid spinning CPU for too long.
|
|
||||||
// If we hit the limit, we assume the count is unchanged.
|
|
||||||
const maxHistory = 20;
|
|
||||||
const events = room.getLiveTimeline().getEvents();
|
|
||||||
|
|
||||||
let highlightCount = 0;
|
|
||||||
|
|
||||||
for (let i = events.length - 1; i >= 0; i--) {
|
|
||||||
if (i === events.length - maxHistory) return; // limit reached
|
|
||||||
|
|
||||||
const event = events[i];
|
|
||||||
|
|
||||||
if (room.hasUserReadEvent(this.getUserId()!, event.getId()!)) {
|
|
||||||
// If the user has read the event, then the counting is done.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pushActions = this.getPushActionsForEvent(event);
|
|
||||||
highlightCount += pushActions?.tweaks?.highlight ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: we don't need to handle 'total' notifications because the counts
|
|
||||||
// will come from the server.
|
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ignoredInvites = new IgnoredInvites(this);
|
this.ignoredInvites = new IgnoredInvites(this);
|
||||||
this._secretStorage = new ServerSideSecretStorageImpl(this, opts.cryptoCallbacks ?? {});
|
this._secretStorage = new ServerSideSecretStorageImpl(this, opts.cryptoCallbacks ?? {});
|
||||||
|
|
||||||
|
@ -68,6 +68,7 @@ import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
|
|||||||
import { isPollEvent, Poll, PollEvent } from "./poll";
|
import { isPollEvent, Poll, PollEvent } from "./poll";
|
||||||
import { RoomReceipts } from "./room-receipts";
|
import { RoomReceipts } from "./room-receipts";
|
||||||
import { compareEventOrdering } from "./compare-event-ordering";
|
import { compareEventOrdering } from "./compare-event-ordering";
|
||||||
|
import * as utils from "../utils";
|
||||||
import { KnownMembership, Membership } from "../@types/membership";
|
import { KnownMembership, Membership } from "../@types/membership";
|
||||||
|
|
||||||
// These constants are used as sane defaults when the homeserver doesn't support
|
// These constants are used as sane defaults when the homeserver doesn't support
|
||||||
@ -474,6 +475,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
this.name = roomId;
|
this.name = roomId;
|
||||||
this.normalizedName = roomId;
|
this.normalizedName = roomId;
|
||||||
|
|
||||||
|
// Listen to our own receipt event as a more modular way of processing our own
|
||||||
|
// receipts. No need to remove the listener: it's on ourself anyway.
|
||||||
|
this.on(RoomEvent.Receipt, this.onReceipt);
|
||||||
|
|
||||||
// all our per-room timeline sets. the first one is the unfiltered ones;
|
// all our per-room timeline sets. the first one is the unfiltered ones;
|
||||||
// the subsequent ones are the filtered ones in no particular order.
|
// the subsequent ones are the filtered ones in no particular order.
|
||||||
this.timelineSets = [new EventTimelineSet(this, opts)];
|
this.timelineSets = [new EventTimelineSet(this, opts)];
|
||||||
@ -1306,6 +1311,60 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onReceipt(event: MatrixEvent): void {
|
||||||
|
if (this.hasEncryptionStateEvent()) {
|
||||||
|
this.clearNotificationsOnReceipt(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearNotificationsOnReceipt(event: MatrixEvent): void {
|
||||||
|
// Like above, we have to listen for read receipts from ourselves in order to
|
||||||
|
// correctly handle notification counts on encrypted rooms.
|
||||||
|
// This fixes https://github.com/vector-im/element-web/issues/9421
|
||||||
|
|
||||||
|
// Figure out if we've read something or if it's just informational
|
||||||
|
const content = event.getContent();
|
||||||
|
const isSelf =
|
||||||
|
Object.keys(content).filter((eid) => {
|
||||||
|
for (const [key, value] of Object.entries(content[eid])) {
|
||||||
|
if (!utils.isSupportedReceiptType(key)) continue;
|
||||||
|
if (!value) continue;
|
||||||
|
|
||||||
|
if (Object.keys(value).includes(this.client.getUserId()!)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}).length > 0;
|
||||||
|
|
||||||
|
if (!isSelf) return;
|
||||||
|
|
||||||
|
// Work backwards to determine how many events are unread. We also set
|
||||||
|
// a limit for how back we'll look to avoid spinning CPU for too long.
|
||||||
|
// If we hit the limit, we assume the count is unchanged.
|
||||||
|
const maxHistory = 20;
|
||||||
|
const events = this.getLiveTimeline().getEvents();
|
||||||
|
|
||||||
|
let highlightCount = 0;
|
||||||
|
|
||||||
|
for (let i = events.length - 1; i >= 0; i--) {
|
||||||
|
if (i === events.length - maxHistory) return; // limit reached
|
||||||
|
|
||||||
|
const event = events[i];
|
||||||
|
|
||||||
|
if (this.hasUserReadEvent(this.client.getUserId()!, event.getId()!)) {
|
||||||
|
// If the user has read the event, then the counting is done.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushActions = this.client.getPushActionsForEvent(event);
|
||||||
|
highlightCount += pushActions?.tweaks?.highlight ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we don't need to handle 'total' notifications because the counts
|
||||||
|
// will come from the server.
|
||||||
|
this.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether there are any devices in the room that are unverified
|
* Returns whether there are any devices in the room that are unverified
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user