diff --git a/src/domain/session/settings/FeaturesViewModel.ts b/src/domain/session/settings/FeaturesViewModel.ts new file mode 100644 index 00000000..69018bd2 --- /dev/null +++ b/src/domain/session/settings/FeaturesViewModel.ts @@ -0,0 +1,70 @@ +/* +Copyright 2020 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 {ViewModel} from "../../ViewModel"; +import type {Options as BaseOptions} from "../../ViewModel"; +import {FeatureFlag, FeatureSet} from "../../../features"; +import type {SegmentType} from "../../navigation/index"; + +export class FeaturesViewModel extends ViewModel { + public readonly featureViewModels: ReadonlyArray; + + constructor(options) { + super(options); + this.featureViewModels = [ + new FeatureViewModel(this.childOptions({ + name: this.i18n`Audio/video calls (experimental)`, + description: this.i18n`Allows starting and participating in A/V calls compatible with Element Call (MSC3401). Look for the start call option in the room menu ((...) in the right corner) to start a call.`, + feature: FeatureFlag.Calls + })), + ]; + } +} + +type FeatureOptions = BaseOptions & { + feature: FeatureFlag, + description: string, + name: string +}; + +export class FeatureViewModel extends ViewModel { + get enabled(): boolean { + return this.features.isFeatureEnabled(this.getOption("feature")); + } + + async enableFeature(enabled: boolean): Promise { + let newFeatures; + if (enabled) { + newFeatures = this.features.withFeature(this.getOption("feature")); + } else { + newFeatures = this.features.withoutFeature(this.getOption("feature")); + } + await newFeatures.store(this.platform.settingsStorage); + this.platform.restart(); + } + + get id(): string { + return `${this.getOption("feature")}`; + } + + get name(): string { + return this.getOption("name"); + } + + get description(): string { + return this.getOption("description"); + } +} diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 952c910b..f8420a53 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -16,6 +16,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel"; import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; +import {FeaturesViewModel} from "./FeaturesViewModel"; import {submitLogsFromSessionToDefaultServer} from "../../../domain/rageshake"; class PushNotificationStatus { @@ -53,6 +54,7 @@ export class SettingsViewModel extends ViewModel { this.pushNotifications = new PushNotificationStatus(); this._activeTheme = undefined; this._logsFeedbackMessage = undefined; + this._featuresViewModel = new FeaturesViewModel(this.childOptions()); } get _session() { @@ -125,6 +127,10 @@ export class SettingsViewModel extends ViewModel { return this._keyBackupViewModel; } + get featuresViewModel() { + return this._featuresViewModel; + } + get storageQuota() { return this._formatBytes(this._estimate?.quota); } diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index bfb585a3..be8c9970 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -283,6 +283,10 @@ export class Platform { } } + restart() { + document.location.reload(); + } + openFile(mimeType = null) { const input = document.createElement("input"); input.setAttribute("type", "file"); diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 7bea169e..fd8e69b0 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -751,6 +751,24 @@ a { margin-bottom: 0; } +.FeatureView { + display: flex; + gap: 8px; +} + +.FeaturesView ul { + list-style: none; + padding: 8px 16px; +} + +.FeaturesView input[type="checkbox"] { + align-self: start; +} + +.FeatureView h4 { + margin: 0; +} + .error { color: var(--error-color); font-weight: 600; diff --git a/src/platform/web/ui/session/settings/FeaturesView.ts b/src/platform/web/ui/session/settings/FeaturesView.ts new file mode 100644 index 00000000..625fc362 --- /dev/null +++ b/src/platform/web/ui/session/settings/FeaturesView.ts @@ -0,0 +1,52 @@ +/* +Copyright 2023 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 {TemplateView, TemplateBuilder} from "../../general/TemplateView"; +import {ViewNode} from "../../general/types"; +import {disableTargetCallback} from "../../general/utils"; +import type {FeaturesViewModel, FeatureViewModel} from "../../../../../domain/session/settings/FeaturesViewModel"; + +export class FeaturesView extends TemplateView { + render(t, vm: FeaturesViewModel): ViewNode { + return t.div({ + className: "FeaturesView", + }, [ + t.p("Enable experimental features here that are still in development. These are not yet ready for primetime, so expect bugs."), + // we don't use a binding/ListView because this is a static list + t.ul(vm.featureViewModels.map(vm => { + return t.li(t.view(new FeatureView(vm))); + })) + ]); + } +} + +class FeatureView extends TemplateView { + render(t, vm): ViewNode { + let id = `feature_${vm.id}`; + return t.div({className: "FeatureView"}, [ + t.input({ + type: "checkbox", + id, + checked: vm => vm.enabled, + onChange: evt => vm.enableFeature(evt.target.checked) + }), + t.div({class: "FeatureView_container"}, [ + t.h4(t.label({for: id}, vm.name)), + t.p(vm.description) + ]) + ]); + } +} diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index d7c48351..0d0d6941 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -17,6 +17,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; import {disableTargetCallback} from "../../general/utils"; import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js" +import {FeaturesView} from "./FeaturesView" export class SettingsView extends TemplateView { render(t, vm) { @@ -110,6 +111,12 @@ export class SettingsView extends TemplateView { logButtons.push(t.button({onClick: disableTargetCallback(() => vm.sendLogsToServer())}, `Submit logs to ${vm.logsServer}`)); } logButtons.push(t.button({onClick: () => vm.exportLogs()}, "Download logs")); + + settingNodes.push( + t.h3("Experimental features"), + t.view(new FeaturesView(vm.featuresViewModel)) + ); + settingNodes.push( t.h3("Application"), row(t, vm.i18n`Version`, version),