diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 7bf9faa..a3fdc01 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -4,9 +4,6 @@ on:
branches: [main]
env:
VITE_DEFAULT_HOMESERVER: "https://call.ems.host"
- VITE_SENTRY_DSN: https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
- VITE_SENTRY_ENVIRONMENT: main-branch-cd
- VITE_RAGESHAKE_SUBMIT_URL: https://element.io/bugreports/submit
jobs:
build:
name: Build
@@ -17,7 +14,7 @@ jobs:
- name: Yarn cache
uses: actions/setup-node@v3
with:
- cache: 'yarn'
+ cache: "yarn"
- name: Install dependencies
run: "yarn install"
- name: Build
diff --git a/.gitignore b/.gitignore
index 9bafa52..f46dc2b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ dist
dist-ssr
*.local
.idea/
+config.json
diff --git a/sample.config.json b/sample.config.json
new file mode 100644
index 0000000..c00e539
--- /dev/null
+++ b/sample.config.json
@@ -0,0 +1,13 @@
+{
+ "posthog": {
+ "api_key": "examplePosthogKey",
+ "api_host": "https://posthog.com"
+ },
+ "sentry": {
+ "environment": "main-branch",
+ "DSN": "https://examplePublicKey@o0.ingest.sentry.io/0"
+ },
+ "rageshake": {
+ "submit_url": "http://localhost:9110/api/submit"
+ }
+}
diff --git a/scripts/dockerbuild.sh b/scripts/dockerbuild.sh
index 491fc02..7dce8f6 100755
--- a/scripts/dockerbuild.sh
+++ b/scripts/dockerbuild.sh
@@ -3,8 +3,6 @@
set -ex
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
-export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
-export VITE_RAGESHAKE_SUBMIT_URL=https://element.io/bugreports/submit
export VITE_PRODUCT_NAME="Element Call"
git clone https://github.com/matrix-org/matrix-js-sdk.git
diff --git a/src/App.tsx b/src/App.tsx
index 0299734..5fc0414 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { Suspense } from "react";
+import React, { Suspense, useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import * as Sentry from "@sentry/react";
import { OverlayProvider } from "@react-aria/overlays";
@@ -28,7 +28,8 @@ import { ClientProvider } from "./ClientContext";
import { usePageFocusStyle } from "./usePageFocusStyle";
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
import { InspectorContextProvider } from "./room/GroupCallInspector";
-import { CrashView } from "./FullScreenView";
+import { CrashView, LoadingView } from "./FullScreenView";
+import { Initializer } from "./initializer";
const SentryRoute = Sentry.withSentryRouting(Route);
@@ -37,42 +38,54 @@ interface AppProps {
}
export default function App({ history }: AppProps) {
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ Initializer.init()?.then(() => {
+ setLoaded(true);
+ });
+ });
+
usePageFocusStyle();
const errorPage = ;
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {loaded ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
);
}
diff --git a/src/config/Config.ts b/src/config/Config.ts
new file mode 100644
index 0000000..9b3e718
--- /dev/null
+++ b/src/config/Config.ts
@@ -0,0 +1,64 @@
+/*
+Copyright 2021-2022 New Vector Ltd
+
+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 { DEFAULT_CONFIG, IConfigOptions } from "./ConfigOptions";
+
+export class Config {
+ private static internalInstance: Config;
+ public static get instance(): Config {
+ if (!this.internalInstance)
+ throw new Error("Config instance read before config got initialized");
+ return this.internalInstance;
+ }
+ public static init(): Promise {
+ if (Config?.internalInstance?.initPromise) {
+ return Config.internalInstance.initPromise;
+ }
+ Config.internalInstance = new Config();
+ Config.internalInstance.initPromise = new Promise((resolve) => {
+ downloadConfig("../config.json").then((config) => {
+ Config.internalInstance.config = { ...DEFAULT_CONFIG, ...config };
+ resolve();
+ });
+ });
+ return Config.internalInstance.initPromise;
+ }
+
+ public config: IConfigOptions;
+ private initPromise: Promise;
+}
+
+async function downloadConfig(
+ configJsonFilename: string
+): Promise {
+ const url = new URL(configJsonFilename, window.location.href);
+ url.searchParams.set("cachebuster", Date.now().toString());
+ const res = await fetch(url, {
+ cache: "no-cache",
+ method: "GET",
+ });
+
+ if (res.status === 404 || res.status === 0) {
+ // 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 {} as IConfigOptions;
+ }
+
+ if (res.ok) {
+ return res.json();
+ }
+}
diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts
new file mode 100644
index 0000000..6e29a94
--- /dev/null
+++ b/src/config/ConfigOptions.ts
@@ -0,0 +1,19 @@
+export interface IConfigOptions {
+ posthog?: {
+ api_key: string;
+ };
+ sentry?: {
+ DSN: string;
+ environment: string;
+ };
+ rageshake?: {
+ submit_url: string;
+ };
+}
+
+export const DEFAULT_CONFIG: IConfigOptions = {
+ sentry: { DSN: "", environment: "production" },
+ rageshake: {
+ submit_url: "https://element.io/bugreports/submit",
+ },
+};
diff --git a/src/initializer.tsx b/src/initializer.tsx
new file mode 100644
index 0000000..1af0574
--- /dev/null
+++ b/src/initializer.tsx
@@ -0,0 +1,146 @@
+/*
+Copyright 2022 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 { Integrations } from "@sentry/tracing";
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import LanguageDetector from "i18next-browser-languagedetector";
+import Backend from "i18next-http-backend";
+import * as Sentry from "@sentry/react";
+
+import { getUrlParams } from "./UrlParams";
+import { Config } from "./config/Config";
+import { DEFAULT_CONFIG } from "./config/ConfigOptions";
+
+enum LoadState {
+ None,
+ Loading,
+ Loaded,
+}
+
+class DependencyLoadStates {
+ // TODO: decide where olm should be initialized (see TODO comment below)
+ // olm: LoadState = LoadState.None;
+ config: LoadState = LoadState.None;
+ sentry: LoadState = LoadState.None;
+ i18n: LoadState = LoadState.None;
+
+ allDepsAreLoaded() {
+ return !Object.values(this).some((s) => s !== LoadState.Loaded);
+ }
+}
+
+export class Initializer {
+ private static internalInstance: Initializer;
+
+ public static init(): Promise | null {
+ if (Initializer?.internalInstance?.initPromise) {
+ return null;
+ }
+ Initializer.internalInstance = new Initializer();
+ Initializer.internalInstance.initPromise = new Promise((resolve) => {
+ // initStep calls itself recursivly until everything is initialized in the correct order.
+ // Then the promise gets resolved.
+ Initializer.internalInstance.initStep(resolve);
+ });
+ return Initializer.internalInstance.initPromise;
+ }
+
+ loadStates = new DependencyLoadStates();
+
+ initStep(resolve: (value: void | PromiseLike) => void) {
+ // TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`)
+ // we need to decide if we want to init it here or keep it in initClient
+ // if (this.loadStates.olm === LoadState.None) {
+ // this.loadStates.olm = LoadState.Loading;
+ // // TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
+ // window.OLM_OPTIONS = {};
+ // Olm.init({ locateFile: () => olmWasmPath }).then(() => {
+ // this.loadStates.olm = LoadState.Loaded;
+ // this.initStep(resolve);
+ // });
+ // }
+
+ // config
+ if (this.loadStates.config === LoadState.None) {
+ this.loadStates.config = LoadState.Loading;
+ Config.init().then(() => {
+ this.loadStates.config = LoadState.Loaded;
+ this.initStep(resolve);
+ });
+ }
+
+ //sentry (only initialize after the config is ready)
+ if (
+ 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,
+ });
+ this.loadStates.sentry = LoadState.Loaded;
+ }
+
+ //i18n
+ if (this.loadStates.i18n === LoadState.None) {
+ const languageDetector = new LanguageDetector();
+ languageDetector.addDetector({
+ name: "urlFragment",
+ // Look for a language code in the URL's fragment
+ lookup: () => getUrlParams().lang ?? undefined,
+ });
+
+ i18n
+ .use(Backend)
+ .use(languageDetector)
+ .use(initReactI18next)
+ .init({
+ fallbackLng: "en-GB",
+ defaultNS: "app",
+ keySeparator: false,
+ nsSeparator: false,
+ pluralSeparator: "|",
+ contextSeparator: "|",
+ interpolation: {
+ escapeValue: false, // React has built-in XSS protections
+ },
+ detection: {
+ // No localStorage detectors or caching here, since we don't have any way
+ // of letting the user manually select a language
+ order: ["urlFragment", "navigator"],
+ caches: [],
+ },
+ });
+ this.loadStates.i18n = LoadState.Loaded;
+ }
+
+ if (this.loadStates.allDepsAreLoaded()) {
+ // resolve if there is no dependency that is not loaded
+ resolve();
+ }
+ }
+ private initPromise: Promise;
+}
diff --git a/src/main.tsx b/src/main.tsx
index f7aa6ca..bf4ab5f 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -23,17 +23,10 @@ import "matrix-js-sdk/src/browser-index";
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserHistory } from "history";
-import * as Sentry from "@sentry/react";
-import { Integrations } from "@sentry/tracing";
-import i18n from "i18next";
-import { initReactI18next } from "react-i18next";
-import Backend from "i18next-http-backend";
-import LanguageDetector from "i18next-browser-languagedetector";
import "./index.css";
import App from "./App";
import { init as initRageshake } from "./settings/rageshake";
-import { getUrlParams } from "./UrlParams";
initRageshake();
@@ -108,47 +101,6 @@ if (import.meta.env.VITE_CUSTOM_THEME) {
const history = createBrowserHistory();
-Sentry.init({
- dsn: import.meta.env.VITE_SENTRY_DSN as string,
- environment:
- (import.meta.env.VITE_SENTRY_ENVIRONMENT as string) ?? "production",
- integrations: [
- new Integrations.BrowserTracing({
- routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
- }),
- ],
- tracesSampleRate: 1.0,
-});
-
-const languageDetector = new LanguageDetector();
-languageDetector.addDetector({
- name: "urlFragment",
- // Look for a language code in the URL's fragment
- lookup: () => getUrlParams().lang ?? undefined,
-});
-
-i18n
- .use(Backend)
- .use(languageDetector)
- .use(initReactI18next)
- .init({
- fallbackLng: "en-GB",
- defaultNS: "app",
- keySeparator: false,
- nsSeparator: false,
- pluralSeparator: "|",
- contextSeparator: "|",
- interpolation: {
- escapeValue: false, // React has built-in XSS protections
- },
- detection: {
- // No localStorage detectors or caching here, since we don't have any way
- // of letting the user manually select a language
- order: ["urlFragment", "navigator"],
- caches: [],
- },
- });
-
root.render(
diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts
index 5128030..dfff6f0 100644
--- a/src/settings/submit-rageshake.ts
+++ b/src/settings/submit-rageshake.ts
@@ -24,6 +24,8 @@ import { getLogsForReport } from "./rageshake";
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;
@@ -252,8 +254,8 @@ export function useSubmitRageshake(): {
}
await fetch(
- (import.meta.env.VITE_RAGESHAKE_SUBMIT_URL as string) ||
- "https://element.io/bugreports/submit",
+ Config.instance.config.rageshake?.submit_url ??
+ DEFAULT_CONFIG.rageshake.submit_url,
{
method: "POST",
body,