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

Commit 32478db

Browse files
authored
Migrate the hidden read receipts flag to new "send read receipts" option (#9141)
* Migrate the hidden read receipts flag to new "send read receipts" option For safety. * Appease linter & ignore guests * `void`
1 parent f467d94 commit 32478db

File tree

6 files changed

+153
-6
lines changed

6 files changed

+153
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/// <reference types="cypress" />
18+
19+
import { SynapseInstance } from "../../plugins/synapsedocker";
20+
21+
function seedLabs(synapse: SynapseInstance, labsVal: boolean | null): void {
22+
cy.initTestUser(synapse, "Sally", () => {
23+
// seed labs flag
24+
cy.window({ log: false }).then(win => {
25+
if (typeof labsVal === "boolean") {
26+
// stringify boolean
27+
win.localStorage.setItem("mx_labs_feature_feature_hidden_read_receipts", `${labsVal}`);
28+
}
29+
});
30+
});
31+
}
32+
33+
function testForVal(settingVal: boolean | null): void {
34+
const testRoomName = "READ RECEIPTS";
35+
cy.createRoom({ name: testRoomName }).as("roomId");
36+
cy.all([cy.get<string>("@roomId")]).then(() => {
37+
cy.viewRoomByName(testRoomName).then(() => {
38+
// if we can see the room, then sync is working for us. It's time to see if the
39+
// migration even ran.
40+
41+
cy.getSettingValue("sendReadReceipts", null, true).should("satisfy", (val) => {
42+
if (typeof settingVal === "boolean") {
43+
return val === settingVal;
44+
} else {
45+
return !val; // falsy - we don't actually care if it's undefined, null, or a literal false
46+
}
47+
});
48+
});
49+
});
50+
}
51+
52+
describe("Hidden Read Receipts Setting Migration", () => {
53+
// We run this as a full-blown end-to-end test to ensure it works in an integration
54+
// sense. If we unit tested it, we'd be testing that the code works but not that the
55+
// migration actually runs.
56+
//
57+
// Here, we get to test that not only the code works but also that it gets run. Most
58+
// of our interactions are with the JS console as we're honestly just checking that
59+
// things got set correctly.
60+
//
61+
// For a security-sensitive feature like hidden read receipts, it's absolutely vital
62+
// that we migrate the setting appropriately.
63+
64+
let synapse: SynapseInstance;
65+
66+
beforeEach(() => {
67+
cy.startSynapse("default").then(data => {
68+
synapse = data;
69+
});
70+
});
71+
72+
afterEach(() => {
73+
cy.stopSynapse(synapse);
74+
});
75+
76+
it('should not migrate the lack of a labs flag', () => {
77+
seedLabs(synapse, null);
78+
testForVal(null);
79+
});
80+
81+
it('should migrate labsHiddenRR=false as sendRR=true', () => {
82+
seedLabs(synapse, false);
83+
testForVal(true);
84+
});
85+
86+
it('should migrate labsHiddenRR=true as sendRR=false', () => {
87+
seedLabs(synapse, true);
88+
testForVal(false);
89+
});
90+
});

cypress/support/login.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,19 @@ declare global {
3535
* Generates a test user and instantiates an Element session with that user.
3636
* @param synapse the synapse returned by startSynapse
3737
* @param displayName the displayName to give the test user
38+
* @param prelaunchFn optional function to run before the app is visited
3839
*/
39-
initTestUser(synapse: SynapseInstance, displayName: string): Chainable<UserCredentials>;
40+
initTestUser(
41+
synapse: SynapseInstance,
42+
displayName: string,
43+
prelaunchFn?: () => void,
44+
): Chainable<UserCredentials>;
4045
}
4146
}
4247
}
4348

