You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Read receipt accumulation for threads (#2881)
This commit is contained in:
@@ -16,11 +16,10 @@ limitations under the License.
|
|||||||
|
|
||||||
import MockHttpBackend from 'matrix-mock-request';
|
import MockHttpBackend from 'matrix-mock-request';
|
||||||
|
|
||||||
import { ReceiptType } from '../../src/@types/read_receipts';
|
import { MAIN_ROOM_TIMELINE, ReceiptType } from '../../src/@types/read_receipts';
|
||||||
import { MatrixClient } from "../../src/client";
|
import { MatrixClient } from "../../src/client";
|
||||||
import { Feature, ServerSupport } from '../../src/feature';
|
import { Feature, ServerSupport } from '../../src/feature';
|
||||||
import { EventType } from '../../src/matrix';
|
import { EventType } from '../../src/matrix';
|
||||||
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
|
|
||||||
import { encodeUri } from '../../src/utils';
|
import { encodeUri } from '../../src/utils';
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
|
|
||||||
|
@@ -38,9 +38,8 @@ import { RoomState } from "../../src/models/room-state";
|
|||||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { emitPromise } from "../test-utils/test-utils";
|
import { emitPromise } from "../test-utils/test-utils";
|
||||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
|
||||||
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||||
import { WrappedReceipt } from "../../src/models/read-receipt";
|
|
||||||
import { Crypto } from "../../src/crypto";
|
import { Crypto } from "../../src/crypto";
|
||||||
|
|
||||||
describe("Room", function() {
|
describe("Room", function() {
|
||||||
|
@@ -364,6 +364,63 @@ describe("SyncAccumulator", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should accumulate threaded read receipts", () => {
|
||||||
|
const receipt1 = {
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
"$event1:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
"@alice:localhost": { ts: 1, thread_id: "main" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const receipt2 = {
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
"$event2:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
"@alice:localhost": { ts: 2, thread_id: "$123" }, // does not clobbers event1 receipt
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
sa.accumulate(syncSkeleton({
|
||||||
|
ephemeral: {
|
||||||
|
events: [receipt1],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
sa.accumulate(syncSkeleton({
|
||||||
|
ephemeral: {
|
||||||
|
events: [receipt2],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
|
||||||
|
).toEqual(1);
|
||||||
|
expect(
|
||||||
|
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events[0],
|
||||||
|
).toEqual({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
"$event1:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
"@alice:localhost": { ts: 1, thread_id: "main" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"$event2:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
"@alice:localhost": { ts: 2, thread_id: "$123" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("summary field", function() {
|
describe("summary field", function() {
|
||||||
function createSyncResponseWithSummary(summary) {
|
function createSyncResponseWithSummary(summary) {
|
||||||
return {
|
return {
|
||||||
|
@@ -19,3 +19,38 @@ export enum ReceiptType {
|
|||||||
FullyRead = "m.fully_read",
|
FullyRead = "m.fully_read",
|
||||||
ReadPrivate = "m.read.private",
|
ReadPrivate = "m.read.private",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAIN_ROOM_TIMELINE = "main";
|
||||||
|
|
||||||
|
export interface Receipt {
|
||||||
|
ts: number;
|
||||||
|
thread_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WrappedReceipt {
|
||||||
|
eventId: string;
|
||||||
|
data: Receipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CachedReceipt {
|
||||||
|
type: ReceiptType;
|
||||||
|
userId: string;
|
||||||
|
data: Receipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReceiptCache = {[eventId: string]: CachedReceipt[]};
|
||||||
|
|
||||||
|
export interface ReceiptContent {
|
||||||
|
[eventId: string]: {
|
||||||
|
[key in ReceiptType]: {
|
||||||
|
[userId: string]: Receipt;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
||||||
|
export type Receipts = {
|
||||||
|
[receiptType: string]: {
|
||||||
|
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -195,7 +195,7 @@ import { MediaHandler } from "./webrtc/mediaHandler";
|
|||||||
import { GroupCallEventHandler } from "./webrtc/groupCallEventHandler";
|
import { GroupCallEventHandler } from "./webrtc/groupCallEventHandler";
|
||||||
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||||
import { ReceiptType } from "./@types/read_receipts";
|
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
|
||||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||||
import {
|
import {
|
||||||
@@ -210,7 +210,6 @@ import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
|||||||
import { UnstableValue } from "./NamespacedValue";
|
import { UnstableValue } from "./NamespacedValue";
|
||||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||||
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
|
|
||||||
import { IgnoredInvites } from "./models/invites-ignorer";
|
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||||
import { UIARequest, UIAResponse } from "./@types/uia";
|
import { UIARequest, UIAResponse } from "./@types/uia";
|
||||||
import { LocalNotificationSettings } from "./@types/local_notifications";
|
import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||||
|
@@ -11,15 +11,21 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReceiptType } from "../@types/read_receipts";
|
import {
|
||||||
|
CachedReceipt,
|
||||||
|
MAIN_ROOM_TIMELINE,
|
||||||
|
Receipt,
|
||||||
|
ReceiptCache,
|
||||||
|
Receipts,
|
||||||
|
ReceiptType,
|
||||||
|
WrappedReceipt,
|
||||||
|
} from "../@types/read_receipts";
|
||||||
import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
|
import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { MatrixEvent } from "./event";
|
import { MatrixEvent } from "./event";
|
||||||
import { EventType } from "../@types/event";
|
import { EventType } from "../@types/event";
|
||||||
import { EventTimelineSet } from "./event-timeline-set";
|
import { EventTimelineSet } from "./event-timeline-set";
|
||||||
|
|
||||||
export const MAIN_ROOM_TIMELINE = "main";
|
|
||||||
|
|
||||||
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
||||||
return new MatrixEvent({
|
return new MatrixEvent({
|
||||||
content: {
|
content: {
|
||||||
@@ -37,40 +43,8 @@ export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptTyp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Receipt {
|
|
||||||
ts: number;
|
|
||||||
thread_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WrappedReceipt {
|
|
||||||
eventId: string;
|
|
||||||
data: Receipt;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CachedReceipt {
|
|
||||||
type: ReceiptType;
|
|
||||||
userId: string;
|
|
||||||
data: Receipt;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReceiptCache = {[eventId: string]: CachedReceipt[]};
|
|
||||||
|
|
||||||
export interface ReceiptContent {
|
|
||||||
[eventId: string]: {
|
|
||||||
[key in ReceiptType]: {
|
|
||||||
[userId: string]: Receipt;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReceiptPairRealIndex = 0;
|
const ReceiptPairRealIndex = 0;
|
||||||
const ReceiptPairSyntheticIndex = 1;
|
const ReceiptPairSyntheticIndex = 1;
|
||||||
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
|
||||||
type Receipts = {
|
|
||||||
[receiptType: string]: {
|
|
||||||
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export abstract class ReadReceipt<
|
export abstract class ReadReceipt<
|
||||||
Events extends string,
|
Events extends string,
|
||||||
|
@@ -54,14 +54,11 @@ import {
|
|||||||
FILTER_RELATED_BY_SENDERS,
|
FILTER_RELATED_BY_SENDERS,
|
||||||
ThreadFilterType,
|
ThreadFilterType,
|
||||||
} from "./thread";
|
} from "./thread";
|
||||||
import { ReceiptType } from "../@types/read_receipts";
|
import { MAIN_ROOM_TIMELINE, Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts";
|
||||||
import { IStateEventWithRoomId } from "../@types/search";
|
import { IStateEventWithRoomId } from "../@types/search";
|
||||||
import { RelationsContainer } from "./relations-container";
|
import { RelationsContainer } from "./relations-container";
|
||||||
import {
|
import {
|
||||||
MAIN_ROOM_TIMELINE,
|
|
||||||
ReadReceipt,
|
ReadReceipt,
|
||||||
Receipt,
|
|
||||||
ReceiptContent,
|
|
||||||
synthesizeReceipt,
|
synthesizeReceipt,
|
||||||
} from "./read-receipt";
|
} from "./read-receipt";
|
||||||
import { Feature, ServerSupport } from "../feature";
|
import { Feature, ServerSupport } from "../feature";
|
||||||
|
@@ -24,7 +24,7 @@ import { deepCopy, isSupportedReceiptType } from "./utils";
|
|||||||
import { IContent, IUnsigned } from "./models/event";
|
import { IContent, IUnsigned } from "./models/event";
|
||||||
import { IRoomSummary } from "./models/room-summary";
|
import { IRoomSummary } from "./models/room-summary";
|
||||||
import { EventType } from "./@types/event";
|
import { EventType } from "./@types/event";
|
||||||
import { ReceiptType } from "./@types/read_receipts";
|
import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_receipts";
|
||||||
import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync';
|
import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync';
|
||||||
|
|
||||||
interface IOpts {
|
interface IOpts {
|
||||||
@@ -165,6 +165,15 @@ interface IRoom {
|
|||||||
eventId: string;
|
eventId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
_threadReadReceipts: {
|
||||||
|
[threadId: string]: {
|
||||||
|
[userId: string]: {
|
||||||
|
data: IMinimalEvent;
|
||||||
|
type: ReceiptType;
|
||||||
|
eventId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISyncData {
|
export interface ISyncData {
|
||||||
@@ -369,6 +378,7 @@ export class SyncAccumulator {
|
|||||||
_unreadThreadNotifications: {},
|
_unreadThreadNotifications: {},
|
||||||
_summary: {},
|
_summary: {},
|
||||||
_readReceipts: {},
|
_readReceipts: {},
|
||||||
|
_threadReadReceipts: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const currentData = this.joinRooms[roomId];
|
const currentData = this.joinRooms[roomId];
|
||||||
@@ -425,23 +435,30 @@ export class SyncAccumulator {
|
|||||||
// of a hassle to work with. We'll inflate this back out when
|
// of a hassle to work with. We'll inflate this back out when
|
||||||
// getJSON() is called.
|
// getJSON() is called.
|
||||||
Object.keys(e.content).forEach((eventId) => {
|
Object.keys(e.content).forEach((eventId) => {
|
||||||
Object.entries<{
|
Object.entries<ReceiptContent>(e.content[eventId]).forEach(([key, value]) => {
|
||||||
[eventId: string]: {
|
|
||||||
[receiptType: string]: {
|
|
||||||
[userId: string]: IMinimalEvent;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>(e.content[eventId]).forEach(([key, value]) => {
|
|
||||||
if (!isSupportedReceiptType(key)) return;
|
if (!isSupportedReceiptType(key)) return;
|
||||||
|
|
||||||
Object.keys(value).forEach((userId) => {
|
for (const userId of Object.keys(value)) {
|
||||||
// clobber on user ID
|
const data = e.content[eventId][key][userId];
|
||||||
currentData._readReceipts[userId] = {
|
|
||||||
|
const receipt = {
|
||||||
data: e.content[eventId][key][userId],
|
data: e.content[eventId][key][userId],
|
||||||
type: key as ReceiptType,
|
type: key as ReceiptType,
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
if (!data.thread_id || data.thread_id === MAIN_ROOM_TIMELINE) {
|
||||||
|
currentData._readReceipts[userId] = receipt;
|
||||||
|
} else {
|
||||||
|
currentData._threadReadReceipts = {
|
||||||
|
...currentData._threadReadReceipts,
|
||||||
|
[data.thread_id]: {
|
||||||
|
...(currentData._threadReadReceipts[data.thread_id] ?? {}),
|
||||||
|
[userId]: receipt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -566,8 +583,8 @@ export class SyncAccumulator {
|
|||||||
// $event_id: { "m.read": { $user_id: $json } }
|
// $event_id: { "m.read": { $user_id: $json } }
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Object.keys(roomData._readReceipts).forEach((userId) => {
|
|
||||||
const receiptData = roomData._readReceipts[userId];
|
for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) {
|
||||||
if (!receiptEvent.content[receiptData.eventId]) {
|
if (!receiptEvent.content[receiptData.eventId]) {
|
||||||
receiptEvent.content[receiptData.eventId] = {};
|
receiptEvent.content[receiptData.eventId] = {};
|
||||||
}
|
}
|
||||||
@@ -577,7 +594,21 @@ export class SyncAccumulator {
|
|||||||
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = (
|
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = (
|
||||||
receiptData.data
|
receiptData.data
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
for (const threadReceipts of Object.values(roomData._threadReadReceipts)) {
|
||||||
|
for (const [userId, receiptData] of Object.entries(threadReceipts)) {
|
||||||
|
if (!receiptEvent.content[receiptData.eventId]) {
|
||||||
|
receiptEvent.content[receiptData.eventId] = {};
|
||||||
|
}
|
||||||
|
if (!receiptEvent.content[receiptData.eventId][receiptData.type]) {
|
||||||
|
receiptEvent.content[receiptData.eventId][receiptData.type] = {};
|
||||||
|
}
|
||||||
|
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = (
|
||||||
|
receiptData.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// add only if we have some receipt data
|
// add only if we have some receipt data
|
||||||
if (Object.keys(receiptEvent.content).length > 0) {
|
if (Object.keys(receiptEvent.content).length > 0) {
|
||||||
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
|
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
|
||||||
|
Reference in New Issue
Block a user