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

Add option to stop sending read receipts (delabs MSC2285: private read receipts) #8629

Merged
merged 40 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7110ced
Add tooltip
SimonBrandner May 17, 2022
a244da7
Add disabled tooltip
SimonBrandner May 17, 2022
bbf3a60
Delabs MSC2285: Private read receipts
SimonBrandner May 17, 2022
53c50ea
i18n
SimonBrandner May 17, 2022
dea9866
Update snaps
SimonBrandner May 17, 2022
1d9916b
Use a turnary operator
SimonBrandner May 18, 2022
c77d2f6
Fix typo
SimonBrandner May 19, 2022
3c04bc4
`disabledTooltip` -> `disabledTooltipText`
SimonBrandner May 19, 2022
c1f4370
Improve test utils
SimonBrandner May 21, 2022
10c14f6
Allow `=`
SimonBrandner May 21, 2022
1984bae
Add `TimelinePanel-tests`
SimonBrandner May 21, 2022
e8d1069
Improve formatting
SimonBrandner May 25, 2022
18aad85
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner May 25, 2022
e62a8f2
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner Jun 10, 2022
7c61400
Move `Presence` settings to the `Preferences` tab
SimonBrandner Jun 13, 2022
0abf5ef
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner Jun 13, 2022
c46607f
Revert "Add tooltip"
SimonBrandner Jun 14, 2022
fdd7065
Move disabled tooltip to microcopy
SimonBrandner Jun 14, 2022
31fa423
Update copy
SimonBrandner Jun 14, 2022
2a3c15d
Fix tests
SimonBrandner Jun 14, 2022
9926bc0
Make categories private
SimonBrandner Jun 15, 2022
356eba9
Add `sendReadReceipts` as a comment
SimonBrandner Jun 15, 2022
4ac6156
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner Jun 15, 2022
eeb2e77
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner Jun 24, 2022
b7bc7fc
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner Jul 14, 2022
fded4fa
Switch to stable prefixes for MSC2285
SimonBrandner Jul 14, 2022
5c3f27e
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner Jul 30, 2022
578d24c
Make sure `inNodeView()` works in tests
SimonBrandner Aug 3, 2022
db69f9e
Switch to stable `msc2285`
SimonBrandner Aug 3, 2022
db72558
Write tests
SimonBrandner Aug 3, 2022
769b14d
Merge branch 'SimonBrandner/feat/disable-rr' into SimonBrandner/feat/…
SimonBrandner Aug 3, 2022
f9e38d9
Show MSC2285 both if stable and unstable
SimonBrandner Aug 4, 2022
e61b634
Support both stabel and unstable private RRs
SimonBrandner Aug 4, 2022
81e709a
Delint
SimonBrandner Aug 4, 2022
7ca38f6
Merge remote-tracking branch 'upstream/develop' into SimonBrandner/fe…
SimonBrandner Aug 4, 2022
6afd527
Add comment
SimonBrandner Aug 5, 2022
b4110b1
Remove duplicate line
SimonBrandner Aug 5, 2022
bcfd67d
Use try-catch
SimonBrandner Aug 5, 2022
2176a11
Add mising mock
SimonBrandner Aug 5, 2022
b953689
Update string
SimonBrandner Aug 5, 2022
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
25 changes: 14 additions & 11 deletions src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -943,25 +943,26 @@ class TimelinePanel extends React.Component<IProps, IState> {
this.lastRMSentEventId = this.state.readMarkerEventId;

const roomId = this.props.timelineSet.room.roomId;
const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
const sendRRs = SettingsStore.getValue("sendReadReceipts", roomId);
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved

debuglog(
`Sending Read Markers for ${this.props.timelineSet.room.roomId}: `,
`rm=${this.state.readMarkerEventId} `,
`rr=${sendRRs ? lastReadEvent?.getId() : null} `,
`prr=${lastReadEvent?.getId()}`,

debuglog('Sending Read Markers for ',
this.props.timelineSet.room.roomId,
'rm', this.state.readMarkerEventId,
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
' hidden:' + hiddenRR,
);
MatrixClientPeg.get().setRoomReadMarkers(
roomId,
this.state.readMarkerEventId,
hiddenRR ? null : lastReadEvent, // Could be null, in which case no RR is sent
lastReadEvent, // Could be null, in which case no private RR is sent
sendRRs ? lastReadEvent : null, // Public read receipt (could be null)
lastReadEvent, // Private read receipt (could be null)
).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent,
hiddenRR ? ReceiptType.ReadPrivate : ReceiptType.Read,
sendRRs ? ReceiptType.Read : ReceiptType.ReadPrivate,
).catch((e) => {
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
logger.error(e);
this.lastRRSentEventId = undefined;
Expand Down Expand Up @@ -1553,8 +1554,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
const isNodeInView = (node) => {
if (node) {
const boundingRect = node.getBoundingClientRect();
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
if (
(allowPartial && boundingRect.top <= wrapperRect.bottom) ||
(!allowPartial && boundingRect.bottom <= wrapperRect.bottom)
) {
return true;
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/components/views/elements/SettingsFlag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface IProps {
// XXX: once design replaces all toggles make this the default
useCheckbox?: boolean;
disabled?: boolean;
disabledDescription?: string;
hideIfCannotSet?: boolean;
onChange?(checked: boolean): void;
}
Expand Down Expand Up @@ -84,6 +85,13 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
: SettingsStore.getDisplayName(this.props.name, this.props.level);
const description = SettingsStore.getDescription(this.props.name);

let disabledDescription: JSX.Element;
if (this.props.disabled && this.props.disabledDescription) {
disabledDescription = <div className="mx_SettingsFlag_microcopy">
{ this.props.disabledDescription }
</div>;
}

if (this.props.useCheckbox) {
return <StyledCheckbox
checked={this.state.value}
Expand All @@ -100,6 +108,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
{ description && <div className="mx_SettingsFlag_microcopy">
{ description }
</div> }
{ disabledDescription }
</label>
<ToggleSwitch
checked={this.state.value}
Expand Down
16 changes: 0 additions & 16 deletions src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,18 @@ export class LabsSettingToggle extends React.Component<ILabsSettingToggleProps>
}

interface IState {
showHiddenReadReceipts: boolean;
showJumpToDate: boolean;
}

export default class LabsUserSettingsTab extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);

MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => {
this.setState({ showHiddenReadReceipts });
});

MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc3030").then((showJumpToDate) => {
this.setState({ showJumpToDate });
});

this.state = {
showHiddenReadReceipts: false,
showJumpToDate: false,
};
}
Expand Down Expand Up @@ -113,16 +107,6 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
/>,
);

