Use & enforce snake_case naming convention on config.json settings (#8062)
* Document and support the established naming convention for config opts This change: * Rename `ConfigOptions` to `IConfigOptions` to match code convention/style, plus move it to a dedicated file * Update comments and surrounding documentation * Define every single documented option (from element-web's config.md) * Enable a linter to enforce the convention * Invent a translation layer for a different change to use * No attempt to fix build errors from doing this (at this stage) * Add demo of lint rule in action * Fix all obvious instances of SdkConfig case conflicts * Fix tests to use SdkConfig directly * Add docs to make unset() calling safer * Appease the linter * Update documentation to match snake_case_config * Fix more instances of square brackets off SdkConfig
This commit is contained in:
parent
09c57b228e
commit
d8a939df5d
56 changed files with 605 additions and 259 deletions
|
@ -15,7 +15,7 @@ order of priority, are:
|
||||||
* `room-account` - The current user's account, but only when in a specific room
|
* `room-account` - The current user's account, but only when in a specific room
|
||||||
* `account` - The current user's account
|
* `account` - The current user's account
|
||||||
* `room` - A specific room (setting for all members of the room)
|
* `room` - A specific room (setting for all members of the room)
|
||||||
* `config` - Values are defined by the `settingDefaults` key (usually) in `config.json`
|
* `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
|
||||||
* `default` - The hardcoded default for the settings
|
* `default` - The hardcoded default for the settings
|
||||||
|
|
||||||
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
||||||
|
@ -27,12 +27,12 @@ that room administrators cannot force account-only settings upon participants.
|
||||||
Settings are the different options a user may set or experience in the application. These are pre-defined in
|
Settings are the different options a user may set or experience in the application. These are pre-defined in
|
||||||
`src/settings/Settings.tsx` under the `SETTINGS` constant, and match the `ISetting` interface as defined there.
|
`src/settings/Settings.tsx` under the `SETTINGS` constant, and match the `ISetting` interface as defined there.
|
||||||
|
|
||||||
Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some
|
Settings that support the config level can be set in the config file under the `setting_defaults` key (note that some
|
||||||
settings, like the "theme" setting, are special cased in the config file):
|
settings, like the "theme" setting, are special cased in the config file):
|
||||||
```json
|
```json5
|
||||||
{
|
{
|
||||||
...
|
...
|
||||||
"settingDefaults": {
|
"setting_defaults": {
|
||||||
"settingName": true
|
"settingName": true
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
|
|
|
@ -41,3 +41,11 @@ export type RecursivePartial<T> = {
|
||||||
T[P] extends object ? RecursivePartial<T[P]> :
|
T[P] extends object ? RecursivePartial<T[P]> :
|
||||||
T[P];
|
T[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Inspired by https://stackoverflow.com/a/60206860
|
||||||
|
export type KeysWithObjectShape<Input> = {
|
||||||
|
[P in keyof Input]: Input[P] extends object
|
||||||
|
// Arrays are counted as objects - exclude them
|
||||||
|
? (Input[P] extends Array<unknown> ? never : P)
|
||||||
|
: never;
|
||||||
|
}[keyof Input];
|
||||||
|
|
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
|
@ -52,7 +52,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import { Skinner } from "../Skinner";
|
import { Skinner } from "../Skinner";
|
||||||
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
|
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
|
||||||
import { ConfigOptions } from "../SdkConfig";
|
import { IConfigOptions } from "../IConfigOptions";
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ declare global {
|
||||||
Olm: {
|
Olm: {
|
||||||
init: () => Promise<void>;
|
init: () => Promise<void>;
|
||||||
};
|
};
|
||||||
mxReactSdkConfig: ConfigOptions;
|
mxReactSdkConfig: IConfigOptions;
|
||||||
|
|
||||||
// Needed for Safari, unknown to TypeScript
|
// Needed for Safari, unknown to TypeScript
|
||||||
webkitAudioContext: typeof AudioContext;
|
webkitAudioContext: typeof AudioContext;
|
||||||
|
|
|
@ -17,12 +17,15 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { getCurrentLanguage, _t, _td, IVariables } from './languageHandler';
|
import { getCurrentLanguage, _t, _td, IVariables } from './languageHandler';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
|
import { SnakedObject } from "./utils/SnakedObject";
|
||||||
|
import { IConfigOptions } from "./IConfigOptions";
|
||||||
|
|
||||||
const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/;
|
const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/;
|
||||||
const hashVarRegex = /#\/(group|room|user)\/.*$/;
|
const hashVarRegex = /#\/(group|room|user)\/.*$/;
|
||||||
|
@ -193,8 +196,12 @@ export class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
public canEnable() {
|
public canEnable() {
|
||||||
const config = SdkConfig.get();
|
const piwikConfig = SdkConfig.get("piwik");
|
||||||
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId;
|
let piwik: Optional<SnakedObject<Extract<IConfigOptions["piwik"], object>>>;
|
||||||
|
if (typeof piwikConfig === 'object') {
|
||||||
|
piwik = new SnakedObject(piwikConfig);
|
||||||
|
}
|
||||||
|
return navigator.doNotTrack !== "1" && piwik?.get("site_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -204,12 +211,16 @@ export class Analytics {
|
||||||
public async enable() {
|
public async enable() {
|
||||||
if (!this.disabled) return;
|
if (!this.disabled) return;
|
||||||
if (!this.canEnable()) return;
|
if (!this.canEnable()) return;
|
||||||
const config = SdkConfig.get();
|
const piwikConfig = SdkConfig.get("piwik");
|
||||||
|
let piwik: Optional<SnakedObject<Extract<IConfigOptions["piwik"], object>>>;
|
||||||
|
if (typeof piwikConfig === 'object') {
|
||||||
|
piwik = new SnakedObject(piwikConfig);
|
||||||
|
}
|
||||||
|
|
||||||
this.baseUrl = new URL("piwik.php", config.piwik.url);
|
this.baseUrl = new URL("piwik.php", piwik.get("url"));
|
||||||
// set constants
|
// set constants
|
||||||
this.baseUrl.searchParams.set("rec", "1"); // rec is required for tracking
|
this.baseUrl.searchParams.set("rec", "1"); // rec is required for tracking
|
||||||
this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking
|
this.baseUrl.searchParams.set("idsite", piwik.get("site_id")); // idsite is required for tracking
|
||||||
this.baseUrl.searchParams.set("apiv", "1"); // API version to use
|
this.baseUrl.searchParams.set("apiv", "1"); // API version to use
|
||||||
this.baseUrl.searchParams.set("send_image", "0"); // we want a 204, not a tiny GIF
|
this.baseUrl.searchParams.set("send_image", "0"); // we want a 204, not a tiny GIF
|
||||||
// set user parameters
|
// set user parameters
|
||||||
|
@ -347,10 +358,14 @@ export class Analytics {
|
||||||
public setLoggedIn(isGuest: boolean, homeserverUrl: string) {
|
public setLoggedIn(isGuest: boolean, homeserverUrl: string) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
const config = SdkConfig.get();
|
const piwikConfig = SdkConfig.get("piwik");
|
||||||
if (!config.piwik) return;
|
let piwik: Optional<SnakedObject<Extract<IConfigOptions["piwik"], object>>>;
|
||||||
|
if (typeof piwikConfig === 'object') {
|
||||||
|
piwik = new SnakedObject(piwikConfig);
|
||||||
|
}
|
||||||
|
if (!piwik) return;
|
||||||
|
|
||||||
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
const whitelistedHSUrls = piwik.get("whitelisted_hs_urls", "whitelistedHSUrls") || [];
|
||||||
|
|
||||||
this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||||
this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||||
|
@ -391,7 +406,12 @@ export class Analytics {
|
||||||
];
|
];
|
||||||
|
|
||||||
// FIXME: Using an import will result in test failures
|
// FIXME: Using an import will result in test failures
|
||||||
const cookiePolicyUrl = SdkConfig.get().piwik?.policyUrl;
|
const piwikConfig = SdkConfig.get("piwik");
|
||||||
|
let piwik: Optional<SnakedObject<Extract<IConfigOptions["piwik"], object>>>;
|
||||||
|
if (typeof piwikConfig === 'object') {
|
||||||
|
piwik = new SnakedObject(piwikConfig);
|
||||||
|
}
|
||||||
|
const cookiePolicyUrl = piwik?.get("policy_url");
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
const cookiePolicyLink = _t(
|
const cookiePolicyLink = _t(
|
||||||
"Our complete cookie policy can be found <CookiePolicyLink>here</CookiePolicyLink>.",
|
"Our complete cookie policy can be found <CookiePolicyLink>here</CookiePolicyLink>.",
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { hideToast as hideUpdateToast } from "./toasts/UpdateToast";
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import { idbLoad, idbSave, idbDelete } from "./utils/StorageManager";
|
import { idbLoad, idbSave, idbDelete } from "./utils/StorageManager";
|
||||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||||
|
import { IConfigOptions } from "./IConfigOptions";
|
||||||
|
|
||||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||||
|
@ -62,7 +63,7 @@ export default abstract class BasePlatform {
|
||||||
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getConfig(): Promise<{}>;
|
abstract getConfig(): Promise<IConfigOptions>;
|
||||||
|
|
||||||
abstract getDefaultDeviceDisplayName(): string;
|
abstract getDefaultDeviceDisplayName(): string;
|
||||||
|
|
||||||
|
|
|
@ -259,7 +259,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldObeyAssertedfIdentity(): boolean {
|
private shouldObeyAssertedfIdentity(): boolean {
|
||||||
return SdkConfig.get()['voip']?.obeyAssertedIdentity;
|
return SdkConfig.getObject("voip")?.get("obey_asserted_identity");
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSupportsPstnProtocol(): boolean {
|
public getSupportsPstnProtocol(): boolean {
|
||||||
|
|
186
src/IConfigOptions.ts
Normal file
186
src/IConfigOptions.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 - 2022 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 { IClientWellKnown } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { ValidatedServerConfig } from "./utils/AutoDiscoveryUtils";
|
||||||
|
|
||||||
|
// Convention decision: All config options are lower_snake_case
|
||||||
|
// We use an isolated file for the interface so we can mess around with the eslint options.
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
/* eslint @typescript-eslint/naming-convention: ["error", { "selector": "property", "format": ["snake_case"] } ] */
|
||||||
|
|
||||||
|
// see element-web config.md for non-developer docs
|
||||||
|
export interface IConfigOptions {
|
||||||
|
// dev note: while true that this is arbitrary JSON, it's valuable to enforce that all
|
||||||
|
// config options are documented for "find all usages" sort of searching.
|
||||||
|
// [key: string]: any;
|
||||||
|
|
||||||
|
// Properties of this interface are roughly grouped by their subject matter, such as
|
||||||
|
// "instance customisation", "login stuff", "branding", etc. Use blank lines to denote
|
||||||
|
// a logical separation of properties, but keep similar ones near each other.
|
||||||
|
|
||||||
|
// Exactly one of the following must be supplied
|
||||||
|
default_server_config?: IClientWellKnown; // copy/paste of client well-known
|
||||||
|
default_server_name?: string; // domain to do well-known lookup on
|
||||||
|
default_hs_url?: string; // http url
|
||||||
|
|
||||||
|
default_is_url?: string; // used in combination with default_hs_url, but for the identity server
|
||||||
|
|
||||||
|
// This is intended to be overridden by app startup and not specified by the user
|
||||||
|
// This is also why it's allowed to have an interface that isn't snake_case
|
||||||
|
validated_server_config?: ValidatedServerConfig;
|
||||||
|
|
||||||
|
fallback_hs_url?: string;
|
||||||
|
|
||||||
|
disable_custom_urls?: boolean;
|
||||||
|
disable_guests?: boolean;
|
||||||
|
disable_login_language_selector?: boolean;
|
||||||
|
disable_3pid_login?: boolean;
|
||||||
|
|
||||||
|
brand: string;
|
||||||
|
branding?: {
|
||||||
|
welcome_background_url?: string | string[]; // chosen at random if array
|
||||||
|
auth_header_logo_url?: string;
|
||||||
|
auth_footer_links?: {text: string, url: string}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
map_style_url?: string; // for location-shared maps
|
||||||
|
|
||||||
|
embedded_pages?: {
|
||||||
|
welcome_url?: string;
|
||||||
|
home_url?: string;
|
||||||
|
login_for_welcome?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
permalink_prefix?: string;
|
||||||
|
|
||||||
|
update_base_url?: string;
|
||||||
|
desktop_builds?: {
|
||||||
|
available: boolean;
|
||||||
|
logo: string; // url
|
||||||
|
url: string; // download url
|
||||||
|
};
|
||||||
|
mobile_builds?: {
|
||||||
|
ios?: string; // download url
|
||||||
|
android?: string; // download url
|
||||||
|
fdroid?: string; // download url
|
||||||
|
};
|
||||||
|
|
||||||
|
mobile_guide_toast?: boolean;
|
||||||
|
|
||||||
|
default_theme?: "light" | "dark" | string; // custom themes are strings
|
||||||
|
default_country_code?: string; // ISO 3166 alpha2 country code
|
||||||
|
default_federate?: boolean;
|
||||||
|
default_device_display_name?: string; // for device naming on login+registration
|
||||||
|
|
||||||
|
setting_defaults?: Record<string, any>; // <SettingName, Value>
|
||||||
|
|
||||||
|
integrations_ui_url?: string;
|
||||||
|
integrations_rest_url?: string;
|
||||||
|
integrations_widgets_urls?: string[];
|
||||||
|
|
||||||
|
show_labs_settings?: boolean;
|
||||||
|
features?: Record<string, boolean>; // <FeatureName, EnabledBool>
|
||||||
|
|
||||||
|
bug_report_endpoint_url?: string; // omission disables bug reporting
|
||||||
|
uisi_autorageshake_app?: string;
|
||||||
|
sentry?: {
|
||||||
|
dsn: string;
|
||||||
|
environment?: string; // "production", etc
|
||||||
|
};
|
||||||
|
|
||||||
|
widget_build_url?: string; // url called to replace jitsi/call widget creation
|
||||||
|
audio_stream_url?: string;
|
||||||
|
jitsi?: {
|
||||||
|
preferred_domain: string;
|
||||||
|
};
|
||||||
|
jitsi_widget?: {
|
||||||
|
skip_built_in_welcome_screen?: boolean;
|
||||||
|
};
|
||||||
|
voip?: {
|
||||||
|
obey_asserted_identity?: boolean; // MSC3086
|
||||||
|
};
|
||||||
|
|
||||||
|
logout_redirect_url?: string;
|
||||||
|
|
||||||
|
// sso_immediate_redirect is deprecated in favour of sso_redirect_options.immediate
|
||||||
|
sso_immediate_redirect?: boolean;
|
||||||
|
sso_redirect_options?: ISsoRedirectOptions;
|
||||||
|
|
||||||
|
custom_translations_url?: string;
|
||||||
|
|
||||||
|
report_event?: {
|
||||||
|
admin_message_md: string; // message for how to contact the server owner when reporting an event
|
||||||
|
};
|
||||||
|
|
||||||
|
welcome_user_id?: string;
|
||||||
|
|
||||||
|
room_directory?: {
|
||||||
|
servers: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// piwik (matomo) is deprecated in favour of posthog
|
||||||
|
piwik?: false | {
|
||||||
|
url: string; // piwik instance
|
||||||
|
site_id: string;
|
||||||
|
policy_url: string; // cookie policy
|
||||||
|
whitelisted_hs_urls: string[];
|
||||||
|
};
|
||||||
|
posthog?: {
|
||||||
|
project_api_key: string;
|
||||||
|
api_host: string; // hostname
|
||||||
|
};
|
||||||
|
analytics_owner?: string; // defaults to `brand`
|
||||||
|
|
||||||
|
// Server hosting upsell options
|
||||||
|
hosting_signup_link?: string; // slightly different from `host_signup`
|
||||||
|
host_signup?: {
|
||||||
|
brand?: string; // acts as the enabled flag too (truthy == show)
|
||||||
|
|
||||||
|
// Required-ness denotes when `brand` is truthy
|
||||||
|
cookie_policy_url: string;
|
||||||
|
privacy_policy_url: string;
|
||||||
|
terms_of_service_url: string;
|
||||||
|
url: string;
|
||||||
|
domains?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
enable_presence_by_hs_url?: Record<string, boolean>; // <HomeserverName, Enabled>
|
||||||
|
|
||||||
|
terms_and_conditions_links?: { url: string, text: string }[];
|
||||||
|
|
||||||
|
latex_maths_delims?: {
|
||||||
|
inline?: {
|
||||||
|
left?: string;
|
||||||
|
right?: string;
|
||||||
|
};
|
||||||
|
display?: {
|
||||||
|
left?: string;
|
||||||
|
right?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sync_timeline_limit?: number;
|
||||||
|
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISsoRedirectOptions {
|
||||||
|
immediate?: boolean;
|
||||||
|
on_welcome_page?: boolean;
|
||||||
|
}
|
|
@ -161,7 +161,7 @@ const callBindings = (): KeyBinding[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const labsBindings = (): KeyBinding[] => {
|
const labsBindings = (): KeyBinding[] => {
|
||||||
if (!SdkConfig.get()['showLabsSettings']) return [];
|
if (!SdkConfig.get("show_labs_settings")) return [];
|
||||||
|
|
||||||
return getBindingsByCategory(CategoryName.LABS);
|
return getBindingsByCategory(CategoryName.LABS);
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,7 @@ import SdkConfig from "./SdkConfig";
|
||||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||||
|
|
||||||
export function getConfigLivestreamUrl() {
|
export function getConfigLivestreamUrl() {
|
||||||
return SdkConfig.get()["audioStreamUrl"];
|
return SdkConfig.get("audio_stream_url");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy rtmp URL used to signal that we want a special audio-only stream
|
// Dummy rtmp URL used to signal that we want a special audio-only stream
|
||||||
|
|
|
@ -126,10 +126,10 @@ export class PosthogAnalytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly posthog: PostHog) {
|
constructor(private readonly posthog: PostHog) {
|
||||||
const posthogConfig = SdkConfig.get()["posthog"];
|
const posthogConfig = SdkConfig.getObject("posthog");
|
||||||
if (posthogConfig) {
|
if (posthogConfig) {
|
||||||
this.posthog.init(posthogConfig.projectApiKey, {
|
this.posthog.init(posthogConfig.get("project_api_key"), {
|
||||||
api_host: posthogConfig.apiHost,
|
api_host: posthogConfig.get("api_host"),
|
||||||
autocapture: false,
|
autocapture: false,
|
||||||
mask_all_text: true,
|
mask_all_text: true,
|
||||||
mask_all_element_attributes: true,
|
mask_all_element_attributes: true,
|
||||||
|
|
|
@ -44,8 +44,8 @@ export default class ScalarAuthClient {
|
||||||
|
|
||||||
// We try and store the token on a per-manager basis, but need a fallback
|
// We try and store the token on a per-manager basis, but need a fallback
|
||||||
// for the default manager.
|
// for the default manager.
|
||||||
const configApiUrl = SdkConfig.get()['integrations_rest_url'];
|
const configApiUrl = SdkConfig.get("integrations_rest_url");
|
||||||
const configUiUrl = SdkConfig.get()['integrations_ui_url'];
|
const configUiUrl = SdkConfig.get("integrations_ui_url");
|
||||||
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
|
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,39 +15,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface ISsoRedirectOptions {
|
import { Optional } from "matrix-events-sdk";
|
||||||
immediate?: boolean;
|
|
||||||
on_welcome_page?: boolean; // eslint-disable-line camelcase
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
import { SnakedObject } from "./utils/SnakedObject";
|
||||||
export interface ConfigOptions {
|
import { IConfigOptions, ISsoRedirectOptions } from "./IConfigOptions";
|
||||||
[key: string]: any;
|
import { KeysWithObjectShape } from "./@types/common";
|
||||||
|
|
||||||
logout_redirect_url?: string;
|
// see element-web config.md for docs, or the IConfigOptions interface for dev docs
|
||||||
|
export const DEFAULTS: Partial<IConfigOptions> = {
|
||||||
// sso_immediate_redirect is deprecated in favour of sso_redirect_options.immediate
|
|
||||||
sso_immediate_redirect?: boolean;
|
|
||||||
sso_redirect_options?: ISsoRedirectOptions;
|
|
||||||
|
|
||||||
custom_translations_url?: string;
|
|
||||||
}
|
|
||||||
/* eslint-enable camelcase*/
|
|
||||||
|
|
||||||
export const DEFAULTS: ConfigOptions = {
|
|
||||||
// Brand name of the app
|
|
||||||
brand: "Element",
|
brand: "Element",
|
||||||
// URL to a page we show in an iframe to configure integrations
|
|
||||||
integrations_ui_url: "https://scalar.vector.im/",
|
integrations_ui_url: "https://scalar.vector.im/",
|
||||||
// Base URL to the REST interface of the integrations server
|
|
||||||
integrations_rest_url: "https://scalar.vector.im/api",
|
integrations_rest_url: "https://scalar.vector.im/api",
|
||||||
// Where to send bug reports. If not specified, bugs cannot be sent.
|
|
||||||
bug_report_endpoint_url: null,
|
bug_report_endpoint_url: null,
|
||||||
// Jitsi conference options
|
|
||||||
jitsi: {
|
jitsi: {
|
||||||
// Default conference domain
|
preferred_domain: "meet.element.io",
|
||||||
preferredDomain: "meet.element.io",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// @ts-ignore - we deliberately use the camelCase version here so we trigger
|
||||||
|
// the fallback behaviour. If we used the snake_case version then we'd break
|
||||||
|
// everyone's config which has the camelCase property because our default would
|
||||||
|
// be preferred over their config.
|
||||||
desktopBuilds: {
|
desktopBuilds: {
|
||||||
available: true,
|
available: true,
|
||||||
logo: require("../res/img/element-desktop-logo.svg").default,
|
logo: require("../res/img/element-desktop-logo.svg").default,
|
||||||
|
@ -56,20 +43,42 @@ export const DEFAULTS: ConfigOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class SdkConfig {
|
export default class SdkConfig {
|
||||||
private static instance: ConfigOptions;
|
private static instance: IConfigOptions;
|
||||||
|
private static fallback: SnakedObject<IConfigOptions>;
|
||||||
|
|
||||||
private static setInstance(i: ConfigOptions) {
|
private static setInstance(i: IConfigOptions) {
|
||||||
SdkConfig.instance = i;
|
SdkConfig.instance = i;
|
||||||
|
SdkConfig.fallback = new SnakedObject(i);
|
||||||
|
|
||||||
// For debugging purposes
|
// For debugging purposes
|
||||||
window.mxReactSdkConfig = i;
|
window.mxReactSdkConfig = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get() {
|
public static get(): IConfigOptions;
|
||||||
return SdkConfig.instance || {};
|
public static get<K extends keyof IConfigOptions>(key: K, altCaseName?: string): IConfigOptions[K];
|
||||||
|
public static get<K extends keyof IConfigOptions = never>(
|
||||||
|
key?: K, altCaseName?: string,
|
||||||
|
): IConfigOptions | IConfigOptions[K] {
|
||||||
|
if (key === undefined) {
|
||||||
|
// safe to cast as a fallback - we want to break the runtime contract in this case
|
||||||
|
return SdkConfig.instance || <IConfigOptions>{};
|
||||||
|
}
|
||||||
|
return SdkConfig.fallback.get(key, altCaseName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static put(cfg: ConfigOptions) {
|
public static getObject<K extends KeysWithObjectShape<IConfigOptions>>(
|
||||||
|
key: K, altCaseName?: string,
|
||||||
|
): Optional<SnakedObject<IConfigOptions[K]>> {
|
||||||
|
const val = SdkConfig.get(key, altCaseName);
|
||||||
|
if (val !== null && val !== undefined) {
|
||||||
|
return new SnakedObject(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the same type for sensitive callers (some want `undefined` specifically)
|
||||||
|
return val === undefined ? undefined : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static put(cfg: IConfigOptions) {
|
||||||
const defaultKeys = Object.keys(DEFAULTS);
|
const defaultKeys = Object.keys(DEFAULTS);
|
||||||
for (let i = 0; i < defaultKeys.length; ++i) {
|
for (let i = 0; i < defaultKeys.length; ++i) {
|
||||||
if (cfg[defaultKeys[i]] === undefined) {
|
if (cfg[defaultKeys[i]] === undefined) {
|
||||||
|
@ -79,18 +88,21 @@ export default class SdkConfig {
|
||||||
SdkConfig.setInstance(cfg);
|
SdkConfig.setInstance(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the config to be completely empty.
|
||||||
|
*/
|
||||||
public static unset() {
|
public static unset() {
|
||||||
SdkConfig.setInstance({});
|
SdkConfig.setInstance(<IConfigOptions>{}); // safe to cast - defaults will be applied
|
||||||
}
|
}
|
||||||
|
|
||||||
public static add(cfg: ConfigOptions) {
|
public static add(cfg: Partial<IConfigOptions>) {
|
||||||
const liveConfig = SdkConfig.get();
|
const liveConfig = SdkConfig.get();
|
||||||
const newConfig = Object.assign({}, liveConfig, cfg);
|
const newConfig = Object.assign({}, liveConfig, cfg);
|
||||||
SdkConfig.put(newConfig);
|
SdkConfig.put(newConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSsoRedirectOptions(config: ConfigOptions): ISsoRedirectOptions {
|
export function parseSsoRedirectOptions(config: IConfigOptions): ISsoRedirectOptions {
|
||||||
// Ignore deprecated options if the config is using new ones
|
// Ignore deprecated options if the config is using new ones
|
||||||
if (config.sso_redirect_options) return config.sso_redirect_options;
|
if (config.sso_redirect_options) return config.sso_redirect_options;
|
||||||
|
|
||||||
|
|
|
@ -103,11 +103,8 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
|
||||||
if (justRegistered) {
|
if (justRegistered) {
|
||||||
introSection = <UserWelcomeTop />;
|
introSection = <UserWelcomeTop />;
|
||||||
} else {
|
} else {
|
||||||
const brandingConfig = config.branding;
|
const brandingConfig = SdkConfig.getObject("branding");
|
||||||
let logoUrl = "themes/element/img/logos/element-logo.svg";
|
const logoUrl = brandingConfig?.get("auth_header_logo_url") ?? "themes/element/img/logos/element-logo.svg";
|
||||||
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
|
|
||||||
logoUrl = brandingConfig.authHeaderLogoUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
introSection = <React.Fragment>
|
introSection = <React.Fragment>
|
||||||
<img src={logoUrl} alt={config.brand} />
|
<img src={logoUrl} alt={config.brand} />
|
||||||
|
|
|
@ -39,8 +39,8 @@ export default class HostSignupAction extends React.PureComponent<IProps, IState
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const hostSignupConfig = SdkConfig.get().hostSignup;
|
const hostSignupConfig = SdkConfig.getObject("host_signup");
|
||||||
if (!hostSignupConfig?.brand) {
|
if (!hostSignupConfig?.get("brand")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ export default class HostSignupAction extends React.PureComponent<IProps, IState
|
||||||
label={_t(
|
label={_t(
|
||||||
"Upgrade to %(hostSignupBrand)s",
|
"Upgrade to %(hostSignupBrand)s",
|
||||||
{
|
{
|
||||||
hostSignupBrand: hostSignupConfig.brand,
|
hostSignupBrand: hostSignupConfig.get("brand"),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={this.openDialog}
|
onClick={this.openDialog}
|
||||||
|
|
|
@ -75,6 +75,7 @@ import RightPanelStore from '../../stores/right-panel/RightPanelStore';
|
||||||
import { TimelineRenderingType } from "../../contexts/RoomContext";
|
import { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||||
import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
|
import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
|
||||||
|
import { IConfigOptions } from "../../IConfigOptions";
|
||||||
import LeftPanelLiveShareWarning from '../views/beacon/LeftPanelLiveShareWarning';
|
import LeftPanelLiveShareWarning from '../views/beacon/LeftPanelLiveShareWarning';
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
|
@ -103,12 +104,7 @@ interface IProps {
|
||||||
roomOobData?: IOOBData;
|
roomOobData?: IOOBData;
|
||||||
currentRoomId: string;
|
currentRoomId: string;
|
||||||
collapseLhs: boolean;
|
collapseLhs: boolean;
|
||||||
config: {
|
config: IConfigOptions;
|
||||||
piwik: {
|
|
||||||
policyUrl: string;
|
|
||||||
};
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
currentGroupId?: string;
|
currentGroupId?: string;
|
||||||
currentGroupIsNew?: boolean;
|
currentGroupIsNew?: boolean;
|
||||||
|
|
|
@ -131,6 +131,8 @@ import { ViewHomePagePayload } from '../../dispatcher/payloads/ViewHomePagePaylo
|
||||||
import { AfterLeaveRoomPayload } from '../../dispatcher/payloads/AfterLeaveRoomPayload';
|
import { AfterLeaveRoomPayload } from '../../dispatcher/payloads/AfterLeaveRoomPayload';
|
||||||
import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyncPreparedPayload';
|
import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyncPreparedPayload';
|
||||||
import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStartChatOrReusePayload';
|
import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStartChatOrReusePayload';
|
||||||
|
import { IConfigOptions } from "../../IConfigOptions";
|
||||||
|
import { SnakedObject } from "../../utils/SnakedObject";
|
||||||
import InfoDialog from '../views/dialogs/InfoDialog';
|
import InfoDialog from '../views/dialogs/InfoDialog';
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
|
@ -154,12 +156,7 @@ interface IScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps { // TODO type things better
|
interface IProps { // TODO type things better
|
||||||
config: {
|
config: IConfigOptions;
|
||||||
piwik: {
|
|
||||||
policyUrl: string;
|
|
||||||
};
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
serverConfig?: ValidatedServerConfig;
|
serverConfig?: ValidatedServerConfig;
|
||||||
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
||||||
enableGuest?: boolean;
|
enableGuest?: boolean;
|
||||||
|
@ -355,7 +352,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
Analytics.enable();
|
Analytics.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
initSentry(SdkConfig.get()["sentry"]);
|
initSentry(SdkConfig.get("sentry"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async postLoginSetup() {
|
private async postLoginSetup() {
|
||||||
|
@ -474,7 +471,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
private getServerProperties() {
|
private getServerProperties() {
|
||||||
let props = this.state.serverConfig;
|
let props = this.state.serverConfig;
|
||||||
if (!props) props = this.props.serverConfig; // for unit tests
|
if (!props) props = this.props.serverConfig; // for unit tests
|
||||||
if (!props) props = SdkConfig.get()["validated_server_config"];
|
if (!props) props = SdkConfig.get("validated_server_config");
|
||||||
return { serverConfig: props };
|
return { serverConfig: props };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -865,7 +862,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the hs url matches then take the hs name we know locally as it is likely prettier
|
// If the hs url matches then take the hs name we know locally as it is likely prettier
|
||||||
const defaultConfig = SdkConfig.get()["validated_server_config"] as ValidatedServerConfig;
|
const defaultConfig = SdkConfig.get("validated_server_config");
|
||||||
if (defaultConfig && defaultConfig.hsUrl === newState.serverConfig.hsUrl) {
|
if (defaultConfig && defaultConfig.hsUrl === newState.serverConfig.hsUrl) {
|
||||||
newState.serverConfig.hsName = defaultConfig.hsName;
|
newState.serverConfig.hsName = defaultConfig.hsName;
|
||||||
newState.serverConfig.hsNameIsDifferent = defaultConfig.hsNameIsDifferent;
|
newState.serverConfig.hsNameIsDifferent = defaultConfig.hsNameIsDifferent;
|
||||||
|
@ -1062,11 +1059,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private chatCreateOrReuse(userId: string) {
|
private chatCreateOrReuse(userId: string) {
|
||||||
|
const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config);
|
||||||
// Use a deferred action to reshow the dialog once the user has registered
|
// Use a deferred action to reshow the dialog once the user has registered
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
// No point in making 2 DMs with welcome bot. This assumes view_set_mxid will
|
// No point in making 2 DMs with welcome bot. This assumes view_set_mxid will
|
||||||
// result in a new DM with the welcome user.
|
// result in a new DM with the welcome user.
|
||||||
if (userId !== this.props.config.welcomeUserId) {
|
if (userId !== snakedConfig.get("welcome_user_id")) {
|
||||||
dis.dispatch<DoAfterSyncPreparedPayload<ViewStartChatOrReusePayload>>({
|
dis.dispatch<DoAfterSyncPreparedPayload<ViewStartChatOrReusePayload>>({
|
||||||
action: Action.DoAfterSyncPrepared,
|
action: Action.DoAfterSyncPrepared,
|
||||||
deferred_action: {
|
deferred_action: {
|
||||||
|
@ -1083,7 +1081,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
// `_chatCreateOrReuse` again)
|
// `_chatCreateOrReuse` again)
|
||||||
go_welcome_on_cancel: true,
|
go_welcome_on_cancel: true,
|
||||||
screen_after: {
|
screen_after: {
|
||||||
screen: `user/${this.props.config.welcomeUserId}`,
|
screen: `user/${snakedConfig.get("welcome_user_id")}`,
|
||||||
params: { action: 'chat' },
|
params: { action: 'chat' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1231,12 +1229,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
await waitFor;
|
await waitFor;
|
||||||
|
|
||||||
|
const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config);
|
||||||
const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(
|
const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(
|
||||||
this.props.config.welcomeUserId,
|
snakedConfig.get("welcome_user_id"),
|
||||||
);
|
);
|
||||||
if (welcomeUserRooms.length === 0) {
|
if (welcomeUserRooms.length === 0) {
|
||||||
const roomId = await createRoom({
|
const roomId = await createRoom({
|
||||||
dmUserId: this.props.config.welcomeUserId,
|
dmUserId: snakedConfig.get("welcome_user_id"),
|
||||||
// Only view the welcome user if we're NOT looking at a room
|
// Only view the welcome user if we're NOT looking at a room
|
||||||
andView: !this.state.currentRoomId,
|
andView: !this.state.currentRoomId,
|
||||||
spinner: false, // we're already showing one: we don't need another one
|
spinner: false, // we're already showing one: we don't need another one
|
||||||
|
@ -1250,7 +1249,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
// user room (it doesn't wait for new data from the server, just
|
// user room (it doesn't wait for new data from the server, just
|
||||||
// the saved sync to be loaded).
|
// the saved sync to be loaded).
|
||||||
const saveWelcomeUser = (ev: MatrixEvent) => {
|
const saveWelcomeUser = (ev: MatrixEvent) => {
|
||||||
if (ev.getType() === EventType.Direct && ev.getContent()[this.props.config.welcomeUserId]) {
|
if (ev.getType() === EventType.Direct && ev.getContent()[snakedConfig.get("welcome_user_id")]) {
|
||||||
MatrixClientPeg.get().store.save(true);
|
MatrixClientPeg.get().store.save(true);
|
||||||
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser);
|
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser);
|
||||||
}
|
}
|
||||||
|
@ -1280,7 +1279,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
} else if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
} else if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||||
MatrixClientPeg.setJustRegisteredUserId(null);
|
MatrixClientPeg.setJustRegisteredUserId(null);
|
||||||
|
|
||||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config);
|
||||||
|
if (snakedConfig.get("welcome_user_id") && getCurrentLanguage().startsWith("en")) {
|
||||||
const welcomeUserRoom = await this.startWelcomeUserChat();
|
const welcomeUserRoom = await this.startWelcomeUserChat();
|
||||||
if (welcomeUserRoom === null) {
|
if (welcomeUserRoom === null) {
|
||||||
// We didn't redirect to the welcome user room, so show
|
// We didn't redirect to the welcome user room, so show
|
||||||
|
@ -1312,7 +1312,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
showAnonymousAnalyticsOptInToast();
|
showAnonymousAnalyticsOptInToast();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SdkConfig.get().mobileGuideToast) {
|
if (SdkConfig.get("mobile_guide_toast")) {
|
||||||
// The toast contains further logic to detect mobile platforms,
|
// The toast contains further logic to detect mobile platforms,
|
||||||
// check if it has been dismissed before, etc.
|
// check if it has been dismissed before, etc.
|
||||||
showMobileGuideToast();
|
showMobileGuideToast();
|
||||||
|
@ -1463,7 +1463,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
if (!localStorage.getItem("mx_seen_feature_thread_experimental")) {
|
if (!localStorage.getItem("mx_seen_feature_thread_experimental")) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (SettingsStore.getValue("feature_thread") && SdkConfig.get()['showLabsSettings']) {
|
if (SettingsStore.getValue("feature_thread") && SdkConfig.get("show_labs_settings")) {
|
||||||
Modal.createDialog(InfoDialog, {
|
Modal.createDialog(InfoDialog, {
|
||||||
title: _t("Threads are no longer experimental! 🎉"),
|
title: _t("Threads are no longer experimental! 🎉"),
|
||||||
description: <>
|
description: <>
|
||||||
|
|
|
@ -103,7 +103,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let roomServer = myHomeserver;
|
let roomServer = myHomeserver;
|
||||||
if (
|
if (
|
||||||
SdkConfig.get().roomDirectory?.servers?.includes(lsRoomServer) ||
|
SdkConfig.getObject("room_directory")?.get("servers")?.includes(lsRoomServer) ||
|
||||||
SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer)
|
SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer)
|
||||||
) {
|
) {
|
||||||
roomServer = lsRoomServer;
|
roomServer = lsRoomServer;
|
||||||
|
|
|
@ -53,7 +53,6 @@ import IconizedContextMenu, {
|
||||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||||
import { UIFeature } from "../../settings/UIFeature";
|
import { UIFeature } from "../../settings/UIFeature";
|
||||||
import HostSignupAction from "./HostSignupAction";
|
import HostSignupAction from "./HostSignupAction";
|
||||||
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
|
|
||||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
@ -375,7 +374,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
if (!this.state.contextMenuPosition) return null;
|
if (!this.state.contextMenuPosition) return null;
|
||||||
|
|
||||||
let topSection;
|
let topSection;
|
||||||
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
|
const hostSignupConfig = SdkConfig.getObject("host_signup");
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
topSection = (
|
topSection = (
|
||||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||||
|
@ -395,16 +394,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (hostSignupConfig) {
|
} else if (hostSignupConfig?.get("url")) {
|
||||||
if (hostSignupConfig && hostSignupConfig.url) {
|
// If hostSignup.domains is set to a non-empty array, only show
|
||||||
// If hostSignup.domains is set to a non-empty array, only show
|
// dialog if the user is on the domain or a subdomain.
|
||||||
// dialog if the user is on the domain or a subdomain.
|
const hostSignupDomains = hostSignupConfig.get("domains") || [];
|
||||||
const hostSignupDomains = hostSignupConfig.domains || [];
|
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||||
const mxDomain = MatrixClientPeg.get().getDomain();
|
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
if (!hostSignupConfig.get("domains") || validDomains.length > 0) {
|
||||||
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
|
||||||
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -245,7 +245,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||||
if (error.errcode === 'M_USER_DEACTIVATED') {
|
if (error.errcode === 'M_USER_DEACTIVATED') {
|
||||||
errorText = _t('This account has been deactivated.');
|
errorText = _t('This account has been deactivated.');
|
||||||
} else if (SdkConfig.get()['disable_custom_urls']) {
|
} else if (SdkConfig.get("disable_custom_urls")) {
|
||||||
errorText = (
|
errorText = (
|
||||||
<div>
|
<div>
|
||||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default class CountryDropdown extends React.Component<IProps, IState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0];
|
let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0];
|
||||||
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
|
const defaultCountryCode = SdkConfig.get("default_country_code");
|
||||||
if (defaultCountryCode) {
|
if (defaultCountryCode) {
|
||||||
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
||||||
if (country) defaultCountry = country;
|
if (country) defaultCountry = country;
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LanguageSelector({ disabled }: IProps): JSX.Element {
|
export default function LanguageSelector({ disabled }: IProps): JSX.Element {
|
||||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
if (SdkConfig.get("disable_login_language_selector")) return <div />;
|
||||||
return <LanguageDropdown
|
return <LanguageDropdown
|
||||||
className="mx_AuthBody_language"
|
className="mx_AuthBody_language"
|
||||||
onOptionChange={onChange}
|
onOptionChange={onChange}
|
||||||
|
|
|
@ -73,7 +73,7 @@ class PassphraseField extends PureComponent<IProps> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const safe = complexity.score >= this.props.minScore;
|
const safe = complexity.score >= this.props.minScore;
|
||||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
const allowUnsafe = SdkConfig.get("dangerously_allow_unsafe_and_insecure_passwords");
|
||||||
return allowUnsafe || safe;
|
return allowUnsafe || safe;
|
||||||
},
|
},
|
||||||
valid: function(complexity) {
|
valid: function(complexity) {
|
||||||
|
|
|
@ -39,10 +39,10 @@ export default class Welcome extends React.PureComponent<IProps> {
|
||||||
// FIXME: Using an import will result in wrench-element-tests failures
|
// FIXME: Using an import will result in wrench-element-tests failures
|
||||||
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
|
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
|
||||||
|
|
||||||
const pagesConfig = SdkConfig.get().embeddedPages;
|
const pagesConfig = SdkConfig.getObject("embedded_pages");
|
||||||
let pageUrl = null;
|
let pageUrl = null;
|
||||||
if (pagesConfig) {
|
if (pagesConfig) {
|
||||||
pageUrl = pagesConfig.welcomeUrl;
|
pageUrl = pagesConfig.get("welcome_url");
|
||||||
}
|
}
|
||||||
if (!pageUrl) {
|
if (!pageUrl) {
|
||||||
pageUrl = 'welcome.html';
|
pageUrl = 'welcome.html';
|
||||||
|
|
|
@ -15,12 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import { SnakedObject } from "../../../utils/SnakedObject";
|
||||||
|
|
||||||
export enum ButtonClicked {
|
export enum ButtonClicked {
|
||||||
Primary,
|
Primary,
|
||||||
|
@ -96,8 +98,12 @@ const AnalyticsLearnMoreDialog: React.FC<IProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showDialog = (props: Omit<IProps, "cookiePolicyUrl" | "analyticsOwner">): void => {
|
export const showDialog = (props: Omit<IProps, "cookiePolicyUrl" | "analyticsOwner">): void => {
|
||||||
const privacyPolicyUrl = SdkConfig.get().piwik?.policyUrl;
|
const piwikConfig = SdkConfig.get("piwik");
|
||||||
const analyticsOwner = SdkConfig.get().analyticsOwner ?? SdkConfig.get().brand;
|
let privacyPolicyUrl: Optional<string>;
|
||||||
|
if (piwikConfig && typeof piwikConfig === "object") {
|
||||||
|
privacyPolicyUrl = (new SnakedObject(piwikConfig)).get("policy_url");
|
||||||
|
}
|
||||||
|
const analyticsOwner = SdkConfig.get("analytics_owner") ?? SdkConfig.get("brand");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
"Analytics Learn More",
|
"Analytics Learn More",
|
||||||
"",
|
"",
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildSuggestions(): IPerson[] {
|
private buildSuggestions(): IPerson[] {
|
||||||
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
|
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get("welcome_user_id")]);
|
||||||
if (this.props.roomId) {
|
if (this.props.roomId) {
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
||||||
|
|
|
@ -75,7 +75,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
joinRule = JoinRule.Restricted;
|
joinRule = JoinRule.Restricted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = SdkConfig.get();
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isPublic: this.props.defaultPublic || false,
|
isPublic: this.props.defaultPublic || false,
|
||||||
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(),
|
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(),
|
||||||
|
@ -84,7 +83,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
topic: "",
|
topic: "",
|
||||||
alias: "",
|
alias: "",
|
||||||
detailsOpen: false,
|
detailsOpen: false,
|
||||||
noFederate: config.default_federate === false,
|
noFederate: SdkConfig.get().default_federate === false,
|
||||||
nameIsValid: false,
|
nameIsValid: false,
|
||||||
canChangeEncryption: true,
|
canChangeEncryption: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,12 +28,13 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||||
import {
|
import {
|
||||||
IHostSignupConfig,
|
|
||||||
IPostmessage,
|
IPostmessage,
|
||||||
IPostmessageResponseData,
|
IPostmessageResponseData,
|
||||||
PostmessageAction,
|
PostmessageAction,
|
||||||
} from "./HostSignupDialogTypes";
|
} from "./HostSignupDialogTypes";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { IConfigOptions } from "../../../IConfigOptions";
|
||||||
|
import { SnakedObject } from "../../../utils/SnakedObject";
|
||||||
|
|
||||||
const HOST_SIGNUP_KEY = "host_signup";
|
const HOST_SIGNUP_KEY = "host_signup";
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ interface IState {
|
||||||
@replaceableComponent("views.dialogs.HostSignupDialog")
|
@replaceableComponent("views.dialogs.HostSignupDialog")
|
||||||
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
|
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
|
||||||
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
|
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||||
private readonly config: IHostSignupConfig;
|
private readonly config: SnakedObject<IConfigOptions["host_signup"]>;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -59,11 +60,11 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
||||||
minimized: false,
|
minimized: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.config = SdkConfig.get().hostSignup;
|
this.config = SdkConfig.getObject("host_signup");
|
||||||
}
|
}
|
||||||
|
|
||||||
private messageHandler = async (message: IPostmessage) => {
|
private messageHandler = async (message: IPostmessage) => {
|
||||||
if (!this.config.url.startsWith(message.origin)) {
|
if (!this.config.get("url").startsWith(message.origin)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (message.data.action) {
|
switch (message.data.action) {
|
||||||
|
@ -142,7 +143,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
||||||
};
|
};
|
||||||
|
|
||||||
private sendMessage = (message: IPostmessageResponseData) => {
|
private sendMessage = (message: IPostmessageResponseData) => {
|
||||||
this.iframeRef.current.contentWindow.postMessage(message, this.config.url);
|
this.iframeRef.current.contentWindow.postMessage(message, this.config.get("url"));
|
||||||
};
|
};
|
||||||
|
|
||||||
private async sendAccountDetails() {
|
private async sendAccountDetails() {
|
||||||
|
@ -176,12 +177,16 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAccountDetailsRequest = () => {
|
private onAccountDetailsRequest = () => {
|
||||||
|
const cookiePolicyUrl = this.config.get("cookie_policy_url");
|
||||||
|
const privacyPolicyUrl = this.config.get("privacy_policy_url");
|
||||||
|
const tosUrl = this.config.get("terms_of_service_url");
|
||||||
|
|
||||||
const textComponent = (
|
const textComponent = (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>
|
||||||
{ _t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
{ _t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
||||||
"account to fetch verified email addresses. This data is not stored.", {
|
"account to fetch verified email addresses. This data is not stored.", {
|
||||||
hostSignupBrand: this.config.brand,
|
hostSignupBrand: this.config.get("brand"),
|
||||||
}) }
|
}) }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -189,17 +194,17 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
cookiePolicyLink: () => (
|
cookiePolicyLink: () => (
|
||||||
<a href={this.config.cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
|
<a href={cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||||
{ _t("Cookie Policy") }
|
{ _t("Cookie Policy") }
|
||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
privacyPolicyLink: () => (
|
privacyPolicyLink: () => (
|
||||||
<a href={this.config.privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
|
<a href={privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||||
{ _t("Privacy Policy") }
|
{ _t("Privacy Policy") }
|
||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
termsOfServiceLink: () => (
|
termsOfServiceLink: () => (
|
||||||
<a href={this.config.termsOfServiceUrl} target="_blank" rel="noreferrer noopener">
|
<a href={tosUrl} target="_blank" rel="noreferrer noopener">
|
||||||
{ _t("Terms of Service") }
|
{ _t("Terms of Service") }
|
||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
|
@ -247,7 +252,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
||||||
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
|
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
|
||||||
<div className="mx_Dialog_title">
|
<div className="mx_Dialog_title">
|
||||||
{ _t("%(hostSignupBrand)s Setup", {
|
{ _t("%(hostSignupBrand)s Setup", {
|
||||||
hostSignupBrand: this.config.brand,
|
hostSignupBrand: this.config.get("brand"),
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
@ -284,10 +289,10 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
||||||
title={_t(
|
title={_t(
|
||||||
"Upgrade to %(hostSignupBrand)s",
|
"Upgrade to %(hostSignupBrand)s",
|
||||||
{
|
{
|
||||||
hostSignupBrand: this.config.brand,
|
hostSignupBrand: this.config.get("brand"),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
src={this.config.url}
|
src={this.config.get("url")}
|
||||||
ref={this.iframeRef}
|
ref={this.iframeRef}
|
||||||
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
|
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -45,12 +45,3 @@ export interface IPostmessage {
|
||||||
data: IPostmessageRequestData;
|
data: IPostmessageRequestData;
|
||||||
origin: string;
|
origin: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHostSignupConfig {
|
|
||||||
brand: string;
|
|
||||||
cookiePolicyUrl: string;
|
|
||||||
domains: Array<string>;
|
|
||||||
privacyPolicyUrl: string;
|
|
||||||
termsOfServiceUrl: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
|
@ -412,7 +412,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
|
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get("welcome_user_id")]);
|
||||||
if (props.roomId) {
|
if (props.roomId) {
|
||||||
const room = MatrixClientPeg.get().getRoom(props.roomId);
|
const room = MatrixClientPeg.get().getRoom(props.roomId);
|
||||||
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
||||||
|
|
|
@ -259,9 +259,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminMessageMD =
|
const adminMessageMD = SdkConfig
|
||||||
SdkConfig.get().reportEvent &&
|
.getObject("report_event")?.get("admin_message_md", "adminMessageMD");
|
||||||
SdkConfig.get().reportEvent.adminMessageMD;
|
|
||||||
let adminMessage;
|
let adminMessage;
|
||||||
if (adminMessageMD) {
|
if (adminMessageMD) {
|
||||||
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||||
|
@ -272,7 +271,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
// Display report-to-moderator dialog.
|
// Display report-to-moderator dialog.
|
||||||
// We let the user pick a nature.
|
// We let the user pick a nature.
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
|
const homeServerName = SdkConfig.get("validated_server_config").hsName;
|
||||||
let subtitle;
|
let subtitle;
|
||||||
switch (this.state.nature) {
|
switch (this.state.nature) {
|
||||||
case Nature.Disagreement:
|
case Nature.Disagreement:
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
this.defaultServer = config["validated_server_config"] as ValidatedServerConfig;
|
this.defaultServer = config["validated_server_config"];
|
||||||
const { serverConfig } = this.props;
|
const { serverConfig } = this.props;
|
||||||
|
|
||||||
let otherHomeserver = "";
|
let otherHomeserver = "";
|
||||||
|
|
|
@ -159,7 +159,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
"UserSettingsSecurityPrivacy",
|
"UserSettingsSecurityPrivacy",
|
||||||
));
|
));
|
||||||
// Show the Labs tab if enabled or if there are any active betas
|
// Show the Labs tab if enabled or if there are any active betas
|
||||||
if (SdkConfig.get()['showLabsSettings']
|
if (SdkConfig.get("show_labs_settings")
|
||||||
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
|
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
|
||||||
) {
|
) {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
|
|
|
@ -40,6 +40,8 @@ import TextInputDialog from "../dialogs/TextInputDialog";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
import { compare } from "../../../utils/strings";
|
import { compare } from "../../../utils/strings";
|
||||||
|
import { SnakedObject } from "../../../utils/SnakedObject";
|
||||||
|
import { IConfigOptions } from "../../../IConfigOptions";
|
||||||
|
|
||||||
// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage
|
// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage
|
||||||
export const ALL_ROOMS = "ALL_ROOMS";
|
export const ALL_ROOMS = "ALL_ROOMS";
|
||||||
|
@ -122,11 +124,11 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
||||||
// we either show the button or the dropdown in its place.
|
// we either show the button or the dropdown in its place.
|
||||||
let content;
|
let content;
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
const config = SdkConfig.get();
|
const roomDirectory = SdkConfig.getObject("room_directory")
|
||||||
const roomDirectory = config.roomDirectory || {};
|
?? new SnakedObject<IConfigOptions["room_directory"]>({ servers: [] });
|
||||||
|
|
||||||
const hsName = MatrixClientPeg.getHomeserverName();
|
const hsName = MatrixClientPeg.getHomeserverName();
|
||||||
const configServers = new Set<string>(roomDirectory.servers);
|
const configServers = new Set<string>(roomDirectory.get("servers"));
|
||||||
|
|
||||||
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
||||||
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
||||||
|
|
|
@ -59,21 +59,23 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { desktopBuilds, brand } = SdkConfig.get();
|
const brand = SdkConfig.get("brand");
|
||||||
|
const desktopBuilds = SdkConfig.getObject("desktop_builds");
|
||||||
|
|
||||||
let text = null;
|
let text = null;
|
||||||
let logo = null;
|
let logo = null;
|
||||||
if (desktopBuilds.available) {
|
if (desktopBuilds.get("available")) {
|
||||||
logo = <img src={desktopBuilds.logo} />;
|
logo = <img src={desktopBuilds.get("logo")} />;
|
||||||
|
const buildUrl = desktopBuilds.get("url");
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case WarningKind.Files:
|
case WarningKind.Files:
|
||||||
text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
|
text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
|
||||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{ sub }</a>),
|
a: sub => (<a href={buildUrl} target="_blank" rel="noreferrer noopener">{ sub }</a>),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case WarningKind.Search:
|
case WarningKind.Search:
|
||||||
text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
|
text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
|
||||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{ sub }</a>),
|
a: sub => (<a href={buildUrl} target="_blank" rel="noreferrer noopener">{ sub }</a>),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ const onHelpClick = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }: IProps) => {
|
const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }: IProps) => {
|
||||||
const disableCustomUrls = SdkConfig.get()["disable_custom_urls"];
|
const disableCustomUrls = SdkConfig.get("disable_custom_urls");
|
||||||
|
|
||||||
let editBtn;
|
let editBtn;
|
||||||
if (!disableCustomUrls && onServerConfigChange) {
|
if (!disableCustomUrls && onServerConfigChange) {
|
||||||
|
|
|
@ -1575,7 +1575,7 @@ const UserInfoHeader: React.FC<{
|
||||||
presenceCurrentlyActive = member.user.currentlyActive;
|
presenceCurrentlyActive = member.user.currentlyActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
const enablePresenceByHsUrl = SdkConfig.get("enable_presence_by_hs_url");
|
||||||
let showPresence = true;
|
let showPresence = true;
|
||||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
|
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
|
||||||
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
||||||
|
|
|
@ -95,7 +95,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.on(ClientEvent.Room, this.onRoom); // invites & joining after peek
|
cli.on(ClientEvent.Room, this.onRoom); // invites & joining after peek
|
||||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
const enablePresenceByHsUrl = SdkConfig.get("enable_presence_by_hs_url");
|
||||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||||
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
private onStartBotChat = (e) => {
|
private onStartBotChat = (e) => {
|
||||||
this.props.closeSettingsFn();
|
this.props.closeSettingsFn();
|
||||||
createRoom({
|
createRoom({
|
||||||
dmUserId: SdkConfig.get().welcomeUserId,
|
dmUserId: SdkConfig.get("welcome_user_id"),
|
||||||
andView: true,
|
andView: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -105,7 +105,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
if (!tocLinks) return null;
|
if (!tocLinks) return null;
|
||||||
|
|
||||||
const legalLinks = [];
|
const legalLinks = [];
|
||||||
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
|
for (const tocEntry of tocLinks) {
|
||||||
legalLinks.push(<div key={tocEntry.url}>
|
legalLinks.push(<div key={tocEntry.url}>
|
||||||
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{ tocEntry.text }</a>
|
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{ tocEntry.text }</a>
|
||||||
</div>);
|
</div>);
|
||||||
|
@ -198,7 +198,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
</a>,
|
</a>,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
|
if (SdkConfig.get("welcome_user_id") && getCurrentLanguage().startsWith('en')) {
|
||||||
faqText = (
|
faqText = (
|
||||||
<div>
|
<div>
|
||||||
{ _t(
|
{ _t(
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface IKeyboardShortcutRowProps {
|
||||||
|
|
||||||
// Filter out the labs section if labs aren't enabled.
|
// Filter out the labs section if labs aren't enabled.
|
||||||
const visibleCategories = Object.entries(CATEGORIES).filter(([categoryName]) =>
|
const visibleCategories = Object.entries(CATEGORIES).filter(([categoryName]) =>
|
||||||
categoryName !== CategoryName.LABS || SdkConfig.get()['showLabsSettings']);
|
categoryName !== CategoryName.LABS || SdkConfig.get("show_labs_settings"));
|
||||||
|
|
||||||
const KeyboardShortcutRow: React.FC<IKeyboardShortcutRowProps> = ({ name }) => {
|
const KeyboardShortcutRow: React.FC<IKeyboardShortcutRowProps> = ({ name }) => {
|
||||||
const displayName = getKeyboardShortcutDisplayName(name);
|
const displayName = getKeyboardShortcutDisplayName(name);
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let labsSection;
|
let labsSection;
|
||||||
if (SdkConfig.get()['showLabsSettings']) {
|
if (SdkConfig.get("show_labs_settings")) {
|
||||||
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
|
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
|
||||||
labs.forEach(f => {
|
labs.forEach(f => {
|
||||||
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
|
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
|
||||||
|
|
|
@ -95,7 +95,7 @@ export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false }
|
||||||
patternNames.forEach(function(patternName) {
|
patternNames.forEach(function(patternName) {
|
||||||
patternTypes.forEach(function(patternType) {
|
patternTypes.forEach(function(patternType) {
|
||||||
// get the regex replace pattern from config or use the default
|
// get the regex replace pattern from config or use the default
|
||||||
const pattern = (((SdkConfig.get()["latex_maths_delims"] ||
|
const pattern = (((SdkConfig.get("latex_maths_delims") ||
|
||||||
{})[patternType] || {})["pattern"] || {})[patternName] ||
|
{})[patternType] || {})["pattern"] || {})[patternName] ||
|
||||||
patternDefaults[patternName][patternType];
|
patternDefaults[patternName][patternType];
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,8 @@ export class IntegrationManagers {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupConfiguredManager() {
|
private setupConfiguredManager() {
|
||||||
const apiUrl: string = SdkConfig.get()['integrations_rest_url'];
|
const apiUrl: string = SdkConfig.get("integrations_rest_url");
|
||||||
const uiUrl: string = SdkConfig.get()['integrations_ui_url'];
|
const uiUrl: string = SdkConfig.get("integrations_ui_url");
|
||||||
|
|
||||||
if (apiUrl && uiUrl) {
|
if (apiUrl && uiUrl) {
|
||||||
this.managers.push(new IntegrationManagerInstance(Kind.Config, apiUrl, uiUrl));
|
this.managers.push(new IntegrationManagerInstance(Kind.Config, apiUrl, uiUrl));
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
import { IConfigOptions } from "./IConfigOptions";
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
@ -173,7 +174,7 @@ async function getContexts(): Promise<Contexts> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendSentryReport(userText: string, issueUrl: string, error: Error): Promise<void> {
|
export async function sendSentryReport(userText: string, issueUrl: string, error: Error): Promise<void> {
|
||||||
const sentryConfig = SdkConfig.get()["sentry"];
|
const sentryConfig = SdkConfig.getObject("sentry");
|
||||||
if (!sentryConfig) return;
|
if (!sentryConfig) return;
|
||||||
|
|
||||||
const captureContext = {
|
const captureContext = {
|
||||||
|
@ -198,12 +199,7 @@ export function setSentryUser(mxid: string): void {
|
||||||
Sentry.setUser({ username: mxid });
|
Sentry.setUser({ username: mxid });
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISentryConfig {
|
export async function initSentry(sentryConfig: IConfigOptions["sentry"]): Promise<void> {
|
||||||
dsn: string;
|
|
||||||
environment?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initSentry(sentryConfig: ISentryConfig): Promise<void> {
|
|
||||||
if (!sentryConfig) return;
|
if (!sentryConfig) return;
|
||||||
// Only enable Integrations.GlobalHandlers, which hooks uncaught exceptions, if automaticErrorReporting is true
|
// Only enable Integrations.GlobalHandlers, which hooks uncaught exceptions, if automaticErrorReporting is true
|
||||||
const integrations = [
|
const integrations = [
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import SettingsHandler from "./SettingsHandler";
|
import SettingsHandler from "./SettingsHandler";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
import { SnakedObject } from "../../utils/SnakedObject";
|
||||||
|
import { IConfigOptions } from "../../IConfigOptions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets and sets settings at the "config" level. This handler does not make use of the
|
* Gets and sets settings at the "config" level. This handler does not make use of the
|
||||||
|
@ -30,10 +32,10 @@ export default class ConfigSettingsHandler extends SettingsHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getValue(settingName: string, roomId: string): any {
|
public getValue(settingName: string, roomId: string): any {
|
||||||
const config = SdkConfig.get() || {};
|
const config = new SnakedObject<IConfigOptions>(SdkConfig.get());
|
||||||
|
|
||||||
if (this.featureNames.includes(settingName)) {
|
if (this.featureNames.includes(settingName)) {
|
||||||
const labsConfig = config["features"] || {};
|
const labsConfig = config.get("features") || {};
|
||||||
const val = labsConfig[settingName];
|
const val = labsConfig[settingName];
|
||||||
if (isNullOrUndefined(val)) return null; // no definition at this level
|
if (isNullOrUndefined(val)) return null; // no definition at this level
|
||||||
if (val === true || val === false) return val; // new style: mapped as a boolean
|
if (val === true || val === false) return val; // new style: mapped as a boolean
|
||||||
|
@ -45,10 +47,10 @@ export default class ConfigSettingsHandler extends SettingsHandler {
|
||||||
|
|
||||||
// Special case themes
|
// Special case themes
|
||||||
if (settingName === "theme") {
|
if (settingName === "theme") {
|
||||||
return config["default_theme"];
|
return config.get("default_theme");
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsConfig = config["settingDefaults"];
|
const settingsConfig = config.get("setting_defaults");
|
||||||
if (!settingsConfig || isNullOrUndefined(settingsConfig[settingName])) return null;
|
if (!settingsConfig || isNullOrUndefined(settingsConfig[settingName])) return null;
|
||||||
return settingsConfig[settingName];
|
return settingsConfig[settingName];
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
|
@ -28,6 +29,8 @@ import {
|
||||||
showDialog as showAnalyticsLearnMoreDialog,
|
showDialog as showAnalyticsLearnMoreDialog,
|
||||||
} from "../components/views/dialogs/AnalyticsLearnMoreDialog";
|
} from "../components/views/dialogs/AnalyticsLearnMoreDialog";
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
|
import { SnakedObject } from "../utils/SnakedObject";
|
||||||
|
import { IConfigOptions } from "../IConfigOptions";
|
||||||
|
|
||||||
const onAccept = () => {
|
const onAccept = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -81,7 +84,12 @@ const TOAST_KEY = "analytics";
|
||||||
const getAnonymousDescription = (): ReactNode => {
|
const getAnonymousDescription = (): ReactNode => {
|
||||||
// get toast description for anonymous tracking (the previous scheme pre-posthog)
|
// get toast description for anonymous tracking (the previous scheme pre-posthog)
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const cookiePolicyUrl = SdkConfig.get().piwik?.policyUrl;
|
const piwikConfig = SdkConfig.get("piwik");
|
||||||
|
let piwik: Optional<SnakedObject<Extract<IConfigOptions["piwik"], object>>>;
|
||||||
|
if (typeof piwikConfig === 'object') {
|
||||||
|
piwik = new SnakedObject(piwikConfig);
|
||||||
|
}
|
||||||
|
const cookiePolicyUrl = piwik?.get("policy_url");
|
||||||
return _t(
|
return _t(
|
||||||
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. " +
|
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. " +
|
||||||
"This will use a <PolicyLink>cookie</PolicyLink>.",
|
"This will use a <PolicyLink>cookie</PolicyLink>.",
|
||||||
|
@ -100,7 +108,7 @@ const getAnonymousDescription = (): ReactNode => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const showToast = (props: Omit<React.ComponentProps<typeof GenericToast>, "toastKey">) => {
|
const showToast = (props: Omit<React.ComponentProps<typeof GenericToast>, "toastKey">) => {
|
||||||
const analyticsOwner = SdkConfig.get().analyticsOwner ?? SdkConfig.get().brand;
|
const analyticsOwner = SdkConfig.get("analytics_owner") ?? SdkConfig.get().brand;
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
key: TOAST_KEY,
|
key: TOAST_KEY,
|
||||||
title: _t("Help improve %(analyticsOwner)s", { analyticsOwner }),
|
title: _t("Help improve %(analyticsOwner)s", { analyticsOwner }),
|
||||||
|
|
|
@ -207,7 +207,7 @@ export default class AutoDiscoveryUtils {
|
||||||
const hsResult = discoveryResult['m.homeserver'];
|
const hsResult = discoveryResult['m.homeserver'];
|
||||||
const isResult = discoveryResult['m.identity_server'];
|
const isResult = discoveryResult['m.identity_server'];
|
||||||
|
|
||||||
const defaultConfig = SdkConfig.get()["validated_server_config"];
|
const defaultConfig = SdkConfig.get("validated_server_config");
|
||||||
|
|
||||||
// Validate the identity server first because an invalid identity server causes
|
// Validate the identity server first because an invalid identity server causes
|
||||||
// an invalid homeserver, which may not be picked up correctly.
|
// an invalid homeserver, which may not be picked up correctly.
|
||||||
|
|
|
@ -21,7 +21,7 @@ import SdkConfig from '../SdkConfig';
|
||||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
|
||||||
export function getDefaultIdentityServerUrl(): string {
|
export function getDefaultIdentityServerUrl(): string {
|
||||||
return SdkConfig.get()['validated_server_config']['isUrl'];
|
return SdkConfig.get("validated_server_config").isUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDefaultIdentityServer(): void {
|
export function useDefaultIdentityServer(): void {
|
||||||
|
|
36
src/utils/SnakedObject.ts
Normal file
36
src/utils/SnakedObject.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 function snakeToCamel(s: string): string {
|
||||||
|
return s.replace(/._./g, v => `${v[0]}${v[2].toUpperCase()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SnakedObject<T = Record<string, any>> {
|
||||||
|
public constructor(private obj: T) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public get<K extends string & keyof T>(key: K, altCaseName?: string): T[K] {
|
||||||
|
const val = this.obj[key];
|
||||||
|
if (val !== undefined) return val;
|
||||||
|
|
||||||
|
return this.obj[altCaseName ?? snakeToCamel(key)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make JSON.stringify() pretend that everything is fine
|
||||||
|
public toJSON() {
|
||||||
|
return this.obj;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,23 +14,37 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ConfigOptions } from "../SdkConfig";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
export function getHomePageUrl(appConfig: ConfigOptions): string | null {
|
import { IConfigOptions } from "../IConfigOptions";
|
||||||
const pagesConfig = appConfig.embeddedPages;
|
import { SnakedObject } from "./SnakedObject";
|
||||||
let pageUrl = pagesConfig?.homeUrl;
|
|
||||||
|
export function getHomePageUrl(appConfig: IConfigOptions): string | null {
|
||||||
|
const config = new SnakedObject(appConfig);
|
||||||
|
|
||||||
|
const pagesConfig = config.get("embedded_pages");
|
||||||
|
let pageUrl = pagesConfig ? (new SnakedObject(pagesConfig).get("home_url")) : null;
|
||||||
|
|
||||||
if (!pageUrl) {
|
if (!pageUrl) {
|
||||||
// This is a deprecated config option for the home page
|
// This is a deprecated config option for the home page
|
||||||
// (despite the name, given we also now have a welcome
|
// (despite the name, given we also now have a welcome
|
||||||
// page, which is not the same).
|
// page, which is not the same).
|
||||||
pageUrl = appConfig.welcomePageUrl;
|
pageUrl = (<any>appConfig).welcomePageUrl;
|
||||||
|
if (pageUrl) {
|
||||||
|
logger.warn(
|
||||||
|
"You are using a deprecated config option: `welcomePageUrl`. Please use " +
|
||||||
|
"`embedded_pages.home_url` instead, per https://github.com/vector-im/element-web/issues/21428",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageUrl;
|
return pageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldUseLoginForWelcome(appConfig: ConfigOptions): boolean {
|
export function shouldUseLoginForWelcome(appConfig: IConfigOptions): boolean {
|
||||||
const pagesConfig = appConfig.embeddedPages;
|
const config = new SnakedObject(appConfig);
|
||||||
return pagesConfig?.loginForWelcome === true;
|
const pagesConfig = config.get("embedded_pages");
|
||||||
|
return pagesConfig
|
||||||
|
? ((new SnakedObject(pagesConfig).get("login_for_welcome")) === true)
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -413,7 +413,7 @@ export function getPrimaryPermalinkEntity(permalink: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPermalinkConstructor(): PermalinkConstructor {
|
function getPermalinkConstructor(): PermalinkConstructor {
|
||||||
const elementPrefix = SdkConfig.get()['permalinkPrefix'];
|
const elementPrefix = SdkConfig.get("permalink_prefix");
|
||||||
if (elementPrefix && elementPrefix !== matrixtoBaseUrl) {
|
if (elementPrefix && elementPrefix !== matrixtoBaseUrl) {
|
||||||
return new ElementPermalinkConstructor(elementPrefix);
|
return new ElementPermalinkConstructor(elementPrefix);
|
||||||
}
|
}
|
||||||
|
@ -423,7 +423,7 @@ function getPermalinkConstructor(): PermalinkConstructor {
|
||||||
|
|
||||||
export function parsePermalink(fullUrl: string): PermalinkParts {
|
export function parsePermalink(fullUrl: string): PermalinkParts {
|
||||||
try {
|
try {
|
||||||
const elementPrefix = SdkConfig.get()['permalinkPrefix'];
|
const elementPrefix = SdkConfig.get("permalink_prefix");
|
||||||
if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) {
|
if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) {
|
||||||
return new MatrixToPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl));
|
return new MatrixToPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl));
|
||||||
} else if (fullUrl.startsWith("matrix:")) {
|
} else if (fullUrl.startsWith("matrix:")) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import SdkConfig from "../SdkConfig";
|
||||||
|
|
||||||
export function isPresenceEnabled() {
|
export function isPresenceEnabled() {
|
||||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||||
const urls = SdkConfig.get()['enable_presence_by_hs_url'];
|
const urls = SdkConfig.get("enable_presence_by_hs_url");
|
||||||
if (!urls) return true;
|
if (!urls) return true;
|
||||||
if (urls[hsUrl] || urls[hsUrl] === undefined) return true;
|
if (urls[hsUrl] || urls[hsUrl] === undefined) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -70,7 +70,7 @@ export class Jitsi {
|
||||||
|
|
||||||
private update = async (discoveryResponse: IClientWellKnown): Promise<any> => {
|
private update = async (discoveryResponse: IClientWellKnown): Promise<any> => {
|
||||||
// Start with a default of the config's domain
|
// Start with a default of the config's domain
|
||||||
let domain = SdkConfig.get().jitsi?.preferredDomain || "meet.element.io";
|
let domain = SdkConfig.getObject("jitsi")?.get("preferred_domain") || "meet.element.io";
|
||||||
|
|
||||||
logger.log("Attempting to get Jitsi conference information from homeserver");
|
logger.log("Attempting to get Jitsi conference information from homeserver");
|
||||||
const wkPreferredDomain = discoveryResponse?.[JITSI_WK_PROPERTY]?.['preferredDomain'];
|
const wkPreferredDomain = discoveryResponse?.[JITSI_WK_PROPERTY]?.['preferredDomain'];
|
||||||
|
|
|
@ -72,20 +72,23 @@ describe("PosthogAnalytics", () => {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.crypto = null;
|
window.crypto = null;
|
||||||
|
SdkConfig.unset(); // we touch the config, so clean up
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Initialisation", () => {
|
describe("Initialisation", () => {
|
||||||
it("Should not be enabled without config being set", () => {
|
it("Should not be enabled without config being set", () => {
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({});
|
// force empty/invalid state for posthog options
|
||||||
|
SdkConfig.put({ brand: "Testing" });
|
||||||
const analytics = new PosthogAnalytics(fakePosthog);
|
const analytics = new PosthogAnalytics(fakePosthog);
|
||||||
expect(analytics.isEnabled()).toBe(false);
|
expect(analytics.isEnabled()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should be enabled if config is set", () => {
|
it("Should be enabled if config is set", () => {
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({
|
SdkConfig.put({
|
||||||
|
brand: "Testing",
|
||||||
posthog: {
|
posthog: {
|
||||||
projectApiKey: "foo",
|
project_api_key: "foo",
|
||||||
apiHost: "bar",
|
api_host: "bar",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const analytics = new PosthogAnalytics(fakePosthog);
|
const analytics = new PosthogAnalytics(fakePosthog);
|
||||||
|
@ -98,10 +101,11 @@ describe("PosthogAnalytics", () => {
|
||||||
let analytics: PosthogAnalytics;
|
let analytics: PosthogAnalytics;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({
|
SdkConfig.put({
|
||||||
|
brand: "Testing",
|
||||||
posthog: {
|
posthog: {
|
||||||
projectApiKey: "foo",
|
project_api_key: "foo",
|
||||||
apiHost: "bar",
|
api_host: "bar",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -42,9 +42,9 @@ describe('Login', function() {
|
||||||
} as unknown as MatrixClient);
|
} as unknown as MatrixClient);
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({
|
SdkConfig.put({
|
||||||
|
brand: "test-brand",
|
||||||
disable_custom_urls: true,
|
disable_custom_urls: true,
|
||||||
brand: 'test-brand',
|
|
||||||
});
|
});
|
||||||
mockClient.login.mockClear().mockResolvedValue({});
|
mockClient.login.mockClear().mockResolvedValue({});
|
||||||
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] });
|
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] });
|
||||||
|
@ -57,6 +57,7 @@ describe('Login', function() {
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
ReactDOM.unmountComponentAtNode(parentDiv);
|
ReactDOM.unmountComponentAtNode(parentDiv);
|
||||||
parentDiv.remove();
|
parentDiv.remove();
|
||||||
|
SdkConfig.unset(); // we touch the config, so clean up
|
||||||
});
|
});
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
@ -69,9 +70,9 @@ describe('Login', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should show form with change server link', async () => {
|
it('should show form with change server link', async () => {
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({
|
SdkConfig.put({
|
||||||
|
brand: "test-brand",
|
||||||
disable_custom_urls: false,
|
disable_custom_urls: false,
|
||||||
brand: 'test',
|
|
||||||
});
|
});
|
||||||
const root = render();
|
const root = render();
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ describe('Registration', function() {
|
||||||
let parentDiv;
|
let parentDiv;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({
|
SdkConfig.put({
|
||||||
...DEFAULTS,
|
...DEFAULTS,
|
||||||
disable_custom_urls: true,
|
disable_custom_urls: true,
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,7 @@ describe('Registration', function() {
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
ReactDOM.unmountComponentAtNode(parentDiv);
|
ReactDOM.unmountComponentAtNode(parentDiv);
|
||||||
parentDiv.remove();
|
parentDiv.remove();
|
||||||
|
SdkConfig.unset(); // we touch the config, so clean up
|
||||||
});
|
});
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
|
63
test/utils/SnakedObject-test.ts
Normal file
63
test/utils/SnakedObject-test.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 { SnakedObject, snakeToCamel } from "../../src/utils/SnakedObject";
|
||||||
|
|
||||||
|
describe('snakeToCamel', () => {
|
||||||
|
it('should convert snake_case to camelCase in simple scenarios', () => {
|
||||||
|
expect(snakeToCamel("snake_case")).toBe("snakeCase");
|
||||||
|
expect(snakeToCamel("snake_case_but_longer")).toBe("snakeCaseButLonger");
|
||||||
|
expect(snakeToCamel("numbered_123")).toBe("numbered123"); // not a thing we would see normally
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not really something we expect to see, but it's defined behaviour of the function
|
||||||
|
it('should not camelCase a trailing or leading underscore', () => {
|
||||||
|
expect(snakeToCamel("_snake")).toBe("_snake");
|
||||||
|
expect(snakeToCamel("snake_")).toBe("snake_");
|
||||||
|
expect(snakeToCamel("_snake_case")).toBe("_snakeCase");
|
||||||
|
expect(snakeToCamel("snake_case_")).toBe("snakeCase_");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Another thing we don't really expect to see, but is "defined behaviour"
|
||||||
|
it('should be predictable with double underscores', () => {
|
||||||
|
expect(snakeToCamel("__snake__")).toBe("_Snake_");
|
||||||
|
expect(snakeToCamel("snake__case")).toBe("snake_case");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SnakedObject', () => {
|
||||||
|
/* eslint-disable camelcase*/
|
||||||
|
const input = {
|
||||||
|
snake_case: "woot",
|
||||||
|
snakeCase: "oh no", // ensure different value from snake_case for tests
|
||||||
|
camelCase: "fallback",
|
||||||
|
};
|
||||||
|
const snake = new SnakedObject(input);
|
||||||
|
/* eslint-enable camelcase*/
|
||||||
|
|
||||||
|
it('should prefer snake_case keys', () => {
|
||||||
|
expect(snake.get("snake_case")).toBe(input.snake_case);
|
||||||
|
expect(snake.get("snake_case", "camelCase")).toBe(input.snake_case);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to camelCase keys when needed', () => {
|
||||||
|
// @ts-ignore - we're deliberately supplying a key that doesn't exist
|
||||||
|
expect(snake.get("camel_case")).toBe(input.camelCase);
|
||||||
|
|
||||||
|
// @ts-ignore - we're deliberately supplying a key that doesn't exist
|
||||||
|
expect(snake.get("e_no_exist", "camelCase")).toBe(input.camelCase);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue