diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f8cd67e..765e758 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,8 +3,6 @@ on: pull_request: {} push: branches: [main] -env: - VITE_DEFAULT_HOMESERVER: "https://call.ems.host" jobs: build: name: Build diff --git a/config/element_io_develop.json b/config/element_io_develop.json index 15198ec..caeddc6 100644 --- a/config/element_io_develop.json +++ b/config/element_io_develop.json @@ -1,4 +1,10 @@ { + "default_server_config": { + "m.homeserver": { + "base_url": "https://call.ems.host", + "server_name": "call.ems.host" + } + }, "posthog": { "api_key": "phc_MhClVy9DiV20vazSYIiedFkM5Xi3z1LPBwrdn9PYZQQ", "api_host": "https://app.posthog.com" diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index a48101f..fa83f5d 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -32,7 +32,6 @@ import { useTranslation } from "react-i18next"; import { ErrorView } from "./FullScreenView"; import { initClient, - defaultHomeserver, CryptoStoreIntegrityError, fallbackICEServerAllowed, } from "./matrix-utils"; @@ -40,6 +39,7 @@ import { widget } from "./widget"; import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics"; import { translatedError } from "./TranslatedError"; import { useEventTarget } from "./useEvents"; +import { Config } from "./config/Config"; declare global { interface Window { @@ -139,7 +139,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { return { client: await initClient( { - baseUrl: defaultHomeserver, + baseUrl: Config.defaultHomeserverUrl(), accessToken: access_token, userId: user_id, deviceId: device_id, @@ -155,7 +155,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { try { const client = await initClient( { - baseUrl: defaultHomeserver, + baseUrl: Config.defaultHomeserverUrl(), accessToken: access_token, userId: user_id, deviceId: device_id, diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 0e40ab0..9e5b245 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -114,8 +114,8 @@ export class PosthogAnalytics { constructor(private readonly posthog: PostHog) { const posthogConfig: PosthogSettings = { - project_api_key: Config.instance.config.posthog?.api_key, - api_host: Config.instance.config.posthog?.api_host, + project_api_key: Config.get().posthog?.api_key, + api_host: Config.get().posthog?.api_host, }; if (posthogConfig.project_api_key && posthogConfig.api_host) { diff --git a/src/auth/LoginPage.tsx b/src/auth/LoginPage.tsx index 3dc4840..88296db 100644 --- a/src/auth/LoginPage.tsx +++ b/src/auth/LoginPage.tsx @@ -14,14 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { - FC, - FormEvent, - useCallback, - useRef, - useState, - useMemo, -} from "react"; +import React, { FC, FormEvent, useCallback, useRef, useState } from "react"; import { useHistory, useLocation, Link } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; @@ -29,11 +22,11 @@ import { ReactComponent as Logo } from "../icons/LogoLarge.svg"; import { useClient } from "../ClientContext"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { Button } from "../button"; -import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils"; import styles from "./LoginPage.module.css"; import { useInteractiveLogin } from "./useInteractiveLogin"; import { usePageTitle } from "../usePageTitle"; import { PosthogAnalytics } from "../PosthogAnalytics"; +import { Config } from "../config/Config"; export const LoginPage: FC = () => { const { t } = useTranslation(); @@ -41,7 +34,7 @@ export const LoginPage: FC = () => { const { setClient } = useClient(); const login = useInteractiveLogin(); - const homeserver = defaultHomeserver; // TODO: Make this configurable + const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable const usernameRef = useRef<HTMLInputElement>(); const passwordRef = useRef<HTMLInputElement>(); const history = useHistory(); @@ -75,14 +68,6 @@ export const LoginPage: FC = () => { [login, location, history, homeserver, setClient] ); - const homeserverHost = useMemo(() => { - try { - return new URL(homeserver).host; - } catch (error) { - return defaultHomeserverHost; - } - }, [homeserver]); - return ( <> <div className={styles.container}> @@ -102,7 +87,7 @@ export const LoginPage: FC = () => { autoCorrect="off" autoCapitalize="none" prefix="@" - suffix={`:${homeserverHost}`} + suffix={`:${Config.defaultServerName()}`} /> </FieldRow> <FieldRow> diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index 9da1d32..f5025b8 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -31,7 +31,6 @@ import { Trans, useTranslation } from "react-i18next"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { Button } from "../button"; import { useClient } from "../ClientContext"; -import { defaultHomeserverHost } from "../matrix-utils"; import { useInteractiveRegistration } from "./useInteractiveRegistration"; import styles from "./LoginPage.module.css"; import { ReactComponent as Logo } from "../icons/LogoLarge.svg"; @@ -40,6 +39,7 @@ import { useRecaptcha } from "./useRecaptcha"; import { Caption, Link } from "../typography/Typography"; import { usePageTitle } from "../usePageTitle"; import { PosthogAnalytics } from "../PosthogAnalytics"; +import { Config } from "../config/Config"; export const RegisterPage: FC = () => { const { t } = useTranslation(); @@ -165,7 +165,7 @@ export const RegisterPage: FC = () => { autoCorrect="off" autoCapitalize="none" prefix="@" - suffix={`:${defaultHomeserverHost}`} + suffix={`:${Config.defaultServerName()}`} /> </FieldRow> <FieldRow> diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index ae0c855..4db466e 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -18,7 +18,7 @@ import { useCallback } from "react"; import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth"; import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { initClient, defaultHomeserver } from "../matrix-utils"; +import { initClient } from "../matrix-utils"; import { Session } from "../ClientContext"; export const useInteractiveLogin = () => @@ -59,7 +59,7 @@ export const useInteractiveLogin = () => const client = await initClient( { - baseUrl: defaultHomeserver, + baseUrl: homeserver, accessToken: access_token, userId: user_id, deviceId: device_id, diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index 6b25c29..bfc7b2a 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -18,8 +18,9 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth"; import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { initClient, defaultHomeserver } from "../matrix-utils"; +import { initClient } from "../matrix-utils"; import { Session } from "../ClientContext"; +import { Config } from "../config/Config"; export const useInteractiveRegistration = (): [ string, @@ -37,7 +38,9 @@ export const useInteractiveRegistration = (): [ const authClient = useRef<MatrixClient>(); if (!authClient.current) { - authClient.current = createClient({ baseUrl: defaultHomeserver }); + authClient.current = createClient({ + baseUrl: Config.defaultHomeserverUrl(), + }); } useEffect(() => { @@ -92,7 +95,7 @@ export const useInteractiveRegistration = (): [ const client = await initClient( { - baseUrl: defaultHomeserver, + baseUrl: Config.defaultHomeserverUrl(), accessToken: access_token, userId: user_id, deviceId: device_id, diff --git a/src/config/Config.ts b/src/config/Config.ts index 2806a5b..cabd460 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -22,13 +22,15 @@ import { export class Config { private static internalInstance: Config; - public static get instance(): Config { - if (!this.internalInstance) + + public static get(): ConfigOptions { + if (!this.internalInstance?.config) throw new Error("Config instance read before config got initialized"); - return this.internalInstance; + return this.internalInstance.config; } + public static init(): Promise<void> { - if (Config?.internalInstance?.initPromise) { + if (Config.internalInstance?.initPromise) { return Config.internalInstance.initPromise; } Config.internalInstance = new Config(); @@ -41,8 +43,17 @@ export class Config { return Config.internalInstance.initPromise; } - public config: ResolvedConfigOptions; - private initPromise: Promise<void>; + // Convenience accessors + public static defaultHomeserverUrl(): string | undefined { + return Config.get().default_server_config["m.homeserver"].base_url; + } + + public static defaultServerName(): string | undefined { + return Config.get().default_server_config["m.homeserver"].server_name; + } + + public config?: ResolvedConfigOptions; + private initPromise?: Promise<void>; } async function downloadConfig( @@ -59,7 +70,7 @@ async function downloadConfig( // Lack of a config isn't an error, we should just use the defaults. // Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file: // URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI. - return {}; + return DEFAULT_CONFIG; } return res.json(); diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 92dd74f..e799d59 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -19,21 +19,33 @@ export interface ConfigOptions { rageshake?: { submit_url: string; }; + + // Describes the default homeserver to use. The same format as Element Web + // (without identity servers as we don't use them). + default_server_config?: { + ["m.homeserver"]: { + base_url: string; + server_name: string; + }; + }; } +// Overrides members from ConfigOptions that are always provided by the +// default config and are therefore non-optional. export interface ResolvedConfigOptions extends ConfigOptions { - sentry: { - DSN: string; - environment: string; - }; - rageshake: { - submit_url: string; + default_server_config: { + ["m.homeserver"]: { + base_url: string; + server_name: string; + }; }; } export const DEFAULT_CONFIG: ResolvedConfigOptions = { - sentry: { DSN: "", environment: "production" }, - rageshake: { - submit_url: "https://element.io/bugreports/submit", + default_server_config: { + ["m.homeserver"]: { + base_url: "http://localhost:8008", + server_name: "localhost", + }, }, }; diff --git a/src/initializer.tsx b/src/initializer.tsx index 6290764..63b90fc 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -23,7 +23,6 @@ import * as Sentry from "@sentry/react"; import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; -import { DEFAULT_CONFIG } from "./config/ConfigOptions"; enum LoadState { None, @@ -192,19 +191,21 @@ export class Initializer { this.loadStates.sentry === LoadState.None && this.loadStates.config === LoadState.Loaded ) { - Sentry.init({ - dsn: Config.instance.config.sentry?.DSN ?? DEFAULT_CONFIG.sentry.DSN, - environment: - Config.instance.config.sentry.environment ?? - DEFAULT_CONFIG.sentry.environment, - integrations: [ - new Integrations.BrowserTracing({ - routingInstrumentation: - Sentry.reactRouterV5Instrumentation(history), - }), - ], - tracesSampleRate: 1.0, - }); + if (Config.get().sentry?.DSN && Config.get().sentry?.environment) { + Sentry.init({ + dsn: Config.get().sentry?.DSN, + environment: Config.get().sentry?.environment, + integrations: [ + new Integrations.BrowserTracing({ + routingInstrumentation: + Sentry.reactRouterV5Instrumentation(history), + }), + ], + tracesSampleRate: 1.0, + }); + } + // Sentry is now 'loadeed' (even if we actually skipped starting + // it due to to not being configured) this.loadStates.sentry = LoadState.Loaded; } diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index fdb5938..2d124a0 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -19,15 +19,11 @@ import type { Room } from "matrix-js-sdk/src/models/room"; import IndexedDBWorker from "./IndexedDBWorker?worker"; import { getUrlParams } from "./UrlParams"; import { loadOlm } from "./olm"; +import { Config } from "./config/Config"; -export const defaultHomeserver = - (import.meta.env.VITE_DEFAULT_HOMESERVER as string) ?? - `${window.location.protocol}//${window.location.host}`; export const fallbackICEServerAllowed = import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true"; -export const defaultHomeserverHost = new URL(defaultHomeserver).host; - export class CryptoStoreIntegrityError extends Error { constructor() { super("Crypto store data was expected, but none was found"); @@ -206,7 +202,7 @@ export function roomNameFromRoomId(roomId: string): string { .toLowerCase(); } -export function isLocalRoomId(roomId: string): boolean { +export function isLocalRoomId(roomId: string, client: MatrixClient): boolean { if (!roomId) { return false; } @@ -217,7 +213,7 @@ export function isLocalRoomId(roomId: string): boolean { return false; } - return parts[1] === defaultHomeserverHost; + return parts[1] === client.getDomain(); } export async function createRoom( @@ -291,11 +287,12 @@ export async function createRoom( return [fullAliasFromRoomName(name, client), result.room_id]; } +// Returns a URL to that will load Element Call with the given room export function getRoomUrl(roomIdOrAlias: string): string { if (roomIdOrAlias.startsWith("#")) { const [localPart, host] = roomIdOrAlias.replace("#", "").split(":"); - if (host !== defaultHomeserverHost) { + if (host !== Config.defaultServerName()) { return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`; } else { return `${window.location.protocol}//${window.location.host}/${localPart}`; diff --git a/src/room/RoomRedirect.tsx b/src/room/RoomRedirect.tsx index eeef083..f52abf6 100644 --- a/src/room/RoomRedirect.tsx +++ b/src/room/RoomRedirect.tsx @@ -17,9 +17,11 @@ limitations under the License. import React, { useEffect } from "react"; import { useLocation, useHistory } from "react-router-dom"; -import { defaultHomeserverHost } from "../matrix-utils"; +import { Config } from "../config/Config"; import { LoadingView } from "../FullScreenView"; +// A component that, when loaded, redirects the client to a full room URL +// based on the current URL being an abbreviated room URL export function RoomRedirect() { const { pathname } = useLocation(); const history = useHistory(); @@ -32,7 +34,7 @@ export function RoomRedirect() { } if (!roomId.startsWith("#") && !roomId.startsWith("!")) { - roomId = `#${roomId}:${defaultHomeserverHost}`; + roomId = `#${roomId}:${Config.defaultServerName()}`; } history.replace(`/room/${roomId.toLowerCase()}`); diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index eae23b9..5b2a976 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -61,7 +61,7 @@ export const useLoadGroupCall = ( return room; } catch (error) { if ( - isLocalRoomId(roomIdOrAlias) && + isLocalRoomId(roomIdOrAlias, client) && (error.errcode === "M_NOT_FOUND" || (error.message && error.message.indexOf("Failed to fetch alias") !== -1)) diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index b8064d8..8a77004 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -25,7 +25,6 @@ import { useClient } from "../ClientContext"; import { InspectorContext } from "../room/GroupCallInspector"; import { useModalTriggerState } from "../Modal"; import { Config } from "../config/Config"; -import { DEFAULT_CONFIG } from "../config/ConfigOptions"; interface RageShakeSubmitOptions { sendLogs: boolean; @@ -54,6 +53,10 @@ export function useSubmitRageshake(): { const submitRageshake = useCallback( async (opts) => { + if (!Config.get().rageshake?.submit_url) { + throw new Error("No rageshake URL is configured"); + } + if (sending) { return; } @@ -258,14 +261,10 @@ export function useSubmitRageshake(): { ); } - await fetch( - Config.instance.config.rageshake?.submit_url ?? - DEFAULT_CONFIG.rageshake.submit_url, - { - method: "POST", - body, - } - ); + await fetch(Config.get().rageshake?.submit_url, { + method: "POST", + body, + }); setState({ sending: false, sent: true, error: null }); } catch (error) {