diff --git a/src/settings/v3/AppSettings.ts b/src/settings/v3/AppSettings.ts new file mode 100644 index 00000000000..736d8c9de84 --- /dev/null +++ b/src/settings/v3/AppSettings.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LabsFeatures, SettingsCategory } from "./maps/Categories"; +import { DeepFlatten, ValuesOf } from "../../utils/ts"; +import { AllSettingsMap } from "./maps/AllSettings"; + +// This looks useless in terms of code, and arguably it is, however it does an important job +// to enforce typechecking on the incoming map. All this definition does is allows us to use +// friendlier names for our settings by putting them into categories for dot exploration. For +// example, a hypothetical RoomListLayout setting might want to be recorded as RoomList.Layout, +// which is hard to do or ugly to represent in code (property names can't have dots, which means +// making them strings, which means our access looks like S["RoomList.Layout"] instead of a +// cleaner S.RoomList.Layout). By defining a SettingsCategory we are able to help make this +// mapping possible, and need to typecheck it for reasons explained in Categories.ts +// +// TLDR: We return the same thing because we're just typechecking our fancy map of setting values. +type MappedSettings = { [P in keyof T]: T[P] }; +function remap(cat: T): MappedSettings { + return cat; +} + +// We define our mapped settings ahead of the global definition so it is easier to exclude settings +// which are mapped to categories. We mostly want to do this to help developers use the right setting +// even if others are technically possible: for example, if we have S.RoomList.Layout then we don't +// want someone to accidentally use S["RoomList.Layout"] as their IDE might suggest. By defining the +// mapped types (S.RoomList.*) ahead of the definition, we can omit the mapped values from the top +// level definition. Inspecting the type of S or the Omit<> below should give a better idea of what +// is going on. +const mappedSettings = { + //RoomList: remap(RoomListSettings), + Features: remap(LabsFeatures), +}; + +// Finally, this is our accessor for setting IDs. Yes, code can use the setting IDs as strings, +// but that can conflict with some "find usages of..." tooling available in IDEs and GitHub. +// This is pretty much just a cheap way to continue using that tooling while also being descriptive +// in code. +// +// As for why we call this just "S": `S.Breadcrumbs` is the same number of characters that are needed +// for `"Breadcrumbs"` - we are actively trying to avoid making lines of code larger by optimizing for +// IDE tooling. The S denotes "Setting ID". +export const S = { + ...AllSettingsMap as Omit>>, + ...mappedSettings, +}; + +// function getValue(id: K): SettingType { +// if (id === S.RoomList.Breadcrumbs) { +// return ['test']; +// } else if (id === S.ShowReadReceipts) { +// return false; +// } else if (id === S["Video.TestDevice"]) { +// return "ok"; +// } +// return null; +// } diff --git a/src/settings/v3/maps/AllSettings.ts b/src/settings/v3/maps/AllSettings.ts new file mode 100644 index 00000000000..ea7b5148799 --- /dev/null +++ b/src/settings/v3/maps/AllSettings.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This is just to create a type which maps setting ID to setting ID. This might seem pointless, +// but it's an intermediary type to create an interface that Settings.get() can later use. +import { RawAppSettings, SettingID } from "./types"; + +export type SettingMap = { + [P in SettingID]: P; +}; + +// This is our "All Settings Map" where we actually build the map defined by the SettingMap +// type we created above. +export const AllSettingsMap = Object.keys(RawAppSettings).reduce((p, c) => { + p[c] = c; + return p; +}, {}) as SettingMap; diff --git a/src/settings/v3/maps/Categories.ts b/src/settings/v3/maps/Categories.ts new file mode 100644 index 00000000000..740468eea53 --- /dev/null +++ b/src/settings/v3/maps/Categories.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {SettingID} from "./types"; +import { AllSettingsMap } from "./AllSettings"; + +export type SettingsCategory = Record; + +// Note: None of the categories listed here are typed to be a SettingsCategory. This +// is because TypeScript wipes out our types, which makes the Settings.get() function +// return a union of all types instead of just the specified setting's type, making +// manual casting a requirement. Instead, we use TypeScript's implied interface support +// and hope that the remap() function in AppSettings.ts will error if someone creates +// an invalid setting map. + +export const LabsFeatures = { + Maths: AllSettingsMap.feature_latex_maths, + CommunitiesV2: AllSettingsMap.feature_communities_v2_prototypes, + NewSpinner: AllSettingsMap.feature_new_spinner, + MessagePinning: AllSettingsMap.feature_pinning, + CustomStatus: AllSettingsMap.feature_custom_status, + CustomTags: AllSettingsMap.feature_custom_tags, + StateCounters: AllSettingsMap.feature_state_counters, + ManyIntegManagers: AllSettingsMap.feature_many_integration_managers, + Mjolnir: AllSettingsMap.feature_mjolnir, + CustomThemes: AllSettingsMap.feature_custom_themes, + PreviewReactionsDMs: AllSettingsMap.feature_roomlist_preview_reactions_dms, + PreviewReactionsAll: AllSettingsMap.feature_roomlist_preview_reactions_all, + Dehydration: AllSettingsMap.feature_dehydration, +}; diff --git a/src/settings/v3/maps/types.ts b/src/settings/v3/maps/types.ts new file mode 100644 index 00000000000..17ae0726dd8 --- /dev/null +++ b/src/settings/v3/maps/types.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CustomSettings} from "../registry/CustomSettings"; + +// First let's define a variable which will help with the rest of this making sense. We just want +// an object which has all of the settings, custom and built-in, so we just map directly to the +// custom settings because they extend the built-in ones. +export const RawAppSettings = CustomSettings; + +// Next we want a type to represent all the setting names. This will be a type which is a union +// of all property names on the AppSettings object. We take off 'prototype' because we don't want +// that to be considered a setting. +export type SettingID = keyof Omit; + +// This just defines a type which references the `ISetting` for each setting ID. Essentially +// we're making a dynamic interface of AppSettings here, mapping the property names to ISetting +// types. +export type SettingDefinition = (typeof RawAppSettings)[K]; + +// Finally we can pull out the setting types by using the same dynamic interface trick from above: +// we map setting IDs (property names) to the type of the definition's `default` property. The value +// of the default can be whatever - we're just stripping the type off of it from the ISetting +// contracts. +export type SettingType = SettingDefinition['default']; + +/** + * The setting IDs which are "legacy". Only useful in the context of the application - if there are + * values which are not read here, they should be removed. Similarly, settings should only be added + * here if there is a need to reference them outside of SettingID. + * + * Typically used during migrations. + */ +export enum LegacySettingID { + +} diff --git a/src/settings/v3/migrator/Migration.ts b/src/settings/v3/migrator/Migration.ts new file mode 100644 index 00000000000..7d76c4706b7 --- /dev/null +++ b/src/settings/v3/migrator/Migration.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Represents a setting migration. + */ +export interface Migration { + /** + * The ID for the migration. Arbitrary string. + */ + id: string; + + /** + * Runs the migration. Resolves when complete. + */ + run(): Promise; +} diff --git a/src/settings/v3/migrator/Migrator.ts b/src/settings/v3/migrator/Migrator.ts new file mode 100644 index 00000000000..7f4c55c9e2e --- /dev/null +++ b/src/settings/v3/migrator/Migrator.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Migration } from "./Migration"; +import { iterableUnion } from "../../../utils/iterables"; +import { NameMigration } from "./NameMigration"; + +const LS_RUN_HISTORY = "mx_settings_migration_history"; + +export class Migrator { + private static migrations: Migration[] = [ + new NameMigration("test", "test2"), + ]; + + private constructor() { + // static only + } + + public static async run(): Promise { + console.log(`[Migrator] Starting migrations...`); + const lsFlagged = localStorage.getItem(LS_RUN_HISTORY); + const doneMigrations = new Set(); + if (lsFlagged) { + (JSON.parse(lsFlagged)).forEach(r => doneMigrations.add(r)); + } + + for (const migration of this.migrations) { + if (doneMigrations.has(migration.id)) { + console.log(`[Migrator] Skipping migration: ${migration.id}`); + continue; + } + console.log(`[Migrator] Running migration: ${migration.id}`); + await migration.run(); + doneMigrations.add(migration.id); + } + + const possibleIds = new Set(this.migrations.map(m => m.id)); + const union = Array.from(iterableUnion(possibleIds, doneMigrations)); + localStorage.setItem(LS_RUN_HISTORY, JSON.stringify(union)); + + console.log(`[Migrator] Done. ` + + `${possibleIds.size} complete, ` + + `${doneMigrations.size} recorded as done, ` + + `${union.length} persisted as complete`); + } +} diff --git a/src/settings/v3/migrator/NameMigration.ts b/src/settings/v3/migrator/NameMigration.ts new file mode 100644 index 00000000000..700d8d6f053 --- /dev/null +++ b/src/settings/v3/migrator/NameMigration.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Migration } from "./Migration"; +import { LegacySettingID, SettingID } from "../maps/types"; + +export class NameMigration implements Migration { + public constructor(private oldName: LegacySettingID, private newName: SettingID) { + } + + public get id(): string { + return `name: ${this.oldName}->${this.newName}`; + } + + public run(): Promise { + // TODO + return; + } +} diff --git a/src/settings/v3/registry/BuiltInSettings.ts b/src/settings/v3/registry/BuiltInSettings.ts new file mode 100644 index 00000000000..34d3dbd5484 --- /dev/null +++ b/src/settings/v3/registry/BuiltInSettings.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonLevels, ISetting } from "./ISetting"; +import { _td } from "../../../languageHandler"; + +// These are all the settings that the app itself defines. Every setting must be registered here. + +export class BuiltInSettings { + public static readonly ["feature_latex_maths"]: ISetting = { + default: false, + displayName: _td("Render LaTeX maths in messages"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_communities_v2_prototypes"]: ISetting = { + default: false, + displayName: _td( + "Communities v2 prototypes. Requires compatible homeserver. " + + "Highly experimental - use with caution.", + ), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_new_spinner"]: ISetting = { + default: false, + displayName: _td("New spinner design"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_pinning"]: ISetting = { + default: false, + displayName: _td("Message Pinning"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_custom_status"]: ISetting = { + default: false, + displayName: _td("Custom user status messages"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_custom_tags"]: ISetting = { + default: false, + displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_state_counters"]: ISetting = { + default: false, + displayName: _td("Render simple counters in room header"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_many_integration_managers"]: ISetting = { + default: false, + displayName: _td("Multiple integration managers"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_mjolnir"]: ISetting = { + default: false, + displayName: _td("Try out new ways to ignore people (experimental)"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_custom_themes"]: ISetting = { + default: false, + displayName: _td("Support adding custom themes"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_roomlist_preview_reactions_dms"]: ISetting = { + default: false, + displayName: _td("Show message previews for reactions in DMs"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_roomlist_preview_reactions_all"]: ISetting = { + default: false, + displayName: _td("Show message previews for reactions in all rooms"), + levels: CommonLevels.LabsFeature, + }; + public static readonly ["feature_dehydration"]: ISetting = { + default: false, + displayName: _td("Offline encrypted messaging using dehydrated devices"), + levels: CommonLevels.LabsFeature, + }; + + protected constructor() { + // readonly class + } +} diff --git a/src/settings/v3/registry/CustomSettings.ts b/src/settings/v3/registry/CustomSettings.ts new file mode 100644 index 00000000000..63ffa135484 --- /dev/null +++ b/src/settings/v3/registry/CustomSettings.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {BuiltInSettings} from "./BuiltInSettings"; + +export class CustomSettings extends BuiltInSettings { + // If you're building a fork which requires different/more settings, this is where + // you can define them. This file *should* remain forwards compatible for easier + // merges in the future. + // + // Note that if you want to override a setting then it would be done here. See + // the Settings.ts file for all the built-in settings, and how to define a setting. + // Do not edit Settings.ts in your fork: it will cause merge conflicts. + + private constructor() { + // this is a readonly class, so a constructor isn't needed. + super(); // we do need to fulfill basic language contracts though + } +} diff --git a/src/settings/v3/registry/ISetting.ts b/src/settings/v3/registry/ISetting.ts new file mode 100644 index 00000000000..31dc2a37def --- /dev/null +++ b/src/settings/v3/registry/ISetting.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum SettingLevel { + Device = "device", + RoomDevice = "room-device", + RoomAccount = "room-account", + Account = "account", + Config = "config", + Default = "default", +} + +export interface ISetting { + /** + * Default value for the setting. May be null. + */ + default: T; + + /** + * The display name for the setting. May be an object to denote the display name + * changes depending on the level. + */ + displayName?: string | { + [level in SettingLevel]?: string; + }; + + /** + * If not supported at each level, which levels the setting is supported at. Note + * that this array is ordered. The Default level will always be appended to the end. + */ + levels?: SettingLevel[]; +} + +export class CommonLevels { + public static readonly LabsFeature = [SettingLevel.Device, SettingLevel.Config]; + + private constructor() { + // readonly class + } +} diff --git a/src/utils/ts.ts b/src/utils/ts.ts new file mode 100644 index 00000000000..02c95e1bb82 --- /dev/null +++ b/src/utils/ts.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A simple type to pull out the value types for a given type. + * Source: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca + */ +export type ValuesOf = T[keyof T]; + +/** + * Gets all non-object keys from a type. This will take arrays, numbers, strings, etc (everything but + * objects) and return that as a type. + * Source: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca + */ +export type NonObjectKeysOf = { + [K in keyof T]: T[K] extends Array ? K : T[K] extends object ? never : K; +}[keyof T]; + +/** + * The inverse of NonObjectKeysOf - this pulls out *only* the object types. + * Source: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca + */ +export type ObjectValuesOf = Exclude, object>, never>, Array>; + +/** + * As the name implies, this converts a union (A | B) to an intersection (A & B). + * Source: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca + * Source: https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286 + */ +export type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; + +// These are setup for the DeepFlatten type below. It's not exactly pretty, but we get 9 levels of recursion +// out of this. As the article (source below) notes, this is not pretty and there might even be a better way +// to represent this with truly infinite recursion - in practice we probably don't need that level of generic +// support, leaving us with 9 levels for now. +// +// We should look at TS 4.1's Recursive Conditional Types to replace this mess: +// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types +// +// Source: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca +type DFBase = Pick> & UnionToIntersection; +type DF2 = T extends any ? DFBase>> : never; +type DF3 = T extends any ? DFBase>> : never; +type DF4 = T extends any ? DFBase>> : never; +type DF5 = T extends any ? DFBase>> : never; +type DF6 = T extends any ? DFBase>> : never; +type DF7 = T extends any ? DFBase>> : never; +type DF8 = T extends any ? DFBase>> : never; +type DF9 = T extends any ? DFBase> : never; + +/** + * Recursively flattens a type to a top level object. Conflicting property names will have their types + * intersected, though this should be avoided. Note that this does have an internal recursion limit: + * use this with relatively shallow types. + * Source: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca + */ +export type DeepFlatten = T extends any ? DFBase>> : never;