Merge commit from fork

and migrate existing account-level settings once-only for
existing sessions.
This commit is contained in:
David Baker 2024-08-06 10:54:11 +01:00 committed by GitHub
parent 5dda51f95c
commit c7bbc1c045
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 13 deletions

View file

@ -102,7 +102,7 @@ dis.register((payload) => {
// If we unset the client and the component is updated, the render will fail and unmount everything. // 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) // (The module dialog closes and fires a `aria_unhide_main_app` that will trigger a re-render)
stopMatrixClient(false); 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 ? // 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. // The module api could use it to display a message to the user.
logger.warn("Failed to overwrite login", e); logger.warn("Failed to overwrite login", e);
@ -208,6 +208,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
guest: true, guest: true,
}, },
true, true,
false,
).then(() => true); ).then(() => true);
} }
const success = await restoreFromLocalStorage({ const success = await restoreFromLocalStorage({
@ -465,6 +466,7 @@ function registerAsGuest(hsUrl: string, isUrl?: string, defaultDeviceDisplayName
guest: true, guest: true,
}, },
true, true,
true,
).then(() => true); ).then(() => true);
}, },
(err) => { (err) => {
@ -610,6 +612,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
freshLogin: freshLogin, freshLogin: freshLogin,
}, },
false, false,
false,
); );
return true; return true;
} else { } else {
@ -663,7 +666,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
logger.log("Pickle key not created"); logger.log("Pickle key not created");
} }
return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true); return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true, true);
} }
/** /**
@ -700,7 +703,7 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise<M
(await PlatformPeg.get()?.getPickleKey(credentials.userId, credentials.deviceId)) ?? undefined; (await PlatformPeg.get()?.getPickleKey(credentials.userId, credentials.deviceId)) ?? undefined;
} }
return doSetLoggedIn(credentials, overwrite); return doSetLoggedIn(credentials, overwrite, false);
} }
/** /**
@ -746,12 +749,17 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis
* optionally clears localstorage, persists new credentials * optionally clears localstorage, persists new credentials
* to localstorage, starts the new client. * to localstorage, starts the new client.
* *
* @param {IMatrixClientCreds} credentials * @param {IMatrixClientCreds} credentials The credentials to use
* @param {Boolean} clearStorageEnabled * @param {Boolean} clearStorageEnabled True to clear storage before starting the new client
* @param {Boolean} isFreshLogin True if this is a fresh login, false if it is previous session being restored
* *
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/ */
async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean): Promise<MatrixClient> { async function doSetLoggedIn(
credentials: IMatrixClientCreds,
clearStorageEnabled: boolean,
isFreshLogin: boolean,
): Promise<MatrixClient> {
checkSessionLock(); checkSessionLock();
credentials.guest = Boolean(credentials.guest); credentials.guest = Boolean(credentials.guest);
@ -840,6 +848,9 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable
clientPegOpts.rustCryptoStoreKey?.fill(0); clientPegOpts.rustCryptoStoreKey?.fill(0);
} }
// Run the migrations after the MatrixClientPeg has been assigned
SettingsStore.runMigrations(isFreshLogin);
return client; return client;
} }
@ -1020,9 +1031,6 @@ async function startMatrixClient(
checkSessionLock(); checkSessionLock();
// Run the migrations after the MatrixClientPeg has been assigned
SettingsStore.runMigrations();
// This needs to be started after crypto is set up // This needs to be started after crypto is set up
DeviceListener.sharedInstance().start(client); DeviceListener.sharedInstance().start(client);
// Similarly, don't start sending presence updates until we've started // Similarly, don't start sending presence updates until we've started
@ -1165,5 +1173,6 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
userId, userId,
}, },
true, true,
false,
); );
}; };

View file

@ -101,7 +101,7 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
( (
<SettingsFlag <SettingsFlag
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"} name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
level={SettingLevel.ROOM_ACCOUNT} level={SettingLevel.ROOM_DEVICE}
roomId={roomId} roomId={roomId}
/> />
); );

View file

@ -900,7 +900,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
controller: new UIFeatureController(UIFeature.URLPreviews), controller: new UIFeatureController(UIFeature.URLPreviews),
}, },
"urlPreviewsEnabled_e2ee": { "urlPreviewsEnabled_e2ee": {
supportedLevels: [SettingLevel.ROOM_DEVICE, SettingLevel.ROOM_ACCOUNT], supportedLevels: [SettingLevel.ROOM_DEVICE],
displayName: { displayName: {
"room-account": _td("settings|inline_url_previews_room_account"), "room-account": _td("settings|inline_url_previews_room_account"),
}, },

View file

@ -17,6 +17,7 @@ limitations under the License.
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { ClientEvent, SyncState } from "matrix-js-sdk/src/matrix";
import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler"; import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler";
import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler"; import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler";
@ -36,6 +37,7 @@ import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayl
import { Action } from "../dispatcher/actions"; import { Action } from "../dispatcher/actions";
import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler"; import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler";
import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
import { MatrixClientPeg } from "../MatrixClientPeg";
// Convert the settings to easier to manage objects for the handlers // Convert the settings to easier to manage objects for the handlers
const defaultSettings: Record<string, any> = {}; const defaultSettings: Record<string, any> = {};
@ -637,10 +639,61 @@ export default class SettingsStore {
return null; 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<void> {
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<void> => {
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. * 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 // 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. // add a comment to note when it can be removed.
return; return;

View file

@ -14,11 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ClientEvent, MatrixClient, Room, SyncState } from "matrix-js-sdk";
import { localstorage } from "modernizr";
import BasePlatform from "../../src/BasePlatform"; import BasePlatform from "../../src/BasePlatform";
import SdkConfig from "../../src/SdkConfig"; import SdkConfig from "../../src/SdkConfig";
import { SettingLevel } from "../../src/settings/SettingLevel"; import { SettingLevel } from "../../src/settings/SettingLevel";
import SettingsStore from "../../src/settings/SettingsStore"; import SettingsStore from "../../src/settings/SettingsStore";
import { mockPlatformPeg } from "../test-utils"; import { mkStubRoom, mockPlatformPeg, stubClient } from "../test-utils";
const TEST_DATA = [ const TEST_DATA = [
{ {
@ -84,4 +87,65 @@ describe("SettingsStore", () => {
expect(SettingsStore.getValueAt(SettingLevel.DEVICE, SETTING_NAME_WITH_CONFIG_OVERRIDE)).toBe(true); 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<void>;
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();
});
});
}); });