Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Read receipts for threads proof of concept (MSC3771) #9130

Closed
wants to merge 7 commits into from
Closed
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
11 changes: 6 additions & 5 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -821,13 +821,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] {
const myUserId = MatrixClientPeg.get().credentials.userId;

// get list of read receipts, sorted most recent first
const { room } = this.props;
if (!room) {
return null;
}

const receiptDestination = this.context.threadId
? room.getThread(this.context.threadId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

room is still optional in props, isn't this going to cause issues here?

: room;

const receipts: IReadReceiptProps[] = [];
room.getReceiptsForEvent(event).forEach((r) => {
receiptDestination.getReceiptsForEvent(event).forEach((r) => {
if (
!r.userId ||
!isSupportedReceiptType(r.type) ||
Expand Down
8 changes: 4 additions & 4 deletions src/components/structures/ThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,10 @@ const ThreadPanel: React.FC<IProps> = ({
? <TimelinePanel
key={timelineSet.getFilter()?.filterId ?? (roomId + ":" + filterOption)}
ref={timelinePanel}
showReadReceipts={false} // No RR support in thread's MVP
manageReadReceipts={false} // No RR support in thread's MVP
manageReadMarkers={false} // No RM support in thread's MVP
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
showReadReceipts={false} // No RR on the thread's list
manageReadReceipts={false} // No RR on the thread's list
manageReadMarkers={false} // No RM on the thread's list
sendReadReceiptOnLoad={false} // No RR on the thread's list
timelineSet={timelineSet}
showUrlPreview={false} // No URL previews at the threads list level
empty={<EmptyThread
Expand Down
5 changes: 2 additions & 3 deletions src/components/structures/ThreadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
<TimelinePanel
key={this.state.thread.id}
ref={this.timelinePanel}
showReadReceipts={false} // Hide the read receipts
// until homeservers speak threads language
showReadReceipts={true}
manageReadReceipts={true}
manageReadMarkers={true}
sendReadReceiptOnLoad={true}
sendReadReceiptOnLoad={false}
timelineSet={this.state.thread.timelineSet}
showUrlPreview={this.context.showUrlPreview}
// ThreadView doesn't support IRC layout at this time
Expand Down
67 changes: 39 additions & 28 deletions src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// the current RR event.
lastReadEventIndex > currentRREventIndex &&
// Only send a RR if the last RR set != the one we would send
this.lastRRSentEventId != lastReadEvent.getId();
this.lastRRSentEventId !== lastReadEvent.getId();

// Only send a RM if the last RM sent != the one we would send
const shouldSendRM =
Expand All @@ -975,33 +975,42 @@ class TimelinePanel extends React.Component<IProps, IState> {
`prr=${lastReadEvent?.getId()}`,

);
MatrixClientPeg.get().setRoomReadMarkers(
roomId,
this.state.readMarkerEventId,
sendRRs ? lastReadEvent : null, // Public read receipt (could be null)
lastReadEvent, // Private read receipt (could be null)
).catch(async (e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
const privateField = await getPrivateReadReceiptField(MatrixClientPeg.get());
if (!sendRRs && !privateField) return;

try {
return await MatrixClientPeg.get().sendReadReceipt(
lastReadEvent,
sendRRs ? ReceiptType.Read : privateField,
);
} catch (error) {

// Not a final solution, but here we go!
if (this.props.timelineSet.thread && sendRRs) {
cli.sendReadReceipt(
lastReadEvent,
sendRRs ? ReceiptType.Read : ReceiptType.ReadPrivate,
);
} else {
cli.setRoomReadMarkers(
roomId,
this.state.readMarkerEventId,
sendRRs ? lastReadEvent : null, // Public read receipt (could be null)
lastReadEvent, // Private read receipt (could be null)
).catch(async (e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
const privateField = await getPrivateReadReceiptField(cli);
if (!sendRRs && !privateField) return;

try {
return await cli.sendReadReceipt(
lastReadEvent,
sendRRs ? ReceiptType.Read : privateField,
);
} catch (error) {
logger.error(e);
this.lastRRSentEventId = undefined;
}
} else {
logger.error(e);
this.lastRRSentEventId = undefined;
}
} else {
logger.error(e);
}
// it failed, so allow retries next time the user is active
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
});
// it failed, so allow retries next time the user is active
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
});
}

// do a quick-reset of our unreadNotificationCount to avoid having
// to wait from the remote echo from the homeserver.
Expand Down Expand Up @@ -1654,15 +1663,17 @@ class TimelinePanel extends React.Component<IProps, IState> {
* SDK.
* @return {String} the event ID
*/
private getCurrentReadReceipt(ignoreSynthesized = false): string {
private getCurrentReadReceipt(ignoreSynthesized = false): string | null {
const client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null) {
return null;
}

const myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);

const receipt = this.props.timelineSet.thread ?? this.props.timelineSet.room;
return receipt.getEventReadUpTo(myUserId, ignoreSynthesized);
}

private setReadMarker(eventId: string, eventTs: number, inhibitSetState = false): void {
Expand Down
1 change: 1 addition & 0 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ msgOption }
</div>,
reactionsRow,
]);
Expand Down
20 changes: 16 additions & 4 deletions src/components/views/settings/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import React from "react";
import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules";
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
import { logger } from "matrix-js-sdk/src/logger";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Thread } from "matrix-js-sdk/src/models/thread";

import Spinner from "../elements/Spinner";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
Expand Down Expand Up @@ -398,13 +401,22 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
};

private onClearNotificationsClicked = () => {
MatrixClientPeg.get().getRooms().forEach(r => {
if (r.getUnreadNotificationCount() > 0) {
const events = r.getLiveTimeline().getEvents();
const client: MatrixClient = MatrixClientPeg.get();
client.getRooms().forEach((room: Room) => {
if (room.getUnreadNotificationCount() > 0) {
const events = room.getLiveTimeline().getEvents();
if (events.length) {
// noinspection JSIgnoredPromiseFromCall
MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]);
client.sendReadReceipt(events[events.length - 1]);
}

room.getThreads().forEach((thread: Thread) => {
// Need to optimise this and send a read receipt only
// for threads that have unread content
// This will be done when integrating with MSC3773
const events = thread.liveTimeline.getEvents();
client.sendReadReceipt(events[events.length - 1]);
});
}
});
};
Expand Down