if (this.state.showHiddenReadReceipts) {
groups.getOrCreate(LabGroup.Messaging, []).push(
<SettingsFlag
key="feature_hidden_read_receipts"
name="feature_hidden_read_receipts"
level={SettingLevel.DEVICE}
/>,
);
}

if (this.state.showJumpToDate) {
groups.getOrCreate(LabGroup.Messaging, []).push(
<SettingsFlag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import { UserTab } from "../../../dialogs/UserTab";
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
import { Action } from "../../../../../dispatcher/actions";
import SdkConfig from "../../../../../SdkConfig";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";

interface IProps {
closeSettingsFn(success: boolean): void;
}

interface IState {
disablingReadReceiptsSupported: boolean;
autocompleteDelay: string;
readMarkerInViewThresholdMs: string;
readMarkerOutOfViewThresholdMs: string;
Expand All @@ -52,11 +54,14 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
'ctrlFForSearch',
];

static PRESENCE_SETTINGS = [
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
"sendTypingNotifications",
];

static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.useMarkdown',
'MessageComposerInput.suggestEmoji',
'sendTypingNotifications',
'MessageComposerInput.ctrlEnterToSend',
'MessageComposerInput.surroundWith',
'MessageComposerInput.showStickersButton',
Expand Down Expand Up @@ -100,6 +105,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
super(props);

this.state = {
disablingReadReceiptsSupported: false,
autocompleteDelay:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay').toString(10),
readMarkerInViewThresholdMs:
Expand All @@ -109,6 +115,14 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
};
}

public async componentDidMount(): Promise<void> {
this.setState({
disablingReadReceiptsSupported: (
await MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285")
),
});
}

private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autocompleteDelay: e.target.value });
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
Expand Down Expand Up @@ -175,6 +189,20 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
{ this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS) }
</div>

<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Presence") }</span>
<span className="mx_SettingsTab_subsectionText">
{ _t("Choose what others can see from your presence.") }
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
</span>
<SettingsFlag
disabled={!this.state.disablingReadReceiptsSupported}
disabledDescription={_t("Your server doesn't support disabling sending read receipts.")}
name="sendReadReceipts"
level={SettingLevel.ACCOUNT}
/>
{ this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS) }
</div>

