From 3d48209fb40e10a93caec5b0571a0b4cb4168f9b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Feb 2021 15:09:18 -0700 Subject: [PATCH 1/7] Initial proof of concept for settings structure --- src/settings/v3/AppSettings.ts | 48 ++++++++++++++++++++++++++++++ src/settings/v3/BuiltInSettings.ts | 36 ++++++++++++++++++++++ src/settings/v3/Categories.ts | 30 +++++++++++++++++++ src/settings/v3/CustomSettings.ts | 32 ++++++++++++++++++++ src/settings/v3/ISetting.ts | 20 +++++++++++++ src/settings/v3/Types.ts | 22 ++++++++++++++ 6 files changed, 188 insertions(+) create mode 100644 src/settings/v3/AppSettings.ts create mode 100644 src/settings/v3/BuiltInSettings.ts create mode 100644 src/settings/v3/Categories.ts create mode 100644 src/settings/v3/CustomSettings.ts create mode 100644 src/settings/v3/ISetting.ts create mode 100644 src/settings/v3/Types.ts diff --git a/src/settings/v3/AppSettings.ts b/src/settings/v3/AppSettings.ts new file mode 100644 index 00000000000..a33bf8be2ea --- /dev/null +++ b/src/settings/v3/AppSettings.ts @@ -0,0 +1,48 @@ +/* + * 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, Settings, SettingType} from "./Types"; +import {RoomListSettings} from "./Categories"; + +export type SettingMap = { + [P in T]: P; +}; + +// We end up using this for everything, but hide available properties by +const AllSettingsMap: SettingMap = Object.keys(Settings).reduce((p, c) => { + p[c] = c; + return p; +}, {}) as SettingMap; + + +export const S = { + ...AllSettingsMap, + RoomList: AllSettingsMap as SettingMap, +}; + +function getValue(id: K): SettingType { + if (id === S.Breadcrumbs) { + return ['test']; + } else if (id === S.ShowReadReceipts) { + return false; + } else if (id === S["Video.TestDevice"]) { + return "ok"; + } + return null; +} + +const test1 = getValue(S.Breadcrumbs); +const test2 = getValue(S.RoomList.Breadcrumbs); diff --git a/src/settings/v3/BuiltInSettings.ts b/src/settings/v3/BuiltInSettings.ts new file mode 100644 index 00000000000..babb5dab015 --- /dev/null +++ b/src/settings/v3/BuiltInSettings.ts @@ -0,0 +1,36 @@ +/* + * 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 {ISetting} from "./ISetting"; + +export class BuiltInSettings { + public static readonly Breadcrumbs: ISetting = { + default: [], + description: "testing 1", + }; + public static readonly ShowReadReceipts: ISetting = { + default: true, + description: "testing 2", + }; + public static readonly ['Video.TestDevice']: ISetting = { + default: "hello", + description: "testing 3", + }; + + protected constructor() { + // readonly class + } +} diff --git a/src/settings/v3/Categories.ts b/src/settings/v3/Categories.ts new file mode 100644 index 00000000000..ddcc5312e44 --- /dev/null +++ b/src/settings/v3/Categories.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. + */ + +import {SettingID} from "./Types"; + +// This exists because the TS rules for generics prevent us from using a pipe +// character unless it is followed by a type. Because these categories could +// be appended to over time, we want the equivalent of a trailing comma to make +// merge conflicts easier to resolve. We do this by just defining the last +// type in the chain to be this placeholder, which will never show up in the +// final type because it breaks our code style guidelines and thus is an invalid +// setting (also we'd be unlikely to pick this wording for a setting anyways). +type typeForEaseOfNewlines = 'will-be-excluded'; + +export type RoomListSettings = Extract; diff --git a/src/settings/v3/CustomSettings.ts b/src/settings/v3/CustomSettings.ts new file mode 100644 index 00000000000..63ffa135484 --- /dev/null +++ b/src/settings/v3/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/ISetting.ts b/src/settings/v3/ISetting.ts new file mode 100644 index 00000000000..cdb841bbc42 --- /dev/null +++ b/src/settings/v3/ISetting.ts @@ -0,0 +1,20 @@ +/* + * 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 interface ISetting { + default: T; + description: string; +} diff --git a/src/settings/v3/Types.ts b/src/settings/v3/Types.ts new file mode 100644 index 00000000000..d8c4b0e1ad2 --- /dev/null +++ b/src/settings/v3/Types.ts @@ -0,0 +1,22 @@ +/* + * 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 "./CustomSettings"; + +export const Settings = CustomSettings; // We use CustomSettings because it extends BuiltInSettings +export type SettingID = keyof Omit; +export type SettingDefinition = (typeof Settings)[K]; +export type SettingType = SettingDefinition['default']; From 0394c78d736fb6cd217e2b868a70197276d7dcb9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Feb 2021 16:20:40 -0700 Subject: [PATCH 2/7] Make remapping more sane to use --- src/settings/v3/AppSettings.ts | 23 +++++++++++++++++------ src/settings/v3/BuiltInSettings.ts | 2 +- src/settings/v3/Categories.ts | 27 ++++++++++++++++----------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/settings/v3/AppSettings.ts b/src/settings/v3/AppSettings.ts index a33bf8be2ea..2d85e121643 100644 --- a/src/settings/v3/AppSettings.ts +++ b/src/settings/v3/AppSettings.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import {SettingID, Settings, SettingType} from "./Types"; -import {RoomListSettings} from "./Categories"; +import { SettingID, Settings, SettingType } from "./Types"; +import { RoomListSettings, SettingsCategory } from "./Categories"; export type SettingMap = { [P in T]: P; @@ -27,14 +27,25 @@ const AllSettingsMap: SettingMap = Object.keys(Settings).reduce((p, c return p; }, {}) as SettingMap; +type MappedSettings = { [P in keyof T]: T[P] }; + +function remap(cat: T): MappedSettings { + return Object.entries(cat).reduce((p, [prop, mapped]) => { + // We cast to `any` because the compiler isn't smart enough to know what is going on here. + // What we're doing is essentially defining MappedSettings with keys from `cat`, mapping + // them to the definitions populated by AllSettingsMap so the typing magically works. + (p as any)[prop] = AllSettingsMap[mapped]; + return p; + }, {} as MappedSettings); +} export const S = { ...AllSettingsMap, - RoomList: AllSettingsMap as SettingMap, + RoomList: remap(RoomListSettings), }; function getValue(id: K): SettingType { - if (id === S.Breadcrumbs) { + if (id === S.RoomListBreadcrumbs) { return ['test']; } else if (id === S.ShowReadReceipts) { return false; @@ -44,5 +55,5 @@ function getValue(id: K): SettingType { return null; } -const test1 = getValue(S.Breadcrumbs); -const test2 = getValue(S.RoomList.Breadcrumbs); +const test1: string[] = getValue(S.RoomListBreadcrumbs); +const test2: string[] = getValue(S.RoomList.Breadcrumbs); diff --git a/src/settings/v3/BuiltInSettings.ts b/src/settings/v3/BuiltInSettings.ts index babb5dab015..8d39fd051de 100644 --- a/src/settings/v3/BuiltInSettings.ts +++ b/src/settings/v3/BuiltInSettings.ts @@ -17,7 +17,7 @@ import {ISetting} from "./ISetting"; export class BuiltInSettings { - public static readonly Breadcrumbs: ISetting = { + public static readonly RoomListBreadcrumbs: ISetting = { default: [], description: "testing 1", }; diff --git a/src/settings/v3/Categories.ts b/src/settings/v3/Categories.ts index ddcc5312e44..1d7c922d903 100644 --- a/src/settings/v3/Categories.ts +++ b/src/settings/v3/Categories.ts @@ -16,15 +16,20 @@ import {SettingID} from "./Types"; -// This exists because the TS rules for generics prevent us from using a pipe -// character unless it is followed by a type. Because these categories could -// be appended to over time, we want the equivalent of a trailing comma to make -// merge conflicts easier to resolve. We do this by just defining the last -// type in the chain to be this placeholder, which will never show up in the -// final type because it breaks our code style guidelines and thus is an invalid -// setting (also we'd be unlikely to pick this wording for a setting anyways). -type typeForEaseOfNewlines = 'will-be-excluded'; +export type SettingsCategory = Record; -export type RoomListSettings = Extract; +// 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. + +// Note: In order to make Settings.get() return the right type, use the syntax shown +// below: 'RoomListBreadcrumbs' as 'RoomListBreadcrumbs' - This maps the value to a +// setting name and forces the parent object's type to be of that type rather than +// string, which is why we don't type these categories to SettingsCategory here. + +export const RoomListSettings = { + Breadcrumbs: 'RoomListBreadcrumbs' as 'RoomListBreadcrumbs', +}; From 38d9cfdc2e216624d4fb395a7396cfd65a435253 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 20 Feb 2021 19:37:10 -0700 Subject: [PATCH 3/7] Omit mapped settings from the root settings type ...even if they are actually on the type --- src/settings/v3/AppSettings.ts | 58 ++++++++++++++++++++--------- src/settings/v3/Types.ts | 22 +++++++++-- src/utils/ts.ts | 67 ++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 src/utils/ts.ts diff --git a/src/settings/v3/AppSettings.ts b/src/settings/v3/AppSettings.ts index 2d85e121643..8fee62f6d01 100644 --- a/src/settings/v3/AppSettings.ts +++ b/src/settings/v3/AppSettings.ts @@ -14,38 +14,60 @@ * limitations under the License. */ -import { SettingID, Settings, SettingType } from "./Types"; +import { SettingID, AppSettings, SettingType } from "./Types"; import { RoomListSettings, SettingsCategory } from "./Categories"; +import { DeepFlatten, ValuesOf } from "../../utils/ts"; -export type SettingMap = { - [P in T]: P; +// 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. +export type SettingMap = { + [P in SettingID]: P; }; -// We end up using this for everything, but hide available properties by -const AllSettingsMap: SettingMap = Object.keys(Settings).reduce((p, c) => { +// This is our "All Settings Map" where we actually build the map defined by the SettingMap +// type we created above. +const AllSettingsMap = Object.keys(AppSettings).reduce((p, c) => { p[c] = c; return p; -}, {}) as SettingMap; +}, {}) as SettingMap; +// 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 Object.entries(cat).reduce((p, [prop, mapped]) => { - // We cast to `any` because the compiler isn't smart enough to know what is going on here. - // What we're doing is essentially defining MappedSettings with keys from `cat`, mapping - // them to the definitions populated by AllSettingsMap so the typing magically works. - (p as any)[prop] = AllSettingsMap[mapped]; - return p; - }, {} as MappedSettings); + return cat; } -export const S = { - ...AllSettingsMap, +// We define our mapped settings ahead of the global definition so it is easier to exclude settings +// which are mapped to categories. See the next comment block for more information on what the final +// object looks like. +const mappedSettings = { RoomList: remap(RoomListSettings), }; +// 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.RoomListBreadcrumbs) { + if (id === S.RoomList.Breadcrumbs) { return ['test']; } else if (id === S.ShowReadReceipts) { return false; @@ -55,5 +77,7 @@ function getValue(id: K): SettingType { return null; } +const inspect1 = AllSettingsMap; +const inspect2 = mappedSettings; const test1: string[] = getValue(S.RoomListBreadcrumbs); const test2: string[] = getValue(S.RoomList.Breadcrumbs); diff --git a/src/settings/v3/Types.ts b/src/settings/v3/Types.ts index d8c4b0e1ad2..72ef3bc6444 100644 --- a/src/settings/v3/Types.ts +++ b/src/settings/v3/Types.ts @@ -16,7 +16,23 @@ import {CustomSettings} from "./CustomSettings"; -export const Settings = CustomSettings; // We use CustomSettings because it extends BuiltInSettings -export type SettingID = keyof Omit; -export type SettingDefinition = (typeof Settings)[K]; +// 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 AppSettings = 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 AppSettings)[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']; diff --git a/src/utils/ts.ts b/src/utils/ts.ts new file mode 100644 index 00000000000..8328912503c --- /dev/null +++ b/src/utils/ts.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ + +// Dev note: Many of the types in this file reference an article which was written before recursive types +// were possible in TypeScript. Many of these should be re-reviewed. +// Article: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca +// TS Notes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types + +/** + * 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. +// 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; From efb30d1356705fb530762cf967cb6feab20a3b7e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 20 Feb 2021 19:45:31 -0700 Subject: [PATCH 4/7] Update docs --- src/settings/v3/AppSettings.ts | 16 ++++++++++------ src/utils/ts.ts | 20 ++++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/settings/v3/AppSettings.ts b/src/settings/v3/AppSettings.ts index 8fee62f6d01..e0a4a4e7812 100644 --- a/src/settings/v3/AppSettings.ts +++ b/src/settings/v3/AppSettings.ts @@ -47,8 +47,12 @@ function remap(cat: T): MappedSettings { } // We define our mapped settings ahead of the global definition so it is easier to exclude settings -// which are mapped to categories. See the next comment block for more information on what the final -// object looks like. +// 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), }; @@ -77,7 +81,7 @@ function getValue(id: K): SettingType { return null; } -const inspect1 = AllSettingsMap; -const inspect2 = mappedSettings; -const test1: string[] = getValue(S.RoomListBreadcrumbs); -const test2: string[] = getValue(S.RoomList.Breadcrumbs); +// const inspect1 = AllSettingsMap; +// const inspect2 = mappedSettings; +// const test1: string[] = getValue(S.RoomListBreadcrumbs); +// const test2: string[] = getValue(S.RoomList.Breadcrumbs); diff --git a/src/utils/ts.ts b/src/utils/ts.ts index 8328912503c..330538aad11 100644 --- a/src/utils/ts.ts +++ b/src/utils/ts.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -// Dev note: Many of the types in this file reference an article which was written before recursive types -// were possible in TypeScript. Many of these should be re-reviewed. -// Article: https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca -// TS Notes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types - /** * 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 @@ -30,7 +25,9 @@ export type ValuesOf = T[keyof T]; * 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]; +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. @@ -43,10 +40,17 @@ export type ObjectValuesOf = Exclude = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; +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. +// 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; From c315dd23b52f7ec3d1c0d29f595e4f791479e732 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 21 Feb 2021 21:08:58 -0700 Subject: [PATCH 5/7] Define feature flags to test --- src/settings/v3/AllSettings.ts | 30 ++++++++++++ src/settings/v3/AppSettings.ts | 45 ++++++----------- src/settings/v3/BuiltInSettings.ts | 77 ++++++++++++++++++++++++++---- src/settings/v3/Categories.ts | 22 ++++++--- src/settings/v3/ISetting.ts | 35 +++++++++++++- src/utils/ts.ts | 2 +- 6 files changed, 161 insertions(+), 50 deletions(-) create mode 100644 src/settings/v3/AllSettings.ts diff --git a/src/settings/v3/AllSettings.ts b/src/settings/v3/AllSettings.ts new file mode 100644 index 00000000000..e2698dbf697 --- /dev/null +++ b/src/settings/v3/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 { AppSettings, 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(AppSettings).reduce((p, c) => { + p[c] = c; + return p; +}, {}) as SettingMap; diff --git a/src/settings/v3/AppSettings.ts b/src/settings/v3/AppSettings.ts index e0a4a4e7812..ad5642c1ad5 100644 --- a/src/settings/v3/AppSettings.ts +++ b/src/settings/v3/AppSettings.ts @@ -14,22 +14,9 @@ * limitations under the License. */ -import { SettingID, AppSettings, SettingType } from "./Types"; -import { RoomListSettings, SettingsCategory } from "./Categories"; +import { LabsFeatures, SettingsCategory } from "./Categories"; import { DeepFlatten, ValuesOf } from "../../utils/ts"; - -// 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. -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. -const AllSettingsMap = Object.keys(AppSettings).reduce((p, c) => { - p[c] = c; - return p; -}, {}) as SettingMap; +import { AllSettingsMap } from "./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 @@ -54,7 +41,8 @@ function remap(cat: T): MappedSettings { // 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), + //RoomList: remap(RoomListSettings), + Features: remap(LabsFeatures), }; // Finally, this is our accessor for setting IDs. Yes, code can use the setting IDs as strings, @@ -70,18 +58,13 @@ export const S = { ...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; -} - -// const inspect1 = AllSettingsMap; -// const inspect2 = mappedSettings; -// const test1: string[] = getValue(S.RoomListBreadcrumbs); -// const test2: string[] = getValue(S.RoomList.Breadcrumbs); +// 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/BuiltInSettings.ts b/src/settings/v3/BuiltInSettings.ts index 8d39fd051de..d91623caf55 100644 --- a/src/settings/v3/BuiltInSettings.ts +++ b/src/settings/v3/BuiltInSettings.ts @@ -14,20 +14,77 @@ * limitations under the License. */ -import {ISetting} from "./ISetting"; +import { CommonLevels, ISetting } from "./ISetting"; +import { _td } from "../../languageHandler"; export class BuiltInSettings { - public static readonly RoomListBreadcrumbs: ISetting = { - default: [], - description: "testing 1", + public static readonly ["feature_latex_maths"]: ISetting = { + default: false, + displayName: _td("Render LaTeX maths in messages"), + levels: CommonLevels.LabsFeature, }; - public static readonly ShowReadReceipts: ISetting = { - default: true, - description: "testing 2", + 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 ['Video.TestDevice']: ISetting = { - default: "hello", - description: "testing 3", + 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() { diff --git a/src/settings/v3/Categories.ts b/src/settings/v3/Categories.ts index 1d7c922d903..182a2140751 100644 --- a/src/settings/v3/Categories.ts +++ b/src/settings/v3/Categories.ts @@ -15,6 +15,7 @@ */ import {SettingID} from "./Types"; +import { AllSettingsMap } from "./AllSettings"; export type SettingsCategory = Record; @@ -25,11 +26,18 @@ export type SettingsCategory = Record; // and hope that the remap() function in AppSettings.ts will error if someone creates // an invalid setting map. -// Note: In order to make Settings.get() return the right type, use the syntax shown -// below: 'RoomListBreadcrumbs' as 'RoomListBreadcrumbs' - This maps the value to a -// setting name and forces the parent object's type to be of that type rather than -// string, which is why we don't type these categories to SettingsCategory here. - -export const RoomListSettings = { - Breadcrumbs: 'RoomListBreadcrumbs' as 'RoomListBreadcrumbs', +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/ISetting.ts b/src/settings/v3/ISetting.ts index cdb841bbc42..31dc2a37def 100644 --- a/src/settings/v3/ISetting.ts +++ b/src/settings/v3/ISetting.ts @@ -14,7 +14,40 @@ * 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; - description: string; + + /** + * 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 index 330538aad11..02c95e1bb82 100644 --- a/src/utils/ts.ts +++ b/src/utils/ts.ts @@ -33,7 +33,7 @@ export type NonObjectKeysOf = { * 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>; +export type ObjectValuesOf = Exclude, object>, never>, Array>; /** * As the name implies, this converts a union (A | B) to an intersection (A & B). From 4b61ba2b09690d99ecd3e5bba5ec07b4bfff2310 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 21 Feb 2021 21:12:54 -0700 Subject: [PATCH 6/7] Restructure --- src/settings/v3/AppSettings.ts | 4 ++-- src/settings/v3/{ => maps}/AllSettings.ts | 4 ++-- src/settings/v3/{ => maps}/Categories.ts | 2 +- src/settings/v3/{Types.ts => maps/types.ts} | 8 ++++---- src/settings/v3/{ => registry}/BuiltInSettings.ts | 4 +++- src/settings/v3/{ => registry}/CustomSettings.ts | 0 src/settings/v3/{ => registry}/ISetting.ts | 0 7 files changed, 12 insertions(+), 10 deletions(-) rename src/settings/v3/{ => maps}/AllSettings.ts (88%) rename src/settings/v3/{ => maps}/Categories.ts (98%) rename src/settings/v3/{Types.ts => maps/types.ts} (86%) rename src/settings/v3/{ => registry}/BuiltInSettings.ts (96%) rename src/settings/v3/{ => registry}/CustomSettings.ts (100%) rename src/settings/v3/{ => registry}/ISetting.ts (100%) diff --git a/src/settings/v3/AppSettings.ts b/src/settings/v3/AppSettings.ts index ad5642c1ad5..736d8c9de84 100644 --- a/src/settings/v3/AppSettings.ts +++ b/src/settings/v3/AppSettings.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { LabsFeatures, SettingsCategory } from "./Categories"; +import { LabsFeatures, SettingsCategory } from "./maps/Categories"; import { DeepFlatten, ValuesOf } from "../../utils/ts"; -import { AllSettingsMap } from "./AllSettings"; +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 diff --git a/src/settings/v3/AllSettings.ts b/src/settings/v3/maps/AllSettings.ts similarity index 88% rename from src/settings/v3/AllSettings.ts rename to src/settings/v3/maps/AllSettings.ts index e2698dbf697..ea7b5148799 100644 --- a/src/settings/v3/AllSettings.ts +++ b/src/settings/v3/maps/AllSettings.ts @@ -16,7 +16,7 @@ // 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 { AppSettings, SettingID } from "./Types"; +import { RawAppSettings, SettingID } from "./types"; export type SettingMap = { [P in SettingID]: P; @@ -24,7 +24,7 @@ export type SettingMap = { // 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(AppSettings).reduce((p, c) => { +export const AllSettingsMap = Object.keys(RawAppSettings).reduce((p, c) => { p[c] = c; return p; }, {}) as SettingMap; diff --git a/src/settings/v3/Categories.ts b/src/settings/v3/maps/Categories.ts similarity index 98% rename from src/settings/v3/Categories.ts rename to src/settings/v3/maps/Categories.ts index 182a2140751..740468eea53 100644 --- a/src/settings/v3/Categories.ts +++ b/src/settings/v3/maps/Categories.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {SettingID} from "./Types"; +import {SettingID} from "./types"; import { AllSettingsMap } from "./AllSettings"; export type SettingsCategory = Record; diff --git a/src/settings/v3/Types.ts b/src/settings/v3/maps/types.ts similarity index 86% rename from src/settings/v3/Types.ts rename to src/settings/v3/maps/types.ts index 72ef3bc6444..777cac0e57d 100644 --- a/src/settings/v3/Types.ts +++ b/src/settings/v3/maps/types.ts @@ -14,22 +14,22 @@ * limitations under the License. */ -import {CustomSettings} from "./CustomSettings"; +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 AppSettings = CustomSettings; +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; +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 AppSettings)[K]; +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 diff --git a/src/settings/v3/BuiltInSettings.ts b/src/settings/v3/registry/BuiltInSettings.ts similarity index 96% rename from src/settings/v3/BuiltInSettings.ts rename to src/settings/v3/registry/BuiltInSettings.ts index d91623caf55..34d3dbd5484 100644 --- a/src/settings/v3/BuiltInSettings.ts +++ b/src/settings/v3/registry/BuiltInSettings.ts @@ -15,7 +15,9 @@ */ import { CommonLevels, ISetting } from "./ISetting"; -import { _td } from "../../languageHandler"; +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 = { diff --git a/src/settings/v3/CustomSettings.ts b/src/settings/v3/registry/CustomSettings.ts similarity index 100% rename from src/settings/v3/CustomSettings.ts rename to src/settings/v3/registry/CustomSettings.ts diff --git a/src/settings/v3/ISetting.ts b/src/settings/v3/registry/ISetting.ts similarity index 100% rename from src/settings/v3/ISetting.ts rename to src/settings/v3/registry/ISetting.ts From dce5e5577bbbe17564daca8c70809bdc1b6d25d8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 21 Feb 2021 21:50:43 -0700 Subject: [PATCH 7/7] Add framework for settings migration --- src/settings/v3/maps/types.ts | 11 +++++ src/settings/v3/migrator/Migration.ts | 30 ++++++++++++ src/settings/v3/migrator/Migrator.ts | 59 +++++++++++++++++++++++ src/settings/v3/migrator/NameMigration.ts | 32 ++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 src/settings/v3/migrator/Migration.ts create mode 100644 src/settings/v3/migrator/Migrator.ts create mode 100644 src/settings/v3/migrator/NameMigration.ts diff --git a/src/settings/v3/maps/types.ts b/src/settings/v3/maps/types.ts index 777cac0e57d..17ae0726dd8 100644 --- a/src/settings/v3/maps/types.ts +++ b/src/settings/v3/maps/types.ts @@ -36,3 +36,14 @@ export type SettingDefinition = (typeof RawAppSettings)[K]; // 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; + } +}