mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 11:35:04 +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) {
|
_showSessionLoader(sessionId) {
|
||||||
const client = new Client(this.platform);
|
const client = new Client(this.platform, this.features);
|
||||||
client.startWithExistingSession(sessionId);
|
client.startWithExistingSession(sessionId);
|
||||||
this._setSection(() => {
|
this._setSection(() => {
|
||||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||||
|
@ -30,6 +30,7 @@ import type {Navigation} from "./navigation/Navigation";
|
|||||||
import type {SegmentType} from "./navigation/index";
|
import type {SegmentType} from "./navigation/index";
|
||||||
import type {IURLRouter} from "./navigation/URLRouter";
|
import type {IURLRouter} from "./navigation/URLRouter";
|
||||||
import type { ITimeFormatter } from "../platform/types/types";
|
import type { ITimeFormatter } from "../platform/types/types";
|
||||||
|
import type { FeatureSet } from "../features";
|
||||||
|
|
||||||
export type Options<T extends object = SegmentType> = {
|
export type Options<T extends object = SegmentType> = {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
@ -37,6 +38,7 @@ export type Options<T extends object = SegmentType> = {
|
|||||||
urlRouter: IURLRouter<T>;
|
urlRouter: IURLRouter<T>;
|
||||||
navigation: Navigation<T>;
|
navigation: Navigation<T>;
|
||||||
emitChange?: (params: any) => void;
|
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;
|
return this._options.urlRouter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get features(): FeatureSet {
|
||||||
|
return this._options.features;
|
||||||
|
}
|
||||||
|
|
||||||
get navigation(): Navigation<N> {
|
get navigation(): Navigation<N> {
|
||||||
// typescript needs a little help here
|
// typescript needs a little help here
|
||||||
return this._options.navigation as unknown as Navigation<N>;
|
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;
|
const {ready, defaultHomeserver, loginToken} = options;
|
||||||
this._ready = ready;
|
this._ready = ready;
|
||||||
this._loginToken = loginToken;
|
this._loginToken = loginToken;
|
||||||
this._client = new Client(this.platform);
|
this._client = new Client(this.platform, this.features);
|
||||||
this._homeserver = defaultHomeserver;
|
this._homeserver = defaultHomeserver;
|
||||||
this._initViewModels();
|
this._initViewModels();
|
||||||
}
|
}
|
||||||
|
@ -130,10 +130,12 @@ export class SessionViewModel extends ViewModel {
|
|||||||
|
|
||||||
start() {
|
start() {
|
||||||
this._sessionStatusViewModel.start();
|
this._sessionStatusViewModel.start();
|
||||||
|
if (this.features.calls) {
|
||||||
this._client.session.callHandler.loadCalls("m.ring");
|
this._client.session.callHandler.loadCalls("m.ring");
|
||||||
// TODO: only do this when opening the room
|
// TODO: only do this when opening the room
|
||||||
this._client.session.callHandler.loadCalls("m.prompt");
|
this._client.session.callHandler.loadCalls("m.prompt");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get activeMiddleViewModel() {
|
get activeMiddleViewModel() {
|
||||||
return (
|
return (
|
||||||
|
@ -50,6 +50,9 @@ export class RoomViewModel extends ErrorReportViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setupCallViewModel() {
|
_setupCallViewModel() {
|
||||||
|
if (!this.features.calls) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// pick call for this room with lowest key
|
// pick call for this room with lowest key
|
||||||
const calls = this.getOption("session").callHandler.calls;
|
const calls = this.getOption("session").callHandler.calls;
|
||||||
this._callObservable = new PickMapObservableValue(calls.filterValues(c => {
|
this._callObservable = new PickMapObservableValue(calls.filterValues(c => {
|
||||||
@ -209,7 +212,7 @@ export class RoomViewModel extends ErrorReportViewModel {
|
|||||||
|
|
||||||
_createTile(entry) {
|
_createTile(entry) {
|
||||||
if (this._tileOptions) {
|
if (this._tileOptions) {
|
||||||
const Tile = this._tileOptions.tileClassForEntry(entry);
|
const Tile = this._tileOptions.tileClassForEntry(entry, this._tileOptions);
|
||||||
if (Tile) {
|
if (Tile) {
|
||||||
return new Tile(entry, this._tileOptions);
|
return new Tile(entry, this._tileOptions);
|
||||||
}
|
}
|
||||||
@ -421,6 +424,10 @@ export class RoomViewModel extends ErrorReportViewModel {
|
|||||||
|
|
||||||
startCall() {
|
startCall() {
|
||||||
return this.logAndCatch("RoomViewModel.startCall", async log => {
|
return this.logAndCatch("RoomViewModel.startCall", async log => {
|
||||||
|
if (!this.features.calls) {
|
||||||
|
log.set("feature_disbled", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.set("roomId", this._room.id);
|
log.set("roomId", this._room.id);
|
||||||
let localMedia;
|
let localMedia;
|
||||||
try {
|
try {
|
||||||
|
@ -34,7 +34,7 @@ export class TilesCollection extends BaseObservableList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_createTile(entry) {
|
_createTile(entry) {
|
||||||
const Tile = this._tileOptions.tileClassForEntry(entry);
|
const Tile = this._tileOptions.tileClassForEntry(entry, this._tileOptions);
|
||||||
if (Tile) {
|
if (Tile) {
|
||||||
return new Tile(entry, this._tileOptions);
|
return new Tile(entry, this._tileOptions);
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ export class BaseMessageTile extends SimpleTile {
|
|||||||
if (action?.shouldReplace || !this._replyTile) {
|
if (action?.shouldReplace || !this._replyTile) {
|
||||||
this.disposeTracked(this._replyTile);
|
this.disposeTracked(this._replyTile);
|
||||||
const tileClassForEntry = this._options.tileClassForEntry;
|
const tileClassForEntry = this._options.tileClassForEntry;
|
||||||
const ReplyTile = tileClassForEntry(replyEntry);
|
const ReplyTile = tileClassForEntry(replyEntry, this._options);
|
||||||
if (ReplyTile) {
|
if (ReplyTile) {
|
||||||
this._replyTile = new ReplyTile(replyEntry, this._options);
|
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 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) {
|
if (entry.isGap) {
|
||||||
return GapTile;
|
return GapTile;
|
||||||
} else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
|
} else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
|
||||||
@ -92,7 +92,7 @@ export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undef
|
|||||||
case "org.matrix.msc3401.call": {
|
case "org.matrix.msc3401.call": {
|
||||||
// if prevContent is present, it's an update to a call event, which we don't render
|
// 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
|
// 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 CallTile;
|
||||||
}
|
}
|
||||||
return undefined;
|
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 {ViewModel} from "../../ViewModel";
|
||||||
import {KeyBackupViewModel} from "./KeyBackupViewModel.js";
|
import {KeyBackupViewModel} from "./KeyBackupViewModel.js";
|
||||||
|
import {FeaturesViewModel} from "./FeaturesViewModel";
|
||||||
import {submitLogsFromSessionToDefaultServer} from "../../../domain/rageshake";
|
import {submitLogsFromSessionToDefaultServer} from "../../../domain/rageshake";
|
||||||
|
|
||||||
class PushNotificationStatus {
|
class PushNotificationStatus {
|
||||||
@ -53,6 +54,7 @@ export class SettingsViewModel extends ViewModel {
|
|||||||
this.pushNotifications = new PushNotificationStatus();
|
this.pushNotifications = new PushNotificationStatus();
|
||||||
this._activeTheme = undefined;
|
this._activeTheme = undefined;
|
||||||
this._logsFeedbackMessage = undefined;
|
this._logsFeedbackMessage = undefined;
|
||||||
|
this._featuresViewModel = new FeaturesViewModel(this.childOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
get _session() {
|
get _session() {
|
||||||
@ -125,6 +127,10 @@ export class SettingsViewModel extends ViewModel {
|
|||||||
return this._keyBackupViewModel;
|
return this._keyBackupViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get featuresViewModel() {
|
||||||
|
return this._featuresViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
get storageQuota() {
|
get storageQuota() {
|
||||||
return this._formatBytes(this._estimate?.quota);
|
return this._formatBytes(this._estimate?.quota);
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,11 @@ export class ToastCollectionViewModel extends ViewModel<SegmentType, Options> {
|
|||||||
constructor(options: Options) {
|
constructor(options: Options) {
|
||||||
super(options);
|
super(options);
|
||||||
const session = this.getOption("session");
|
const session = this.getOption("session");
|
||||||
|
if (this.features.calls) {
|
||||||
const callsObservableMap = session.callHandler.calls;
|
const callsObservableMap = session.callHandler.calls;
|
||||||
this.track(callsObservableMap.subscribe(this));
|
this.track(callsObservableMap.subscribe(this));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async onAdd(_, call: GroupCall) {
|
async onAdd(_, call: GroupCall) {
|
||||||
if (this._shouldShowNotification(call)) {
|
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 {SSOLoginHelper} from "./login/SSOLoginHelper";
|
||||||
import {getDehydratedDevice} from "./e2ee/Dehydration.js";
|
import {getDehydratedDevice} from "./e2ee/Dehydration.js";
|
||||||
import {Registration} from "./registration/Registration";
|
import {Registration} from "./registration/Registration";
|
||||||
|
import {FeatureSet} from "../features";
|
||||||
|
|
||||||
export const LoadStatus = createEnum(
|
export const LoadStatus = createEnum(
|
||||||
"NotLoading",
|
"NotLoading",
|
||||||
@ -53,7 +54,7 @@ export const LoginFailure = createEnum(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
constructor(platform) {
|
constructor(platform, features = new FeatureSet(0)) {
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
this._sessionStartedByReconnector = false;
|
this._sessionStartedByReconnector = false;
|
||||||
this._status = new ObservableValue(LoadStatus.NotLoading);
|
this._status = new ObservableValue(LoadStatus.NotLoading);
|
||||||
@ -68,6 +69,7 @@ export class Client {
|
|||||||
this._olmPromise = platform.loadOlm();
|
this._olmPromise = platform.loadOlm();
|
||||||
this._workerPromise = platform.loadOlmWorker();
|
this._workerPromise = platform.loadOlmWorker();
|
||||||
this._accountSetup = undefined;
|
this._accountSetup = undefined;
|
||||||
|
this._features = features;
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewSessionId() {
|
createNewSessionId() {
|
||||||
@ -278,6 +280,7 @@ export class Client {
|
|||||||
olmWorker,
|
olmWorker,
|
||||||
mediaRepository,
|
mediaRepository,
|
||||||
platform: this._platform,
|
platform: this._platform,
|
||||||
|
features: this._features
|
||||||
});
|
});
|
||||||
await this._session.load(log);
|
await this._session.load(log);
|
||||||
if (dehydratedDevice) {
|
if (dehydratedDevice) {
|
||||||
|
@ -74,6 +74,7 @@ export class DeviceMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async afterSyncCompleted(decryptionResults, deviceTracker, hsApi, log) {
|
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
|
// 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
|
// and check the keys, and we should only do network requests during
|
||||||
// sync processing in the afterSyncCompleted step.
|
// sync processing in the afterSyncCompleted step.
|
||||||
@ -99,6 +100,7 @@ export class DeviceMessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncPreparation {
|
class SyncPreparation {
|
||||||
|
@ -54,7 +54,7 @@ const PUSHER_KEY = "pusher";
|
|||||||
|
|
||||||
export class Session {
|
export class Session {
|
||||||
// sessionInfo contains deviceId, userId and homeserver
|
// 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._platform = platform;
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._hsApi = hsApi;
|
this._hsApi = hsApi;
|
||||||
@ -75,36 +75,7 @@ export class Session {
|
|||||||
};
|
};
|
||||||
this._roomsBeingCreated = new ObservableMap();
|
this._roomsBeingCreated = new ObservableMap();
|
||||||
this._user = new User(sessionInfo.userId);
|
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._roomStateHandler = new RoomStateHandlerSet();
|
||||||
this.observeRoomState(this._callHandler);
|
|
||||||
this._deviceMessageHandler = new DeviceMessageHandler({storage, callHandler: this._callHandler});
|
this._deviceMessageHandler = new DeviceMessageHandler({storage, callHandler: this._callHandler});
|
||||||
this._olm = olm;
|
this._olm = olm;
|
||||||
this._olmUtil = null;
|
this._olmUtil = null;
|
||||||
@ -132,6 +103,10 @@ export class Session {
|
|||||||
this._createRoomEncryption = this._createRoomEncryption.bind(this);
|
this._createRoomEncryption = this._createRoomEncryption.bind(this);
|
||||||
this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this);
|
this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this);
|
||||||
this.needsKeyBackup = new ObservableValue(false);
|
this.needsKeyBackup = new ObservableValue(false);
|
||||||
|
|
||||||
|
if (features.calls) {
|
||||||
|
this._setupCallHandler();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get fingerprintKey() {
|
get fingerprintKey() {
|
||||||
@ -154,6 +129,38 @@ export class Session {
|
|||||||
return this._callHandler;
|
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
|
// called once this._e2eeAccount is assigned
|
||||||
_setupEncryption() {
|
_setupEncryption() {
|
||||||
// TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account
|
// 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() {
|
export function tests() {
|
||||||
function createStorageMock(session, pendingEvents = []) {
|
function createStorageMock(session, pendingEvents = []) {
|
||||||
return {
|
return {
|
||||||
@ -1051,7 +1059,8 @@ export function tests() {
|
|||||||
clock: {
|
clock: {
|
||||||
createTimeout: () => undefined
|
createTimeout: () => undefined
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
features: new FeatureSet(0)
|
||||||
});
|
});
|
||||||
await session.load();
|
await session.load();
|
||||||
let syncSet = false;
|
let syncSet = false;
|
||||||
|
@ -283,6 +283,10 @@ export class Platform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
document.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
openFile(mimeType = null) {
|
openFile(mimeType = null) {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.setAttribute("type", "file");
|
input.setAttribute("type", "file");
|
||||||
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||||||
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
|
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
|
||||||
import {RootViewModel} from "../../domain/RootViewModel.js";
|
import {RootViewModel} from "../../domain/RootViewModel.js";
|
||||||
import {createNavigation, createRouter} from "../../domain/navigation/index";
|
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,
|
// Don't use a default export here, as we use multiple entries during legacy build,
|
||||||
// which does not support default exports,
|
// which does not support default exports,
|
||||||
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
||||||
@ -33,6 +35,7 @@ export async function main(platform) {
|
|||||||
// const request = recorder.request;
|
// const request = recorder.request;
|
||||||
// window.getBrawlFetchLog = () => recorder.log();
|
// window.getBrawlFetchLog = () => recorder.log();
|
||||||
await platform.init();
|
await platform.init();
|
||||||
|
const features = await FeatureSet.load(platform.settingsStorage);
|
||||||
const navigation = createNavigation();
|
const navigation = createNavigation();
|
||||||
platform.setNavigation(navigation);
|
platform.setNavigation(navigation);
|
||||||
const urlRouter = createRouter({navigation, history: platform.history});
|
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
|
// so we call it that in the view models
|
||||||
urlRouter: urlRouter,
|
urlRouter: urlRouter,
|
||||||
navigation,
|
navigation,
|
||||||
|
features
|
||||||
});
|
});
|
||||||
await vm.load();
|
await vm.load();
|
||||||
platform.createAndMountRootView(vm);
|
platform.createAndMountRootView(vm);
|
||||||
|
@ -751,6 +751,24 @@ a {
|
|||||||
margin-bottom: 0;
|
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 {
|
.error {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
@ -73,8 +73,10 @@ export class RoomView extends TemplateView {
|
|||||||
} else {
|
} else {
|
||||||
const vm = this.value;
|
const vm = this.value;
|
||||||
const options = [];
|
const options = [];
|
||||||
options.push(Menu.option(vm.i18n`Room details`, () => vm.openDetailsPanel()))
|
options.push(Menu.option(vm.i18n`Room details`, () => vm.openDetailsPanel()));
|
||||||
options.push(Menu.option(vm.i18n`Start call`, () => vm.startCall()))
|
if (vm.features.calls) {
|
||||||
|
options.push(Menu.option(vm.i18n`Start call`, () => vm.startCall()));
|
||||||
|
}
|
||||||
if (vm.canLeave) {
|
if (vm.canLeave) {
|
||||||
options.push(Menu.option(vm.i18n`Leave room`, () => this._confirmToLeaveRoom()).setDestructive());
|
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 {TemplateView} from "../../general/TemplateView";
|
||||||
import {disableTargetCallback} from "../../general/utils";
|
import {disableTargetCallback} from "../../general/utils";
|
||||||
import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js"
|
import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js"
|
||||||
|
import {FeaturesView} from "./FeaturesView"
|
||||||
|
|
||||||
export class SettingsView extends TemplateView {
|
export class SettingsView extends TemplateView {
|
||||||
render(t, vm) {
|
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: disableTargetCallback(() => vm.sendLogsToServer())}, `Submit logs to ${vm.logsServer}`));
|
||||||
}
|
}
|
||||||
logButtons.push(t.button({onClick: () => vm.exportLogs()}, "Download logs"));
|
logButtons.push(t.button({onClick: () => vm.exportLogs()}, "Download logs"));
|
||||||
|
|
||||||
|
settingNodes.push(
|
||||||
|
t.h3("Experimental features"),
|
||||||
|
t.view(new FeaturesView(vm.featuresViewModel))
|
||||||
|
);
|
||||||
|
|
||||||
settingNodes.push(
|
settingNodes.push(
|
||||||
t.h3("Application"),
|
t.h3("Application"),
|
||||||
row(t, vm.i18n`Version`, version),
|
row(t, vm.i18n`Version`, version),
|
||||||
|
Loading…
Reference in New Issue
Block a user