44-
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
49+
// eslint-disable-next-line max-len
50+
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string, prelaunchFn?: () => void): Chainable<UserCredentials> => {
4551
// XXX: work around Cypress not clearing IDB between tests
4652
cy.window({ log: false }).then(win => {
4753
win.indexedDB.databases().then(databases => {
@@ -87,6 +93,8 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
8793
win.localStorage.setItem("mx_local_settings", '{"language":"en"}');
8894
});
8995

96+
prelaunchFn?.();
97+
9098
return cy.visit("/").then(() => {
9199
// wait for the app to load
92100
return cy.get(".mx_MatrixChat", { timeout: 15000 });

cypress/support/settings.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ declare global {
9696
* value.
9797
* @return {*} The value, or null if not found
9898
*/
99-
getSettingValue<T>(name: string, roomId?: string): Chainable<T>;
99+
getSettingValue<T>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T>;
100100
}
101101
}
102102
}
@@ -116,9 +116,10 @@ Cypress.Commands.add("setSettingValue", (
116116
});
117117
});
118118

119-
Cypress.Commands.add("getSettingValue", <T = any>(name: string, roomId?: string): Chainable<T> => {
119+
// eslint-disable-next-line max-len
120+
Cypress.Commands.add("getSettingValue", <T = any>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T> => {
120121
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
121-
return store.getValue(name, roomId);
122+
return store.getValue(name, roomId, excludeDefault);
122123
});
123124
});
124125

src/Lifecycle.ts

+3
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,9 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
826826
await MatrixClientPeg.assign();
827827
}
828828

829+
// Run the migrations after the MatrixClientPeg has been assigned
830+
SettingsStore.runMigrations();
831+
829832
// This needs to be started after crypto is set up
830833
DeviceListener.sharedInstance().start();
831834
// Similarly, don't start sending presence updates until we've started

src/settings/SettingsStore.ts

+41
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import SettingsHandler from "./handlers/SettingsHandler";
3535
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
3636
import { Action } from "../dispatcher/actions";
3737
import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler";
38+
import dispatcher from "../dispatcher/dispatcher";
39+
import { ActionPayload } from "../dispatcher/payloads";
40+
import { MatrixClientPeg } from "../MatrixClientPeg";
3841

3942
const defaultWatchManager = new WatchManager();
4043

@@ -565,6 +568,44 @@ export default class SettingsStore {
565568
return null;
566569
}
567570

571+
/**
572+
* Runs or queues any setting migrations needed.
573+
*/
574+
public static runMigrations(): void {
575+
// Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and
576+
// add a comment to note when it can be removed.
577+
578+
SettingsStore.migrateHiddenReadReceipts(); // Can be removed after October 2022.
579+
}
580+
581+
private static migrateHiddenReadReceipts(): void {
582+
if (MatrixClientPeg.get().isGuest()) return; // not worth it
583+
584+
// We wait for the first sync to ensure that the user's existing account data has loaded, as otherwise
585+
// getValue() for an account-level setting like sendReadReceipts will return `null`.
586+
const disRef = dispatcher.register((payload: ActionPayload) => {
587+
if (payload.action === "MatrixActions.sync") {
588+
dispatcher.unregister(disRef);
589+
590+
const rrVal = SettingsStore.getValue("sendReadReceipts", null, true);
591+
if (typeof rrVal !== "boolean") {
592+
// new setting isn't set - see if the labs flag was. We have to manually reach into the
593+
// handler for this because it isn't a setting anymore (`getValue` will yell at us).
594+
const handler = LEVEL_HANDLERS[SettingLevel.DEVICE] as DeviceSettingsHandler;
595+
const labsVal = handler.readFeature("feature_hidden_read_receipts");
596+
if (typeof labsVal === "boolean") {
597+
// Inverse of labs flag because negative->positive language switch in setting name
598+
const newVal = !labsVal;
599+
console.log(`Setting sendReadReceipts to ${newVal} because of previously-set labs flag`);
600+
601+
// noinspection JSIgnoredPromiseFromCall
602+
SettingsStore.setValue("sendReadReceipts", null, SettingLevel.ACCOUNT, newVal);
603+
}
604+
}
605+
}
606+
});
607+
}
608+
568609
/**
569610
* Debugging function for reading explicit setting values without going through the
570611
* complicated/biased functions in the SettingsStore. This will print information to

src/settings/handlers/DeviceSettingsHandler.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,16 @@ export default class DeviceSettingsHandler extends AbstractLocalStorageSettingsH
114114
// Note: features intentionally don't use the same key as settings to avoid conflicts
115115
// and to be backwards compatible.
116116

117-
private readFeature(featureName: string): boolean | null {
117+
// public for access to migrations - not exposed from the SettingsHandler interface
118+
public readFeature(featureName: string): boolean | null {
118119
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest()) {
119120
// Guests should not have any labs features enabled.
120121
return false;
121122
}
122123

124+
// XXX: This turns they key names into `mx_labs_feature_feature_x` (double feature).
125+
// This is because all feature names start with `feature_` as a matter of policy.
126+
// Oh well.
123127
return this.getBoolean("mx_labs_feature_" + featureName);
124128
}
125129

0 commit comments

Comments
 (0)