<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Composer") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
<div className="mx_SettingsTab_subsectionText">
<p>
{ _t("Share anonymous data to help us identify issues. Nothing personal. " +
"No third parties.") }
"No third parties.") }
</p>
<p>
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={onClickAnalyticsLearnMore}>
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@
"To leave, just return to this page or click on the beta badge when you search.": "To leave, just return to this page or click on the beta badge when you search.",
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
"Don't send read receipts": "Don't send read receipts",
"Send read receipts": "Send read receipts",
"Right-click message context menu": "Right-click message context menu",
"Location sharing - pin drop": "Location sharing - pin drop",
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
Expand Down Expand Up @@ -1515,6 +1515,9 @@
"Keyboard shortcuts": "Keyboard shortcuts",
"To view all keyboard shortcuts, <a>click here</a>.": "To view all keyboard shortcuts, <a>click here</a>.",
"Displaying time": "Displaying time",
"Presence": "Presence",
"Choose what others can see from your presence.": "Choose what others can see from your presence.",
"Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.",
"Composer": "Composer",
"Code blocks": "Code blocks",
"Images, GIFs and videos": "Images, GIFs and videos",
Expand Down
8 changes: 4 additions & 4 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -424,10 +424,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: null,
},
"feature_hidden_read_receipts": {
supportedLevels: LEVELS_FEATURE,
displayName: _td("Don't send read receipts"),
default: false,
"sendReadReceipts": {
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Send read receipts"),
default: true,
},
"feature_message_right_click_context_menu": {
isFeature: true,
Expand Down
104 changes: 87 additions & 17 deletions test/components/structures/TimelinePanel-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,55 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import React from "react";
import { mount, ReactWrapper } from "enzyme";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { MessageEvent } from 'matrix-events-sdk';
import { EventTimelineSet, MatrixEvent, PendingEventOrdering, Room } from 'matrix-js-sdk/src/matrix';
import { EventType } from "matrix-js-sdk/src/@types/event";

import { stubClient } from "../../test-utils";
import TimelinePanel from '../../../src/components/structures/TimelinePanel';
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
import TimelinePanel from "../../../src/components/structures/TimelinePanel";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { mkRoom, stubClient } from "../../test-utils";
import SettingsStore from "../../../src/settings/SettingsStore";

function newReceipt(eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent {
const receiptContent = {
[eventId]: {
"m.read": { [userId]: { ts: readTs } },
"org.matrix.msc2285.read.private": { [userId]: { ts: readTs } },
"m.fully_read": { [userId]: { ts: fullyReadTs } },
},
};
return new MatrixEvent({ content: receiptContent, type: "m.receipt" });
}
describe("TimelinePanel", () => {
beforeAll(() => {
stubClient();
});

describe("read receipts and read markers", () => {
it("sends public read receipt when enabled", () => {
const client = MatrixClientPeg.get();
const room = mkRoom(client, "roomId");
const events = mockEvents(room);

const getValueCopy = SettingsStore.getValue;
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
if (name === "sendReadReceipts") return true;
return getValueCopy(name);
});

mountPanel(room, events);
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(room.roomId, null, events[0], events[0]);
});

it("does not send public read receipt when enabled", () => {
const client = MatrixClientPeg.get();
const room = mkRoom(client, "roomId");
const events = mockEvents(room);

const getValueCopy = SettingsStore.getValue;
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
if (name === "sendReadReceipts") return false;
return getValueCopy(name);
});

mountPanel(room, events);
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(room.roomId, null, null, events[0]);
});

describe('TimelinePanel', () => {
describe('Read Receipts and Markers', () => {
it('Forgets the read marker when asked to', () => {
it('forgets the read marker when asked to', () => {
stubClient();
const cli = MatrixClientPeg.get();
const readMarkersSent = [];
Expand Down Expand Up @@ -96,3 +122,47 @@ describe('TimelinePanel', () => {
});
});
});

const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent => {
const receiptContent = {
[eventId]: {
"m.read": { [userId]: { ts: readTs } },
"org.matrix.msc2285.read.private": { [userId]: { ts: readTs } },
"m.fully_read": { [userId]: { ts: fullyReadTs } },
},
};
return new MatrixEvent({ content: receiptContent, type: "m.receipt" });
};

const mountPanel = (room: Room, events: MatrixEvent[]): ReactWrapper => {
const timelineSet = { room: room as Room } as EventTimelineSet;
const timeline = new EventTimeline(timelineSet);
events.forEach((event) => timeline.addEvent(event, true));
timelineSet.getLiveTimeline = () => timeline;
timelineSet.getTimelineForEvent = () => timeline;
timelineSet.getPendingEvents = () => events;
timelineSet.room.getEventReadUpTo = () => events[1].getId();

return mount(
<TimelinePanel
timelineSet={timelineSet}
manageReadReceipts
sendReadReceiptOnLoad
/>,
);
};

const mockEvents = (room: Room, count = 2): MatrixEvent[] => {
const events = [];
for (let index = 0; index < count; index++) {
events.push(new MatrixEvent({
room_id: room.roomId,
event_id: `event_${index}`,
type: EventType.RoomMessage,
user_id: "userId",
content: MessageEvent.from(`Event${index}`).serialize().content,
}));
}

return events;
};
4 changes: 4 additions & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function createTestClient(): MatrixClient {
mxcUrlToHttp: (mxc) => `http://this.is.a.url/${mxc.substring(6)}`,
setAccountData: jest.fn(),
setRoomAccountData: jest.fn(),
setRoomReadMarkers: jest.fn().mockResolvedValue({}),
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
setRoomTopic: jest.fn(),
sendTyping: jest.fn().mockResolvedValue({}),
sendMessage: () => jest.fn().mockResolvedValue({}),
Expand Down Expand Up @@ -359,6 +360,8 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
getMembersWithMembership: jest.fn().mockReturnValue([]),
getJoinedMembers: jest.fn().mockReturnValue([]),
getJoinedMemberCount: jest.fn().mockReturnValue(1),
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1),
setUnreadNotificationCount: jest.fn(),
getMembers: jest.fn().mockReturnValue([]),
getPendingEvents: () => [],
getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
Expand Down Expand Up @@ -403,6 +406,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
myUserId: client?.getUserId(),
canInvite: jest.fn(),
getThreads: jest.fn().mockReturnValue([]),
eventShouldLiveIn: jest.fn().mockReturnValue({}),
} as unknown as Room;
}

Expand Down