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

Commit 7eaed1a

Browse files
Add option to stop sending read receipts (delabs MSC2285: private read receipts) (#8629)
Co-authored-by: Travis Ralston <[email protected]>
1 parent b61cc48 commit 7eaed1a

File tree

11 files changed

+188
-68
lines changed

11 files changed

+188
-68
lines changed

src/components/structures/MessagePanel.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
2323
import { Relations } from "matrix-js-sdk/src/models/relations";
2424
import { logger } from 'matrix-js-sdk/src/logger';
2525
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
26-
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
2726
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
27+
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
2828

2929
import shouldHideEvent from '../../shouldHideEvent';
3030
import { wantsDateSeparator } from '../../DateUtils';
@@ -828,7 +828,11 @@ export default class MessagePanel extends React.Component<IProps, IState> {
828828
}
829829
const receipts: IReadReceiptProps[] = [];
830830
room.getReceiptsForEvent(event).forEach((r) => {
831-
if (!r.userId || ![ReceiptType.Read, ReceiptType.ReadPrivate].includes(r.type) || r.userId === myUserId) {
831+
if (
832+
!r.userId ||
833+
!isSupportedReceiptType(r.type) ||
834+
r.userId === myUserId
835+
) {
832836
return; // ignore non-read receipts and receipts from self.
833837
}
834838
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {

src/components/structures/TimelinePanel.tsx

+25-16
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ClientEvent } from "matrix-js-sdk/src/client";
3030
import { Thread } from 'matrix-js-sdk/src/models/thread';
3131
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
3232
import { MatrixError } from 'matrix-js-sdk/src/http-api';
33+
import { getPrivateReadReceiptField } from "matrix-js-sdk/src/utils";
3334

3435
import SettingsStore from "../../settings/SettingsStore";
3536
import { Layout } from "../../settings/enums/Layout";
@@ -965,29 +966,35 @@ class TimelinePanel extends React.Component<IProps, IState> {
965966
this.lastRMSentEventId = this.state.readMarkerEventId;
966967

967968
const roomId = this.props.timelineSet.room.roomId;
968-
const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
969+
const sendRRs = SettingsStore.getValue("sendReadReceipts", roomId);
970+
971+
debuglog(
972+
`Sending Read Markers for ${this.props.timelineSet.room.roomId}: `,
973+
`rm=${this.state.readMarkerEventId} `,
974+
`rr=${sendRRs ? lastReadEvent?.getId() : null} `,
975+
`prr=${lastReadEvent?.getId()}`,
969976

970-
debuglog('Sending Read Markers for ',
971-
this.props.timelineSet.room.roomId,
972-
'rm', this.state.readMarkerEventId,
973-
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
974-
' hidden:' + hiddenRR,
975977
);
976978
MatrixClientPeg.get().setRoomReadMarkers(
977979
roomId,
978980
this.state.readMarkerEventId,
979-
hiddenRR ? null : lastReadEvent, // Could be null, in which case no RR is sent
980-
lastReadEvent, // Could be null, in which case no private RR is sent
981-
).catch((e) => {
981+
sendRRs ? lastReadEvent : null, // Public read receipt (could be null)
982+
lastReadEvent, // Private read receipt (could be null)
983+
).catch(async (e) => {
982984
// /read_markers API is not implemented on this HS, fallback to just RR
983985
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
984-
return MatrixClientPeg.get().sendReadReceipt(
985-
lastReadEvent,
986-
hiddenRR ? ReceiptType.ReadPrivate : ReceiptType.Read,
987-
).catch((e) => {
986+
const privateField = await getPrivateReadReceiptField(MatrixClientPeg.get());
987+
if (!sendRRs && !privateField) return;
988+
989+
try {
990+
return await MatrixClientPeg.get().sendReadReceipt(
991+
lastReadEvent,
992+
sendRRs ? ReceiptType.Read : privateField,
993+
);
994+
} catch (error) {
988995
logger.error(e);
989996
this.lastRRSentEventId = undefined;
990-
});
997+
}
991998
} else {
992999
logger.error(e);
9931000
}
@@ -1575,8 +1582,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
15751582
const isNodeInView = (node) => {
15761583
if (node) {
15771584
const boundingRect = node.getBoundingClientRect();
1578-
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
1579-
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
1585+
if (
1586+
(allowPartial && boundingRect.top <= wrapperRect.bottom) ||
1587+
(!allowPartial && boundingRect.bottom <= wrapperRect.bottom)
1588+
) {
15801589
return true;
15811590
}
15821591
}

src/components/views/elements/SettingsFlag.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface IProps {
3333
// XXX: once design replaces all toggles make this the default
3434
useCheckbox?: boolean;
3535
disabled?: boolean;
36+
disabledDescription?: string;
3637
hideIfCannotSet?: boolean;
3738
onChange?(checked: boolean): void;
3839
}
@@ -84,6 +85,13 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
8485
: SettingsStore.getDisplayName(this.props.name, this.props.level);
8586
const description = SettingsStore.getDescription(this.props.name);
8687

88+
let disabledDescription: JSX.Element;
89+
if (this.props.disabled && this.props.disabledDescription) {
90+
disabledDescription = <div className="mx_SettingsFlag_microcopy">
91+
{ this.props.disabledDescription }
92+
</div>;
93+
}
94+
8795
if (this.props.useCheckbox) {
8896
return <StyledCheckbox
8997
checked={this.state.value}
@@ -100,6 +108,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
100108
{ description && <div className="mx_SettingsFlag_microcopy">
101109
{ description }
102110
</div> }
111+
{ disabledDescription }
103112
</label>
104113
<ToggleSwitch
105114
checked={this.state.value}

src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx

-16
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export class LabsSettingToggle extends React.Component<ILabsSettingToggleProps>
4747
}
4848

4949
interface IState {
50-
showHiddenReadReceipts: boolean;
5150
showJumpToDate: boolean;
5251
showExploringPublicSpaces: boolean;
5352
}
@@ -58,10 +57,6 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
5857

5958
const cli = MatrixClientPeg.get();
6059

61-
cli.doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
62-
this.setState({ showHiddenReadReceipts });
63-
});
64-
6560
cli.doesServerSupportUnstableFeature("org.matrix.msc3030").then((showJumpToDate) => {
6661
this.setState({ showJumpToDate });
6762
});
@@ -71,7 +66,6 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
7166
});
7267

7368
this.state = {
74-
showHiddenReadReceipts: false,
7569
showJumpToDate: false,
7670
showExploringPublicSpaces: false,
7771
};
@@ -121,16 +115,6 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
121115
/>,
122116
);
123117

124-
if (this.state.showHiddenReadReceipts) {
125-
groups.getOrCreate(LabGroup.Messaging, []).push(
126-
<SettingsFlag
127-
key="feature_hidden_read_receipts"
128-
name="feature_hidden_read_receipts"
129-
level={SettingLevel.DEVICE}
130-
/>,
131-
);
132-
}
133-
134118
if (this.state.showJumpToDate) {
135119
groups.getOrCreate(LabGroup.Messaging, []).push(
136120
<SettingsFlag

src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx

+44-10
Original file line numberDiff line numberDiff line change
@@ -29,58 +29,67 @@ import { UserTab } from "../../../dialogs/UserTab";
2929
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
3030
import { Action } from "../../../../../dispatcher/actions";
3131
import SdkConfig from "../../../../../SdkConfig";
32+
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
3233
import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingPage";
3334

3435
interface IProps {
3536
closeSettingsFn(success: boolean): void;
3637
}
3738

3839
interface IState {
40+
disablingReadReceiptsSupported: boolean;
3941
autocompleteDelay: string;
4042
readMarkerInViewThresholdMs: string;
4143
readMarkerOutOfViewThresholdMs: string;
4244
}
4345

4446
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
45-
static ROOM_LIST_SETTINGS = [
47+
private static ROOM_LIST_SETTINGS = [
4648
'breadcrumbs',
4749
];
4850

49-
static SPACES_SETTINGS = [
51+
private static SPACES_SETTINGS = [
5052
"Spaces.allRoomsInHome",
5153
];
5254

53-
static KEYBINDINGS_SETTINGS = [
55+
private static KEYBINDINGS_SETTINGS = [
5456
'ctrlFForSearch',
5557
];
5658

57-
static COMPOSER_SETTINGS = [
59+
private static PRESENCE_SETTINGS = [
60+
"sendTypingNotifications",
61+
// sendReadReceipts - handled specially due to server needing support
62+
];
63+
64+
private static COMPOSER_SETTINGS = [
5865
'MessageComposerInput.autoReplaceEmoji',
5966
'MessageComposerInput.useMarkdown',
6067
'MessageComposerInput.suggestEmoji',
61-
'sendTypingNotifications',
6268
'MessageComposerInput.ctrlEnterToSend',
6369
'MessageComposerInput.surroundWith',
6470
'MessageComposerInput.showStickersButton',
6571
'MessageComposerInput.insertTrailingColon',
6672
];
6773

68-
static TIME_SETTINGS = [
74+
private static TIME_SETTINGS = [
6975
'showTwelveHourTimestamps',
7076
'alwaysShowTimestamps',
7177
];
72-
static CODE_BLOCKS_SETTINGS = [
78+
79+
private static CODE_BLOCKS_SETTINGS = [
7380
'enableSyntaxHighlightLanguageDetection',
7481
'expandCodeByDefault',
7582
'showCodeLineNumbers',
7683
];
77-
static IMAGES_AND_VIDEOS_SETTINGS = [
84+
85+
private static IMAGES_AND_VIDEOS_SETTINGS = [
7886
'urlPreviewsEnabled',
7987
'autoplayGifs',
8088
'autoplayVideo',
8189
'showImages',
8290
];
83-
static TIMELINE_SETTINGS = [
91+
92+
private static TIMELINE_SETTINGS = [
8493
'showTypingNotifications',
8594
'showRedactions',
8695
'showReadReceipts',
@@ -93,7 +102,8 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
93102
'scrollToBottomOnMessageSent',
94103
'useOnlyCurrentProfiles',
95104
];
96-
static GENERAL_SETTINGS = [
105+
106+
private static GENERAL_SETTINGS = [
97107
'promptBeforeInviteUnknownUsers',
98108
// Start automatically after startup (electron-only)
99109
// Autocomplete delay (niche text box)
@@ -103,6 +113,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
103113
super(props);
104114

105115
this.state = {
116+
disablingReadReceiptsSupported: false,
106117
autocompleteDelay:
107118
SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay').toString(10),
108119
readMarkerInViewThresholdMs:
@@ -112,6 +123,15 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
112123
};
113124
}
114125

126+
public async componentDidMount(): Promise<void> {
127+
this.setState({
128+
disablingReadReceiptsSupported: (
129+
await MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285.stable") ||
130+
await MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285")
131+
),
132+
});
133+
}
134+
115135
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
116136
this.setState({ autocompleteDelay: e.target.value });
117137
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
@@ -185,6 +205,20 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
185205
{ this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS) }
186206
</div>
187207

208+
<div className="mx_SettingsTab_section">
209+
<span className="mx_SettingsTab_subheading">{ _t("Presence") }</span>
210+
<span className="mx_SettingsTab_subsectionText">
211+
{ _t("Share your activity and status with others.") }
212+
</span>
213+
<SettingsFlag
214+
disabled={!this.state.disablingReadReceiptsSupported}
215+
disabledDescription={_t("Your server doesn't support disabling sending read receipts.")}
216+
name="sendReadReceipts"
217+
level={SettingLevel.ACCOUNT}
218+
/>
219+
{ this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS) }
220+
</div>
221+
188222
<div className="mx_SettingsTab_section">
189223
<span className="mx_SettingsTab_subheading">{ _t("Composer") }</span>
190224
{ this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS) }

src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
304304
<div className="mx_SettingsTab_subsectionText">
305305
<p>
306306
{ _t("Share anonymous data to help us identify issues. Nothing personal. " +
307-
"No third parties.") }
307+
"No third parties.") }
308308
</p>
309309
<AccessibleButton
310310
kind="link"

src/i18n/strings/en_EN.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,7 @@
898898
"Use new room breadcrumbs": "Use new room breadcrumbs",
899899
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
900900
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
901-
"Don't send read receipts": "Don't send read receipts",
901+
"Send read receipts": "Send read receipts",
902902
"Right-click message context menu": "Right-click message context menu",
903903
"Location sharing - pin drop": "Location sharing - pin drop",
904904
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
@@ -1538,6 +1538,9 @@
15381538
"Keyboard shortcuts": "Keyboard shortcuts",
15391539
"To view all keyboard shortcuts, <a>click here</a>.": "To view all keyboard shortcuts, <a>click here</a>.",
15401540
"Displaying time": "Displaying time",
1541+
"Presence": "Presence",
1542+
"Share your activity and status with others.": "Share your activity and status with others.",
1543+
"Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.",
15411544
"Composer": "Composer",
15421545
"Code blocks": "Code blocks",
15431546
"Images, GIFs and videos": "Images, GIFs and videos",

src/settings/Settings.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
401401
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
402402
default: null,
403403
},
404-
"feature_hidden_read_receipts": {
405-
supportedLevels: LEVELS_FEATURE,
406-
displayName: _td("Don't send read receipts"),
407-
default: false,
404+
"sendReadReceipts": {
405+
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
406+
displayName: _td("Send read receipts"),
407+
default: true,
408408
},
409409
"feature_message_right_click_context_menu": {
410410
isFeature: true,

src/utils/read-receipts.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
1818
import { MatrixClient } from "matrix-js-sdk/src/client";
19-
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
19+
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
2020

2121
/**
2222
* Determines if a read receipt update event includes the client's own user.
@@ -27,13 +27,10 @@ import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
2727
export function readReceiptChangeIsFor(event: MatrixEvent, client: MatrixClient): boolean {
2828
const myUserId = client.getUserId();
2929
for (const eventId of Object.keys(event.getContent())) {
30-
const readReceiptUsers = Object.keys(event.getContent()[eventId][ReceiptType.Read] || {});
31-
if (readReceiptUsers.includes(myUserId)) {
32-
return true;
33-
}
34-
const readPrivateReceiptUsers = Object.keys(event.getContent()[eventId][ReceiptType.ReadPrivate] || {});
35-
if (readPrivateReceiptUsers.includes(myUserId)) {
36-
return true;
30+
for (const [receiptType, receipt] of Object.entries(event.getContent()[eventId])) {
31+
if (!isSupportedReceiptType(receiptType)) continue;
32+
33+
if (Object.keys((receipt || {})).includes(myUserId)) return true;
3734
}
3835
}
3936
}

0 commit comments

Comments
 (0)