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 { ReceiptType } from '../../src/@types/read_receipts';
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptType } from '../../src/@types/read_receipts';
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Feature, ServerSupport } from '../../src/feature';
|
||||
import { EventType } from '../../src/matrix';
|
||||
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
|
||||
import { encodeUri } from '../../src/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 { TestClient } from "../TestClient";
|
||||
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 { WrappedReceipt } from "../../src/models/read-receipt";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
|
||||
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() {
|
||||
function createSyncResponseWithSummary(summary) {
|
||||
return {
|
||||
|
@@ -19,3 +19,38 @@ export enum ReceiptType {
|
||||
FullyRead = "m.fully_read",
|
||||
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 { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||
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 { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||
import {
|
||||
@@ -210,7 +210,6 @@ import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
|
||||
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||
import { UIARequest, UIAResponse } from "./@types/uia";
|
||||
import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
|
@@ -11,15 +11,21 @@ See the License for the specific language governing permissions and
|
||||
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 * as utils from "../utils";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventType } from "../@types/event";
|
||||
import { EventTimelineSet } from "./event-timeline-set";
|
||||
|
||||
export const MAIN_ROOM_TIMELINE = "main";
|
||||
|
||||
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
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 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<
|
||||
Events extends string,
|
||||
|
@@ -54,14 +54,11 @@ import {
|
||||
FILTER_RELATED_BY_SENDERS,
|
||||
ThreadFilterType,
|
||||
} 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 { RelationsContainer } from "./relations-container";
|
||||
import {
|
||||
MAIN_ROOM_TIMELINE,
|
||||
ReadReceipt,
|
||||
Receipt,
|
||||
ReceiptContent,
|
||||
synthesizeReceipt,
|
||||
} from "./read-receipt";
|
||||
import { Feature, ServerSupport } from "../feature";
|
||||
|
@@ -24,7 +24,7 @@ import { deepCopy, isSupportedReceiptType } from "./utils";
|
||||
import { IContent, IUnsigned } from "./models/event";
|
||||
import { IRoomSummary } from "./models/room-summary";
|
||||
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';
|
||||
|
||||
interface IOpts {
|
||||
@@ -165,6 +165,15 @@ interface IRoom {
|
||||
eventId: string;
|
||||
};
|
||||
};
|
||||
_threadReadReceipts: {
|
||||
[threadId: string]: {
|
||||
[userId: string]: {
|
||||
data: IMinimalEvent;
|
||||
type: ReceiptType;
|
||||
eventId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISyncData {
|
||||
@@ -369,6 +378,7 @@ export class SyncAccumulator {
|
||||
_unreadThreadNotifications: {},
|
||||
_summary: {},
|
||||
_readReceipts: {},
|
||||
_threadReadReceipts: {},
|
||||
};
|
||||
}
|
||||
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
|
||||
// getJSON() is called.
|
||||
Object.keys(e.content).forEach((eventId) => {
|
||||
Object.entries<{
|
||||
[eventId: string]: {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: IMinimalEvent;
|
||||
};
|
||||
};
|
||||
}>(e.content[eventId]).forEach(([key, value]) => {
|
||||
Object.entries<ReceiptContent>(e.content[eventId]).forEach(([key, value]) => {
|
||||
if (!isSupportedReceiptType(key)) return;
|
||||
|
||||
Object.keys(value).forEach((userId) => {
|
||||
// clobber on user ID
|
||||
currentData._readReceipts[userId] = {
|
||||
for (const userId of Object.keys(value)) {
|
||||
const data = e.content[eventId][key][userId];
|
||||
|
||||
const receipt = {
|
||||
data: e.content[eventId][key][userId],
|
||||
type: key as ReceiptType,
|
||||
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 } }
|
||||
},
|
||||
};
|
||||
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]) {
|
||||
receiptEvent.content[receiptData.eventId] = {};
|
||||
}
|
||||
@@ -577,7 +594,21 @@ export class SyncAccumulator {
|
||||
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = (
|
||||
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
|
||||
if (Object.keys(receiptEvent.content).length > 0) {
|
||||
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
|
||||
|
Reference in New Issue
Block a user