diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index c7b7825fe6..87635a421c 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -72,7 +72,7 @@ export default abstract class BasePlatform {
protected _favicon?: Favicon;
protected constructor() {
- dis.register(this.onAction);
+ dis.register(this.onAction.bind(this));
this.startUpdateCheck = this.startUpdateCheck.bind(this);
}
@@ -85,14 +85,14 @@ export default abstract class BasePlatform {
*/
public abstract getDefaultDeviceDisplayName(): string;
- protected onAction = (payload: ActionPayload): void => {
+ protected onAction(payload: ActionPayload): void {
switch (payload.action) {
case "on_client_not_viable":
case Action.OnLoggedOut:
this.setNotificationCount(0);
break;
}
- };
+ }
// Used primarily for Analytics
public abstract getHumanReadableName(): string;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e65d4809df..65370997f4 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2457,6 +2457,10 @@
"recent_changes_heading": "Recent changes that have not yet been received",
"title": "Server isn't responding"
},
+ "service_worker_error": {
+ "description": "%(brand)s requires a service worker for loading authenticated media from Matrix content repositories. This is not supported by your browser so you may experience media failing to load.",
+ "title": "Failed to load service worker"
+ },
"seshat": {
"error_initialising": "Message search initialisation failed, check your settings for more information",
"reset_button": "Reset event store",
diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts
index 316f479e73..46b6a61f83 100644
--- a/src/vector/platform/WebPlatform.ts
+++ b/src/vector/platform/WebPlatform.ts
@@ -18,6 +18,10 @@ import { Action } from "../../dispatcher/actions";
import { type CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload";
import { parseQs } from "../url_utils";
import { _t } from "../../languageHandler";
+import ToastStore from "../../stores/ToastStore.ts";
+import GenericToast from "../../components/views/toasts/GenericToast.tsx";
+import SdkConfig from "../../SdkConfig.ts";
+import type { ActionPayload } from "../../dispatcher/payloads.ts";
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
@@ -32,32 +36,59 @@ function getNormalizedAppVersion(version: string): string {
export default class WebPlatform extends BasePlatform {
private static readonly VERSION = process.env.VERSION!; // baked in by Webpack
+ private readonly registerServiceWorkerPromise: Promise;
public constructor() {
super();
// Register the service worker in the background
- this.tryRegisterServiceWorker().catch((e) => console.error("Error registering/updating service worker:", e));
+ this.registerServiceWorkerPromise = this.registerServiceWorker();
+ this.registerServiceWorkerPromise.catch((e) => {
+ console.error("Error registering/updating service worker:", e);
+ });
}
- private async tryRegisterServiceWorker(): Promise {
- if (!("serviceWorker" in navigator)) {
- return; // not available on this platform - don't try to register the service worker
- }
+ protected onAction(payload: ActionPayload): void {
+ super.onAction(payload);
+ switch (payload.action) {
+ case "client_started":
+ // Defer drawing the toast until the client is started as the lifecycle methods reset the ToastStore right before
+ this.registerServiceWorkerPromise.catch(this.handleServiceWorkerRegistrationError);
+ break;
+ }
+ }
+
+ private async registerServiceWorker(): Promise {
// sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts`
const registration = await navigator.serviceWorker.register("sw.js");
if (!registration) {
- // Registration didn't work for some reason - assume failed and ignore.
- // This typically happens in Jest.
- return;
+ throw new Error("Service worker registration failed");
}
- navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage.bind(this));
+ navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage);
await registration.update();
}
- private onServiceWorkerPostMessage(event: MessageEvent): void {
+ private handleServiceWorkerRegistrationError = (): void => {
+ const key = "service_worker_error";
+ const brand = SdkConfig.get().brand;
+ ToastStore.sharedInstance().addOrReplaceToast({
+ key,
+ title: _t("service_worker_error|title"),
+ props: {
+ description: _t("service_worker_error|description", { brand }),
+ primaryLabel: _t("action|ok"),
+ onPrimaryClick: () => {
+ ToastStore.sharedInstance().dismissToast(key);
+ },
+ },
+ component: GenericToast,
+ priority: 95,
+ });
+ };
+
+ private onServiceWorkerPostMessage = (event: MessageEvent): void => {
try {
if (event.data?.["type"] === "userinfo" && event.data?.["responseKey"]) {
const userId = localStorage.getItem("mx_user_id");
@@ -73,7 +104,7 @@ export default class WebPlatform extends BasePlatform {
} catch (e) {
console.error("Error responding to service worker: ", e);
}
- }
+ };
public getHumanReadableName(): string {
return "Web Platform"; // no translation required: only used for analytics
diff --git a/test/unit-tests/vector/platform/WebPlatform-test.ts b/test/unit-tests/vector/platform/WebPlatform-test.ts
index b47f2d5537..7c1048fe05 100644
--- a/test/unit-tests/vector/platform/WebPlatform-test.ts
+++ b/test/unit-tests/vector/platform/WebPlatform-test.ts
@@ -12,6 +12,9 @@ import { UpdateCheckStatus } from "../../../../src/BasePlatform";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import WebPlatform from "../../../../src/vector/platform/WebPlatform";
import { setupLanguageMock } from "../../../setup/setupLanguage";
+import ToastStore from "../../../../src/stores/ToastStore.ts";
+import defaultDispatcher from "../../../../src/dispatcher/dispatcher.ts";
+import { emitPromise } from "../../../test-utils";
fetchMock.config.overwriteRoutes = true;
@@ -26,11 +29,32 @@ describe("WebPlatform", () => {
expect(platform.getHumanReadableName()).toEqual("Web Platform");
});
- it("registers service worker", () => {
- // @ts-ignore - mocking readonly object
- navigator.serviceWorker = { register: jest.fn() };
- new WebPlatform();
- expect(navigator.serviceWorker.register).toHaveBeenCalled();
+ describe("service worker", () => {
+ it("registers successfully", () => {
+ // @ts-expect-error - mocking readonly object
+ navigator.serviceWorker = {
+ register: jest.fn().mockResolvedValue({
+ update: jest.fn(),
+ }),
+ addEventListener: jest.fn(),
+ };
+ new WebPlatform();
+ expect(navigator.serviceWorker.register).toHaveBeenCalled();
+ });
+
+ it("handles errors", async () => {
+ // @ts-expect-error - mocking readonly object
+ navigator.serviceWorker = {
+ register: undefined,
+ };
+ new WebPlatform();
+
+ defaultDispatcher.dispatch({ action: "client_started" });
+ await emitPromise(ToastStore.sharedInstance(), "update");
+ const toasts = ToastStore.sharedInstance().getToasts();
+ expect(toasts).toHaveLength(1);
+ expect(toasts[0].title).toEqual("Failed to load service worker");
+ });
});
it("should call reload on window location object", () => {
@@ -146,24 +170,20 @@ describe("WebPlatform", () => {
});
describe("pollForUpdate()", () => {
- it(
- "should return not available and call showNoUpdate when current version " +
- "matches most recent version",
- async () => {
- // @ts-ignore
- WebPlatform.VERSION = prodVersion;
- fetchMock.getOnce("/version", prodVersion);
- const platform = new WebPlatform();
+ it("should return not available and call showNoUpdate when current version matches most recent version", async () => {
+ // @ts-ignore
+ WebPlatform.VERSION = prodVersion;
+ fetchMock.getOnce("/version", prodVersion);
+ const platform = new WebPlatform();
- const showUpdate = jest.fn();
- const showNoUpdate = jest.fn();
- const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
+ const showUpdate = jest.fn();
+ const showNoUpdate = jest.fn();
+ const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
- expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
- expect(showUpdate).not.toHaveBeenCalled();
- expect(showNoUpdate).toHaveBeenCalled();
- },
- );
+ expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
+ expect(showUpdate).not.toHaveBeenCalled();
+ expect(showNoUpdate).toHaveBeenCalled();
+ });
it("should strip v prefix from versions before comparing", async () => {
// @ts-ignore