diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index e2141cd94d..a6bd5ba5b2 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -102,7 +102,7 @@ dis.register((payload) => { // If we unset the client and the component is updated, the render will fail and unmount everything. // (The module dialog closes and fires a `aria_unhide_main_app` that will trigger a re-render) stopMatrixClient(false); - doSetLoggedIn(typed.credentials, true).catch((e) => { + doSetLoggedIn(typed.credentials, true, true).catch((e) => { // XXX we might want to fire a new event here to let the app know that the login failed ? // The module api could use it to display a message to the user. logger.warn("Failed to overwrite login", e); @@ -208,6 +208,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise guest: true, }, true, + false, ).then(() => true); } const success = await restoreFromLocalStorage({ @@ -465,6 +466,7 @@ function registerAsGuest(hsUrl: string, isUrl?: string, defaultDeviceDisplayName guest: true, }, true, + true, ).then(() => true); }, (err) => { @@ -610,6 +612,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): freshLogin: freshLogin, }, false, + false, ); return true; } else { @@ -663,7 +666,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { +async function doSetLoggedIn( + credentials: IMatrixClientCreds, + clearStorageEnabled: boolean, + isFreshLogin: boolean, +): Promise { checkSessionLock(); credentials.guest = Boolean(credentials.guest); @@ -840,6 +848,9 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable clientPegOpts.rustCryptoStoreKey?.fill(0); } + // Run the migrations after the MatrixClientPeg has been assigned + SettingsStore.runMigrations(isFreshLogin); + return client; } @@ -1020,9 +1031,6 @@ async function startMatrixClient( checkSessionLock(); - // Run the migrations after the MatrixClientPeg has been assigned - SettingsStore.runMigrations(); - // This needs to be started after crypto is set up DeviceListener.sharedInstance().start(client); // Similarly, don't start sending presence updates until we've started @@ -1165,5 +1173,6 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom userId, }, true, + false, ); }; diff --git a/src/components/views/room_settings/UrlPreviewSettings.tsx b/src/components/views/room_settings/UrlPreviewSettings.tsx index b2b4c553f0..ad0d1bd98e 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.tsx +++ b/src/components/views/room_settings/UrlPreviewSettings.tsx @@ -101,7 +101,7 @@ export default class UrlPreviewSettings extends React.Component { ( ); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index ab7cb664b5..476818a138 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -900,7 +900,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { controller: new UIFeatureController(UIFeature.URLPreviews), }, "urlPreviewsEnabled_e2ee": { - supportedLevels: [SettingLevel.ROOM_DEVICE, SettingLevel.ROOM_ACCOUNT], + supportedLevels: [SettingLevel.ROOM_DEVICE], displayName: { "room-account": _td("settings|inline_url_previews_room_account"), }, diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 6e3e9e3e1f..a63958f594 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -17,6 +17,7 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; import { ReactNode } from "react"; +import { ClientEvent, SyncState } from "matrix-js-sdk/src/matrix"; import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler"; import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler"; @@ -36,6 +37,7 @@ import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayl import { Action } from "../dispatcher/actions"; import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler"; import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; +import { MatrixClientPeg } from "../MatrixClientPeg"; // Convert the settings to easier to manage objects for the handlers const defaultSettings: Record = {}; @@ -637,10 +639,61 @@ export default class SettingsStore { return null; } + /** + * Migrate the setting for URL previews in e2e rooms from room account + * data to the room device level. + * + * @param isFreshLogin True if the user has just logged in, false if a previous session is being restored. + */ + private static async migrateURLPreviewsE2EE(isFreshLogin: boolean): Promise { + const MIGRATION_DONE_FLAG = "url_previews_e2ee_migration_done"; + if (localStorage.getItem(MIGRATION_DONE_FLAG)) return; + if (isFreshLogin) return; + + const client = MatrixClientPeg.safeGet(); + + const doMigration = async (): Promise => { + logger.info("Performing one-time settings migration of URL previews in E2EE rooms"); + + const roomAccounthandler = LEVEL_HANDLERS[SettingLevel.ROOM_ACCOUNT]; + + for (const room of client.getRooms()) { + // We need to use the handler directly because this setting is no longer supported + // at this level at all + const val = roomAccounthandler.getValue("urlPreviewsEnabled_e2ee", room.roomId); + + if (val !== undefined) { + await SettingsStore.setValue("urlPreviewsEnabled_e2ee", room.roomId, SettingLevel.ROOM_DEVICE, val); + } + } + + localStorage.setItem(MIGRATION_DONE_FLAG, "true"); + }; + + const onSync = (state: SyncState): void => { + if (state === SyncState.Prepared) { + client.removeListener(ClientEvent.Sync, onSync); + + doMigration().catch((e) => { + logger.error("Failed to migrate URL previews in E2EE rooms:", e); + }); + } + }; + + client.on(ClientEvent.Sync, onSync); + } + /** * Runs or queues any setting migrations needed. */ - public static runMigrations(): void { + public static runMigrations(isFreshLogin: boolean): void { + // This can be removed once enough users have run a version of Element with + // this migration. A couple of months after its release should be sufficient + // (so around October 2024). + // The consequences of missing the migration are only that URL previews will + // be disabled in E2EE rooms. + SettingsStore.migrateURLPreviewsE2EE(isFreshLogin); + // Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and // add a comment to note when it can be removed. return; diff --git a/test/settings/SettingsStore-test.ts b/test/settings/SettingsStore-test.ts index 64c63d1b56..6b2a4a6d6a 100644 --- a/test/settings/SettingsStore-test.ts +++ b/test/settings/SettingsStore-test.ts @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ClientEvent, MatrixClient, Room, SyncState } from "matrix-js-sdk"; +import { localstorage } from "modernizr"; + import BasePlatform from "../../src/BasePlatform"; import SdkConfig from "../../src/SdkConfig"; import { SettingLevel } from "../../src/settings/SettingLevel"; import SettingsStore from "../../src/settings/SettingsStore"; -import { mockPlatformPeg } from "../test-utils"; +import { mkStubRoom, mockPlatformPeg, stubClient } from "../test-utils"; const TEST_DATA = [ { @@ -84,4 +87,65 @@ describe("SettingsStore", () => { expect(SettingsStore.getValueAt(SettingLevel.DEVICE, SETTING_NAME_WITH_CONFIG_OVERRIDE)).toBe(true); }); }); + + describe("runMigrations", () => { + let client: MatrixClient; + let room: Room; + let localStorageSetItemSpy: jest.SpyInstance; + let localStorageSetPromise: Promise; + + beforeEach(() => { + client = stubClient(); + room = mkStubRoom("!room:example.org", "Room", client); + room.getAccountData = jest.fn().mockReturnValue({ + getContent: jest.fn().mockReturnValue({ + urlPreviewsEnabled_e2ee: true, + }), + }); + client.getRooms = jest.fn().mockReturnValue([room]); + client.getRoom = jest.fn().mockReturnValue(room); + + localStorageSetPromise = new Promise((resolve) => { + localStorageSetItemSpy = jest + .spyOn(localStorage.__proto__, "setItem") + .mockImplementation(() => resolve()); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("migrates URL previews setting for e2ee rooms", async () => { + SettingsStore.runMigrations(false); + client.emit(ClientEvent.Sync, SyncState.Prepared, null); + + expect(room.getAccountData).toHaveBeenCalled(); + + await localStorageSetPromise; + + expect(localStorageSetItemSpy!).toHaveBeenCalledWith( + `mx_setting_urlPreviewsEnabled_e2ee_${room.roomId}`, + JSON.stringify({ value: true }), + ); + }); + + it("does not migrate e2ee URL previews on a fresh login", async () => { + SettingsStore.runMigrations(true); + client.emit(ClientEvent.Sync, SyncState.Prepared, null); + + expect(room.getAccountData).not.toHaveBeenCalled(); + }); + + it("does not migrate if the device is flagged as migrated", async () => { + jest.spyOn(localStorage.__proto__, "getItem").mockImplementation((key: unknown): string | undefined => { + if (key === "url_previews_e2ee_migration_done") return JSON.stringify({ value: true }); + return undefined; + }); + SettingsStore.runMigrations(false); + client.emit(ClientEvent.Sync, SyncState.Prepared, null); + + expect(room.getAccountData).not.toHaveBeenCalled(); + }); + }); });