diff --git a/.github/workflows/blocked.yaml b/.github/workflows/blocked.yaml
new file mode 100644
index 00000000..d6e592cb
--- /dev/null
+++ b/.github/workflows/blocked.yaml
@@ -0,0 +1,17 @@
+name: Prevent blocked
+on:
+ pull_request:
+ types: [opened, labeled, unlabeled]
+jobs:
+ prevent-blocked:
+ name: Prevent blocked
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ steps:
+ - name: Add notice
+ uses: actions/github-script@v7
+ if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
+ with:
+ script: |
+ core.setFailed("PR has been labeled with X-Blocked; it cannot be merged.");
diff --git a/public/index.html b/public/index.html
index bf26d8ec..579f5a00 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,26 +8,26 @@
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
-
<%- title %>
+ <%- brand %>
-
+
-
+
diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts
index 8e185abc..dce46754 100644
--- a/src/UrlParams.test.ts
+++ b/src/UrlParams.test.ts
@@ -110,8 +110,8 @@ describe("UrlParams", () => {
});
describe("returnToLobby", () => {
- it("is true in SPA mode", () => {
- expect(getUrlParams("?returnToLobby=false").returnToLobby).toBe(true);
+ it("is false in SPA mode", () => {
+ expect(getUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
});
it("defaults to false in widget mode", () => {
diff --git a/src/UrlParams.ts b/src/UrlParams.ts
index 61b777c7..fda4a95f 100644
--- a/src/UrlParams.ts
+++ b/src/UrlParams.ts
@@ -264,7 +264,9 @@ export const getUrlParams = (
"skipLobby",
isWidget && intent === UserIntent.StartNewCall,
),
- returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : true,
+ // In SPA mode the user should always exit to the home screen when hanging
+ // up, rather than being sent back to the lobby
+ returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : false,
theme: parser.getParam("theme"),
viaServers: !isWidget ? parser.getParam("viaServers") : null,
homeserver: !isWidget ? parser.getParam("homeserver") : null,
diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx
index 0f82eae9..e1456e2c 100644
--- a/src/room/GroupCallView.tsx
+++ b/src/room/GroupCallView.tsx
@@ -539,9 +539,7 @@ export const GroupCallView: FC = ({
}
} else if (left && widget !== null) {
// Left in widget mode:
- if (!returnToLobby) {
- body = null;
- }
+ body = returnToLobby ? lobbyView : null;
} else if (preload || skipLobby) {
body = null;
} else {
diff --git a/src/rtcSessionHelpers.test.ts b/src/rtcSessionHelpers.test.ts
index 62a2c187..8d0b95d3 100644
--- a/src/rtcSessionHelpers.test.ts
+++ b/src/rtcSessionHelpers.test.ts
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
-import { expect, test, vi } from "vitest";
+import { expect, onTestFinished, test, vi } from "vitest";
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
import EventEmitter from "events";
@@ -15,11 +15,17 @@ import { mockConfig } from "./utils/test";
import { ElementWidgetActions, widget } from "./widget";
import { ErrorCode } from "./utils/errors.ts";
+const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
+vi.mock("./UrlParams", () => ({ getUrlParams }));
+
const actualWidget = await vi.hoisted(async () => vi.importActual("./widget"));
vi.mock("./widget", () => ({
...actualWidget,
widget: {
- api: { transport: { send: vi.fn(), reply: vi.fn(), stop: vi.fn() } },
+ api: {
+ setAlwaysOnScreen: (): void => {},
+ transport: { send: vi.fn(), reply: vi.fn(), stop: vi.fn() },
+ },
lazyActions: new EventEmitter(),
},
}));
@@ -110,34 +116,45 @@ test("It joins the correct Session", async () => {
);
});
-test("leaveRTCSession closes the widget on a normal hangup", async () => {
+async function testLeaveRTCSession(
+ cause: "user" | "error",
+ expectClose: boolean,
+): Promise {
vi.clearAllMocks();
const session = { leaveRoomSession: vi.fn() } as unknown as MatrixRTCSession;
- await leaveRTCSession(session, "user");
+ await leaveRTCSession(session, cause);
expect(session.leaveRoomSession).toHaveBeenCalled();
expect(widget!.api.transport.send).toHaveBeenCalledWith(
ElementWidgetActions.HangupCall,
expect.anything(),
);
- expect(widget!.api.transport.send).toHaveBeenCalledWith(
- ElementWidgetActions.Close,
- expect.anything(),
- );
+ if (expectClose) {
+ expect(widget!.api.transport.send).toHaveBeenCalledWith(
+ ElementWidgetActions.Close,
+ expect.anything(),
+ );
+ expect(widget!.api.transport.stop).toHaveBeenCalled();
+ } else {
+ expect(widget!.api.transport.send).not.toHaveBeenCalledWith(
+ ElementWidgetActions.Close,
+ expect.anything(),
+ );
+ expect(widget!.api.transport.stop).not.toHaveBeenCalled();
+ }
+}
+
+test("leaveRTCSession closes the widget on a normal hangup", async () => {
+ await testLeaveRTCSession("user", true);
});
test("leaveRTCSession doesn't close the widget on a fatal error", async () => {
- vi.clearAllMocks();
- const session = { leaveRoomSession: vi.fn() } as unknown as MatrixRTCSession;
- await leaveRTCSession(session, "error");
- expect(session.leaveRoomSession).toHaveBeenCalled();
- expect(widget!.api.transport.send).toHaveBeenCalledWith(
- ElementWidgetActions.HangupCall,
- expect.anything(),
- );
- expect(widget!.api.transport.send).not.toHaveBeenCalledWith(
- ElementWidgetActions.Close,
- expect.anything(),
- );
+ await testLeaveRTCSession("error", false);
+});
+
+test("leaveRTCSession doesn't close the widget when returning to lobby", async () => {
+ getUrlParams.mockReturnValue({ returnToLobby: true });
+ onTestFinished(() => void getUrlParams.mockReset());
+ await testLeaveRTCSession("user", false);
});
test("It fails with configuration error if no live kit url config is set in fallback", async () => {
diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts
index 838dbe95..0f43fd90 100644
--- a/src/rtcSessionHelpers.ts
+++ b/src/rtcSessionHelpers.ts
@@ -19,6 +19,7 @@ import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
import { Config } from "./config/Config";
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
import { MatrixRTCFocusMissingError } from "./utils/errors.ts";
+import { getUrlParams } from "./UrlParams.ts";
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
@@ -126,6 +127,13 @@ export async function enterRTCSession(
makeKeyDelay: matrixRtcSessionConfig?.key_rotation_on_leave_delay,
},
);
+ if (widget) {
+ try {
+ await widget.api.transport.send(ElementWidgetActions.JoinCall, {});
+ } catch (e) {
+ logger.error("Failed to send join action", e);
+ }
+ }
}
const widgetPostHangupProcedure = async (
@@ -151,7 +159,7 @@ const widgetPostHangupProcedure = async (
}
// On a normal user hangup we can shut down and close the widget. But if an
// error occurs we should keep the widget open until the user reads it.
- if (cause === "user") {
+ if (cause === "user" && !getUrlParams().returnToLobby) {
try {
await widget.api.transport.send(ElementWidgetActions.Close, {});
} catch (e) {
diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts
index f634233e..ce104396 100644
--- a/src/state/CallViewModel.ts
+++ b/src/state/CallViewModel.ts
@@ -496,6 +496,10 @@ export class CallViewModel extends ViewModel {
}
return displaynameMap;
}),
+ // It turns out that doing the disambiguation above is rather expensive on Safari (10x slower
+ // than on Chrome/Firefox). This means it is important that we share() the result so that we
+ // don't do this work more times than we need to. This is achieve through the state() operator:
+ this.scope.state(),
);
/**
diff --git a/vite.config.js b/vite.config.js
index 4c9871a2..8f067357 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -29,7 +29,7 @@ export default defineConfig(({ mode }) => {
}),
htmlTemplate.default({
data: {
- title: env.VITE_PRODUCT_NAME || "Element Call",
+ brand: env.VITE_PRODUCT_NAME || "Element Call",
},
}),