Read receipts for threads (#9239)
* Use EventType enum instead of hardcoded value * Enable read receipts on thread timelines * Strict null checks * Strict null checks * fix import group * strict checks * strict checks * null check * fix testspull/28217/head
parent
fa2ec7f6c9
commit
71cf9bf932
|
@ -25,6 +25,8 @@ import { logger } from 'matrix-js-sdk/src/logger';
|
|||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
|
||||
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
|
||||
import { ReadReceipt } from 'matrix-js-sdk/src/models/read-receipt';
|
||||
import { ListenerMap } from 'matrix-js-sdk/src/models/typed-event-emitter';
|
||||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import { wantsDateSeparator } from '../../DateUtils';
|
||||
|
@ -135,7 +137,7 @@ interface IProps {
|
|||
showUrlPreview?: boolean;
|
||||
|
||||
// event after which we should show a read marker
|
||||
readMarkerEventId?: string;
|
||||
readMarkerEventId?: string | null;
|
||||
|
||||
// whether the read marker should be visible
|
||||
readMarkerVisible?: boolean;
|
||||
|
@ -826,8 +828,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const receiptDestination: ReadReceipt<string, ListenerMap<string>> = this.context.threadId
|
||||
? room.getThread(this.context.threadId)
|
||||
: room;
|
||||
|
||||
const receipts: IReadReceiptProps[] = [];
|
||||
room.getReceiptsForEvent(event).forEach((r) => {
|
||||
receiptDestination.getReceiptsForEvent(event).forEach((r) => {
|
||||
if (
|
||||
!r.userId ||
|
||||
!isSupportedReceiptType(r.type) ||
|
||||
|
|
|
@ -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 support in thread's list
|
||||
manageReadReceipts={false} // No RR support in thread's list
|
||||
manageReadMarkers={false} // No RM support in thread's list
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's list
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview={false} // No URL previews at the threads list level
|
||||
empty={<EmptyThread
|
||||
|
|
|
@ -329,8 +329,7 @@ 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}
|
||||
|
|
|
@ -30,6 +30,7 @@ import { ClientEvent } from "matrix-js-sdk/src/client";
|
|||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
||||
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||
import { MatrixError } from 'matrix-js-sdk/src/http-api';
|
||||
import { ReadReceipt } from 'matrix-js-sdk/src/models/read-receipt';
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
|
@ -179,7 +180,7 @@ interface IState {
|
|||
// disappearance when switching into the room.
|
||||
readMarkerVisible: boolean;
|
||||
|
||||
readMarkerEventId: string;
|
||||
readMarkerEventId: string | null;
|
||||
|
||||
backPaginating: boolean;
|
||||
forwardPaginating: boolean;
|
||||
|
@ -229,8 +230,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
disableGrouping: false,
|
||||
};
|
||||
|
||||
private lastRRSentEventId: string = undefined;
|
||||
private lastRMSentEventId: string = undefined;
|
||||
private lastRRSentEventId: string | null | undefined = undefined;
|
||||
private lastRMSentEventId: string | null | undefined = undefined;
|
||||
|
||||
private readonly messagePanel = createRef<MessagePanel>();
|
||||
private readonly dispatcherRef: string;
|
||||
|
@ -250,7 +251,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
|
||||
// XXX: we could track RM per TimelineSet rather than per Room.
|
||||
// but for now we just do it per room for simplicity.
|
||||
let initialReadMarker = null;
|
||||
let initialReadMarker: string | null = null;
|
||||
if (this.props.manageReadMarkers) {
|
||||
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
|
||||
if (readmarker) {
|
||||
|
@ -942,13 +943,13 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
if (lastReadEventIndex === null) {
|
||||
shouldSendRR = false;
|
||||
}
|
||||
let lastReadEvent = this.state.events[lastReadEventIndex];
|
||||
let lastReadEvent: MatrixEvent | null = this.state.events[lastReadEventIndex ?? 0];
|
||||
shouldSendRR = shouldSendRR &&
|
||||
// Only send a RR if the last read event is ahead in the timeline relative to
|
||||
// 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 =
|
||||
|
@ -958,7 +959,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
// same one at the server repeatedly
|
||||
if (shouldSendRR || shouldSendRM) {
|
||||
if (shouldSendRR) {
|
||||
this.lastRRSentEventId = lastReadEvent.getId();
|
||||
this.lastRRSentEventId = lastReadEvent?.getId();
|
||||
} else {
|
||||
lastReadEvent = null;
|
||||
}
|
||||
|
@ -974,48 +975,57 @@ 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) {
|
||||
if (
|
||||
!sendRRs
|
||||
&& !MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285.stable")
|
||||
) return;
|
||||
|
||||
try {
|
||||
return await MatrixClientPeg.get().sendReadReceipt(
|
||||
lastReadEvent,
|
||||
sendRRs ? ReceiptType.Read : ReceiptType.ReadPrivate,
|
||||
);
|
||||
} catch (error) {
|
||||
if (this.props.timelineSet.thread && sendRRs && lastReadEvent) {
|
||||
// There's no support for fully read markers on threads
|
||||
// as defined by MSC3771
|
||||
cli.sendReadReceipt(
|
||||
lastReadEvent,
|
||||
sendRRs ? ReceiptType.Read : ReceiptType.ReadPrivate,
|
||||
);
|
||||
} else {
|
||||
cli.setRoomReadMarkers(
|
||||
roomId,
|
||||
this.state.readMarkerEventId ?? "",
|
||||
sendRRs ? (lastReadEvent ?? undefined) : undefined, // Public read receipt (could be null)
|
||||
lastReadEvent ?? undefined, // 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) {
|
||||
if (
|
||||
!sendRRs
|
||||
&& !cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")
|
||||
) return;
|
||||
try {
|
||||
return await cli.sendReadReceipt(
|
||||
lastReadEvent,
|
||||
sendRRs ? ReceiptType.Read : ReceiptType.ReadPrivate,
|
||||
);
|
||||
} 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;
|
||||
});
|
||||
|
||||
// do a quick-reset of our unreadNotificationCount to avoid having
|
||||
// to wait from the remote echo from the homeserver.
|
||||
// we only do this if we're right at the end, because we're just assuming
|
||||
// that sending an RR for the latest message will set our notif counter
|
||||
// to zero: it may not do this if we send an RR for somewhere before the end.
|
||||
if (this.isAtEndOfLiveTimeline()) {
|
||||
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
||||
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
dis.dispatch({
|
||||
action: 'on_room_read',
|
||||
roomId: this.props.timelineSet.room.roomId,
|
||||
// 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.
|
||||
// we only do this if we're right at the end, because we're just assuming
|
||||
// that sending an RR for the latest message will set our notif counter
|
||||
// to zero: it may not do this if we send an RR for somewhere before the end.
|
||||
if (this.isAtEndOfLiveTimeline()) {
|
||||
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
||||
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
dis.dispatch({
|
||||
action: 'on_room_read',
|
||||
roomId: this.props.timelineSet.room.roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1149,7 +1159,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
const rmId = this.getCurrentReadReceipt();
|
||||
|
||||
// Look up the timestamp if we can find it
|
||||
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
||||
const tl = this.props.timelineSet.getTimelineForEvent(rmId ?? "");
|
||||
let rmTs: number;
|
||||
if (tl) {
|
||||
const event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
||||
|
@ -1554,7 +1564,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private indexForEventId(evId: string): number | null {
|
||||
private indexForEventId(evId: string | null): number | null {
|
||||
if (evId === null) { return null; }
|
||||
/* Threads do not have server side support for read receipts and the concept
|
||||
is very tied to the main room timeline, we are forcing the timeline to
|
||||
send read receipts for threaded events */
|
||||
|
@ -1655,7 +1666,7 @@ 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) {
|
||||
|
@ -1663,21 +1674,23 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const myUserId = client.credentials.userId;
|
||||
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||
const receiptStore: ReadReceipt<any, any> =
|
||||
this.props.timelineSet.thread ?? this.props.timelineSet.room;
|
||||
return receiptStore?.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||
}
|
||||
|
||||
private setReadMarker(eventId: string, eventTs: number, inhibitSetState = false): void {
|
||||
const roomId = this.props.timelineSet.room.roomId;
|
||||
private setReadMarker(eventId: string | null, eventTs: number, inhibitSetState = false): void {
|
||||
const roomId = this.props.timelineSet.room?.roomId;
|
||||
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
if (eventId === this.state.readMarkerEventId) {
|
||||
if (eventId === this.state.readMarkerEventId || eventId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// in order to later figure out if the read marker is
|
||||
// above or below the visible timeline, we stash the timestamp.
|
||||
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
||||
TimelinePanel.roomReadMarkerTsMap[roomId ?? ""] = eventTs;
|
||||
|
||||
if (inhibitSetState) {
|
||||
return;
|
||||
|
|
|
@ -1333,6 +1333,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ msgOption }
|
||||
</div>,
|
||||
reactionsRow,
|
||||
]);
|
||||
|
|
|
@ -398,12 +398,13 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
private onClearNotificationsClicked = () => {
|
||||
MatrixClientPeg.get().getRooms().forEach(r => {
|
||||
const client = MatrixClientPeg.get();
|
||||
client.getRooms().forEach(r => {
|
||||
if (r.getUnreadNotificationCount() > 0) {
|
||||
const events = r.getLiveTimeline().getEvents();
|
||||
if (events.length) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]);
|
||||
client.sendReadReceipt(events[events.length - 1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -42,7 +42,7 @@ const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs
|
|||
[ReceiptType.FullyRead]: { [userId]: { ts: fullyReadTs } },
|
||||
},
|
||||
};
|
||||
return new MatrixEvent({ content: receiptContent, type: "m.receipt" });
|
||||
return new MatrixEvent({ content: receiptContent, type: EventType.Receipt });
|
||||
};
|
||||
|
||||
const renderPanel = (room: Room, events: MatrixEvent[]): RenderResult => {
|
||||
|
@ -154,7 +154,7 @@ describe('TimelinePanel', () => {
|
|||
});
|
||||
|
||||
renderPanel(room, events);
|
||||
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(room.roomId, null, events[0], events[0]);
|
||||
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(room.roomId, "", events[0], events[0]);
|
||||
});
|
||||
|
||||
it("does not send public read receipt when enabled", () => {
|
||||
|
@ -169,7 +169,7 @@ describe('TimelinePanel', () => {
|
|||
});
|
||||
|
||||
renderPanel(room, events);
|
||||
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(room.roomId, null, null, events[0]);
|
||||
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(room.roomId, "", undefined, events[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue