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", }, }),