mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
Merge pull request #1027 from vector-im/windels/calls-feature-flag
Put calls support behind a feature flag
This commit is contained in:
commit
df05f40de7
@ -158,7 +158,7 @@ export class RootViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
_showSessionLoader(sessionId) {
|
||||
const client = new Client(this.platform);
|
||||
const client = new Client(this.platform, this.features);
|
||||
client.startWithExistingSession(sessionId);
|
||||
this._setSection(() => {
|
||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||
|
@ -30,6 +30,7 @@ import type {Navigation} from "./navigation/Navigation";
|
||||
import type {SegmentType} from "./navigation/index";
|
||||
import type {IURLRouter} from "./navigation/URLRouter";
|
||||
import type { ITimeFormatter } from "../platform/types/types";
|
||||
import type { FeatureSet } from "../features";
|
||||
|
||||
export type Options<T extends object = SegmentType> = {
|
||||
platform: Platform;
|
||||
@ -37,6 +38,7 @@ export type Options<T extends object = SegmentType> = {
|
||||
urlRouter: IURLRouter<T>;
|
||||
navigation: Navigation<T>;
|
||||
emitChange?: (params: any) => void;
|
||||
features: FeatureSet
|
||||
}
|
||||
|
||||
|
||||
@ -142,6 +144,10 @@ export class ViewModel<N extends object = SegmentType, O extends Options<N> = Op
|
||||
return this._options.urlRouter;
|
||||
}
|
||||
|
||||
get features(): FeatureSet {
|
||||
return this._options.features;
|
||||
}
|
||||
|
||||
get navigation(): Navigation<N> {
|
||||
// typescript needs a little help here
|
||||
return this._options.navigation as unknown as Navigation<N>;
|
||||
|
@ -55,7 +55,7 @@ export class LoginViewModel extends ViewModel<SegmentType, Options> {
|
||||
const {ready, defaultHomeserver, loginToken} = options;
|
||||
this._ready = ready;
|
||||
this._loginToken = loginToken;
|
||||
this._client = new Client(this.platform);
|
||||
this._client = new Client(this.platform, this.features);
|
||||
this._homeserver = defaultHomeserver;
|
||||
this._initViewModels();
|
||||
}
|
||||
|
@ -130,10 +130,12 @@ export class SessionViewModel extends ViewModel {
|
||||
|
||||
start() {
|
||||
this._sessionStatusViewModel.start();
|
||||
if (this.features.calls) {
|
||||
this._client.session.callHandler.loadCalls("m.ring");
|
||||
// TODO: only do this when opening the room
|
||||
this._client.session.callHandler.loadCalls("m.prompt");
|
||||
}
|
||||
}
|
||||
|
||||
get activeMiddleViewModel() {
|
||||
return (
|
||||
|
@ -50,6 +50,9 @@ export class RoomViewModel extends ErrorReportViewModel {
|
||||
}
|
||||
|
||||
_setupCallViewModel() {
|
||||
if (!this.features.calls) {
|
||||
return;
|
||||
}
|
||||
// pick call for this room with lowest key
|
||||
const calls = this.getOption("session").callHandler.calls;
|
||||
this._callObservable = new PickMapObservableValue(calls.filterValues(c => {
|
||||
@ -209,7 +212,7 @@ export class RoomViewModel extends ErrorReportViewModel {
|
||||
|
||||
_createTile(entry) {
|
||||
if (this._tileOptions) {
|
||||
const Tile = this._tileOptions.tileClassForEntry(entry);
|
||||
const Tile = this._tileOptions.tileClassForEntry(entry, this._tileOptions);
|
||||
if (Tile) {
|
||||
return new Tile(entry, this._tileOptions);
|
||||
}
|
||||
@ -421,6 +424,10 @@ export class RoomViewModel extends ErrorReportViewModel {
|
||||
|
||||
startCall() {
|
||||
return this.logAndCatch("RoomViewModel.startCall", async log => {
|
||||
if (!this.features.calls) {
|
||||
log.set("feature_disbled", true);
|
||||
return;
|
||||
}
|
||||
log.set("roomId", this._room.id);
|
||||
let localMedia;
|
||||
try {
|
||||
|
@ -34,7 +34,7 @@ export class TilesCollection extends BaseObservableList {
|
||||
}
|
||||
|
||||
_createTile(entry) {
|
||||
const Tile = this._tileOptions.tileClassForEntry(entry);
|
||||
const Tile = this._tileOptions.tileClassForEntry(entry, this._tileOptions);
|
||||
if (Tile) {
|
||||
return new Tile(entry, this._tileOptions);
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ export class BaseMessageTile extends SimpleTile {
|
||||
if (action?.shouldReplace || !this._replyTile) {
|
||||
this.disposeTracked(this._replyTile);
|
||||
const tileClassForEntry = this._options.tileClassForEntry;
|
||||
const ReplyTile = tileClassForEntry(replyEntry);
|
||||
const ReplyTile = tileClassForEntry(replyEntry, this._options);
|
||||
if (ReplyTile) {
|
||||
this._replyTile = new ReplyTile(replyEntry, this._options);
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export type Options = ViewModelOptions & {
|
||||
};
|
||||
export type TileConstructor = new (entry: TimelineEntry, options: Options) => ITile;
|
||||
|
||||
export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undefined {
|
||||
export function tileClassForEntry(entry: TimelineEntry, options: Options): TileConstructor | undefined {
|
||||
if (entry.isGap) {
|
||||
return GapTile;
|
||||
} else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
|
||||
@ -92,7 +92,7 @@ export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undef
|
||||
case "org.matrix.msc3401.call": {
|
||||
// if prevContent is present, it's an update to a call event, which we don't render
|
||||
// as the original event is updated through the call object which receive state event updates
|
||||
if (entry.stateKey && !entry.prevContent) {
|
||||
if (options.features.calls && entry.stateKey && !entry.prevContent) {
|
||||
return CallTile;
|
||||
}
|
||||
return undefined;
|
||||
|
70
src/domain/session/settings/FeaturesViewModel.ts
Normal file
70
src/domain/session/settings/FeaturesViewModel.ts
Normal file
@ -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<FeatureViewModel>;
|
||||
|
||||
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<SegmentType, FeatureOptions> {
|
||||
get enabled(): boolean {
|
||||
return this.features.isFeatureEnabled(this.getOption("feature"));
|
||||
}
|
||||
|
||||
async enableFeature(enabled: boolean): Promise<void> {
|
||||
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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -33,9 +33,11 @@ export class ToastCollectionViewModel extends ViewModel<SegmentType, Options> {
|
||||
constructor(options: Options) {
|
||||
super(options);
|
||||
const session = this.getOption("session");
|
||||
if (this.features.calls) {
|
||||
const callsObservableMap = session.callHandler.calls;
|
||||
this.track(callsObservableMap.subscribe(this));
|
||||
}
|
||||
}
|
||||
|
||||
async onAdd(_, call: GroupCall) {
|
||||
if (this._shouldShowNotification(call)) {
|
||||
|
50
src/features.ts
Normal file
50
src/features.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
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 type {SettingsStorage} from "./platform/web/dom/SettingsStorage";
|
||||
|
||||
export enum FeatureFlag {
|
||||
Calls = 1 << 0,
|
||||
}
|
||||
|
||||
export class FeatureSet {
|
||||
constructor(public readonly flags: number = 0) {}
|
||||
|
||||
withFeature(flag: FeatureFlag): FeatureSet {
|
||||
return new FeatureSet(this.flags | flag);
|
||||
}
|
||||
|
||||
withoutFeature(flag: FeatureFlag): FeatureSet {
|
||||
return new FeatureSet(this.flags ^ flag);
|
||||
}
|
||||
|
||||
isFeatureEnabled(flag: FeatureFlag): boolean {
|
||||
return (this.flags & flag) !== 0;
|
||||
}
|
||||
|
||||
get calls(): boolean {
|
||||
return this.isFeatureEnabled(FeatureFlag.Calls);
|
||||
}
|
||||
|
||||
static async load(settingsStorage: SettingsStorage): Promise<FeatureSet> {
|
||||
const flags = await settingsStorage.getInt("enabled_features") || 0;
|
||||
return new FeatureSet(flags);
|
||||
}
|
||||
|
||||
async store(settingsStorage: SettingsStorage): Promise<void> {
|
||||
await settingsStorage.setInt("enabled_features", this.flags);
|
||||
}
|
||||
}
|
@ -31,6 +31,7 @@ import {TokenLoginMethod} from "./login/TokenLoginMethod";
|
||||
import {SSOLoginHelper} from "./login/SSOLoginHelper";
|
||||
import {getDehydratedDevice} from "./e2ee/Dehydration.js";
|
||||
import {Registration} from "./registration/Registration";
|
||||
import {FeatureSet} from "../features";
|
||||
|
||||
export const LoadStatus = createEnum(
|
||||
"NotLoading",
|
||||
@ -53,7 +54,7 @@ export const LoginFailure = createEnum(
|
||||
);
|
||||
|
||||
export class Client {
|
||||
constructor(platform) {
|
||||
constructor(platform, features = new FeatureSet(0)) {
|
||||
this._platform = platform;
|
||||
this._sessionStartedByReconnector = false;
|
||||
this._status = new ObservableValue(LoadStatus.NotLoading);
|
||||
@ -68,6 +69,7 @@ export class Client {
|
||||
this._olmPromise = platform.loadOlm();
|
||||
this._workerPromise = platform.loadOlmWorker();
|
||||
this._accountSetup = undefined;
|
||||
this._features = features;
|
||||
}
|
||||
|
||||
createNewSessionId() {
|
||||
@ -278,6 +280,7 @@ export class Client {
|
||||
olmWorker,
|
||||
mediaRepository,
|
||||
platform: this._platform,
|
||||
features: this._features
|
||||
});
|
||||
await this._session.load(log);
|
||||
if (dehydratedDevice) {
|
||||
|
@ -74,6 +74,7 @@ export class DeviceMessageHandler {
|
||||
}
|
||||
|
||||
async afterSyncCompleted(decryptionResults, deviceTracker, hsApi, log) {
|
||||
if (this._callHandler) {
|
||||
// if we don't have a device, we need to fetch the device keys the message claims
|
||||
// and check the keys, and we should only do network requests during
|
||||
// sync processing in the afterSyncCompleted step.
|
||||
@ -100,6 +101,7 @@ export class DeviceMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyncPreparation {
|
||||
constructor(olmDecryptChanges, newRoomKeys) {
|
||||
|
@ -54,7 +54,7 @@ const PUSHER_KEY = "pusher";
|
||||
|
||||
export class Session {
|
||||
// sessionInfo contains deviceId, userId and homeserver
|
||||
constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) {
|
||||
constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository, features}) {
|
||||
this._platform = platform;
|
||||
this._storage = storage;
|
||||
this._hsApi = hsApi;
|
||||
@ -75,36 +75,7 @@ export class Session {
|
||||
};
|
||||
this._roomsBeingCreated = new ObservableMap();
|
||||
this._user = new User(sessionInfo.userId);
|
||||
this._callHandler = new CallHandler({
|
||||
clock: this._platform.clock,
|
||||
random: this._platform.random,
|
||||
hsApi: this._hsApi,
|
||||
encryptDeviceMessage: async (roomId, userId, deviceId, message, log) => {
|
||||
if (!this._deviceTracker || !this._olmEncryption) {
|
||||
log.set("encryption_disabled", true);
|
||||
return;
|
||||
}
|
||||
const device = await log.wrap("get device key", async log => {
|
||||
const device = this._deviceTracker.deviceForId(userId, deviceId, this._hsApi, log);
|
||||
if (!device) {
|
||||
log.set("not_found", true);
|
||||
}
|
||||
return device;
|
||||
});
|
||||
if (device) {
|
||||
const encryptedMessages = await this._olmEncryption.encrypt(message.type, message.content, [device], this._hsApi, log);
|
||||
return encryptedMessages;
|
||||
}
|
||||
},
|
||||
storage: this._storage,
|
||||
webRTC: this._platform.webRTC,
|
||||
ownDeviceId: sessionInfo.deviceId,
|
||||
ownUserId: sessionInfo.userId,
|
||||
logger: this._platform.logger,
|
||||
forceTURN: false,
|
||||
});
|
||||
this._roomStateHandler = new RoomStateHandlerSet();
|
||||
this.observeRoomState(this._callHandler);
|
||||
this._deviceMessageHandler = new DeviceMessageHandler({storage, callHandler: this._callHandler});
|
||||
this._olm = olm;
|
||||
this._olmUtil = null;
|
||||
@ -132,6 +103,10 @@ export class Session {
|
||||
this._createRoomEncryption = this._createRoomEncryption.bind(this);
|
||||
this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this);
|
||||
this.needsKeyBackup = new ObservableValue(false);
|
||||
|
||||
if (features.calls) {
|
||||
this._setupCallHandler();
|
||||
}
|
||||
}
|
||||
|
||||
get fingerprintKey() {
|
||||
@ -154,6 +129,38 @@ export class Session {
|
||||
return this._callHandler;
|
||||
}
|
||||
|
||||
_setupCallHandler() {
|
||||
this._callHandler = new CallHandler({
|
||||
clock: this._platform.clock,
|
||||
random: this._platform.random,
|
||||
hsApi: this._hsApi,
|
||||
encryptDeviceMessage: async (roomId, userId, deviceId, message, log) => {
|
||||
if (!this._deviceTracker || !this._olmEncryption) {
|
||||
log.set("encryption_disabled", true);
|
||||
return;
|
||||
}
|
||||
const device = await log.wrap("get device key", async log => {
|
||||
const device = this._deviceTracker.deviceForId(userId, deviceId, this._hsApi, log);
|
||||
if (!device) {
|
||||
log.set("not_found", true);
|
||||
}
|
||||
return device;
|
||||
});
|
||||
if (device) {
|
||||
const encryptedMessages = await this._olmEncryption.encrypt(message.type, message.content, [device], this._hsApi, log);
|
||||
return encryptedMessages;
|
||||
}
|
||||
},
|
||||
storage: this._storage,
|
||||
webRTC: this._platform.webRTC,
|
||||
ownDeviceId: this._sessionInfo.deviceId,
|
||||
ownUserId: this._sessionInfo.userId,
|
||||
logger: this._platform.logger,
|
||||
forceTURN: false,
|
||||
});
|
||||
this.observeRoomState(this._callHandler);
|
||||
}
|
||||
|
||||
// called once this._e2eeAccount is assigned
|
||||
_setupEncryption() {
|
||||
// TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account
|
||||
@ -1008,6 +1015,7 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
import {FeatureSet} from "../features";
|
||||
export function tests() {
|
||||
function createStorageMock(session, pendingEvents = []) {
|
||||
return {
|
||||
@ -1051,7 +1059,8 @@ export function tests() {
|
||||
clock: {
|
||||
createTimeout: () => undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
features: new FeatureSet(0)
|
||||
});
|
||||
await session.load();
|
||||
let syncSet = false;
|
||||
|
@ -283,6 +283,10 @@ export class Platform {
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
document.location.reload();
|
||||
}
|
||||
|
||||
openFile(mimeType = null) {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("type", "file");
|
||||
|
@ -18,6 +18,8 @@ limitations under the License.
|
||||
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
|
||||
import {RootViewModel} from "../../domain/RootViewModel.js";
|
||||
import {createNavigation, createRouter} from "../../domain/navigation/index";
|
||||
import {FeatureSet} from "../../features";
|
||||
|
||||
// Don't use a default export here, as we use multiple entries during legacy build,
|
||||
// which does not support default exports,
|
||||
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
||||
@ -33,6 +35,7 @@ export async function main(platform) {
|
||||
// const request = recorder.request;
|
||||
// window.getBrawlFetchLog = () => recorder.log();
|
||||
await platform.init();
|
||||
const features = await FeatureSet.load(platform.settingsStorage);
|
||||
const navigation = createNavigation();
|
||||
platform.setNavigation(navigation);
|
||||
const urlRouter = createRouter({navigation, history: platform.history});
|
||||
@ -43,6 +46,7 @@ export async function main(platform) {
|
||||
// so we call it that in the view models
|
||||
urlRouter: urlRouter,
|
||||
navigation,
|
||||
features
|
||||
});
|
||||
await vm.load();
|
||||
platform.createAndMountRootView(vm);
|
||||
|
@ -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;
|
||||
|
@ -73,8 +73,10 @@ export class RoomView extends TemplateView {
|
||||
} else {
|
||||
const vm = this.value;
|
||||
const options = [];
|
||||
options.push(Menu.option(vm.i18n`Room details`, () => vm.openDetailsPanel()))
|
||||
options.push(Menu.option(vm.i18n`Start call`, () => vm.startCall()))
|
||||
options.push(Menu.option(vm.i18n`Room details`, () => vm.openDetailsPanel()));
|
||||
if (vm.features.calls) {
|
||||
options.push(Menu.option(vm.i18n`Start call`, () => vm.startCall()));
|
||||
}
|
||||
if (vm.canLeave) {
|
||||
options.push(Menu.option(vm.i18n`Leave room`, () => this._confirmToLeaveRoom()).setDestructive());
|
||||
}
|
||||
|
52
src/platform/web/ui/session/settings/FeaturesView.ts
Normal file
52
src/platform/web/ui/session/settings/FeaturesView.ts
Normal file
@ -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<FeaturesViewModel> {
|
||||
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<FeatureViewModel> {
|
||||
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)
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user