From f46468296798de1582075baf0f7e72a6f2f8caae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 20 Oct 2022 20:39:24 +0200
Subject: [PATCH] Make Element Call screensharing work on desktop (#9476)

---
 src/models/Call.ts                         | 28 ++++++++--
 src/stores/widgets/ElementWidgetActions.ts |  1 +
 test/models/Call-test.ts                   | 65 ++++++++++++++++++++++
 3 files changed, 88 insertions(+), 6 deletions(-)

diff --git a/src/models/Call.ts b/src/models/Call.ts
index fd207cf1be..ed9e227d24 100644
--- a/src/models/Call.ts
+++ b/src/models/Call.ts
@@ -43,6 +43,8 @@ import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widge
 import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore";
 import PlatformPeg from "../PlatformPeg";
 import { getCurrentLanguage } from "../languageHandler";
+import DesktopCapturerSourcePicker from "../components/views/elements/DesktopCapturerSourcePicker";
+import Modal from "../Modal";
 
 const TIMEOUT_MS = 16000;
 
@@ -639,10 +641,6 @@ export class ElementCall extends Call {
             baseUrl: client.baseUrl,
             lang: getCurrentLanguage().replace("_", "-"),
         });
-        // Currently, the screen-sharing support is the same is it is for Jitsi
-        if (!PlatformPeg.get().supportsJitsiScreensharing()) {
-            params.append("hideScreensharing", "");
-        }
         url.hash = `#?${params.toString()}`;
 
         // To use Element Call without touching room state, we create a virtual
@@ -818,6 +816,7 @@ export class ElementCall extends Call {
         this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
         this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
         this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
+        this.messaging!.on(`action:${ElementWidgetActions.Screenshare}`, this.onScreenshare);
     }
 
     protected async performDisconnection(): Promise<void> {
@@ -831,8 +830,9 @@ export class ElementCall extends Call {
     public setDisconnected() {
         this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
         this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
-        this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
-        this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
+        this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
+        this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
+        this.messaging!.off(`action:${ElementWidgetActions.Screenshare}`, this.onSpotlightLayout);
         super.setDisconnected();
     }
 
@@ -951,4 +951,20 @@ export class ElementCall extends Call {
         this.layout = Layout.Spotlight;
         await this.messaging!.transport.reply(ev.detail, {}); // ack
     };
+
+    private onScreenshare = async (ev: CustomEvent<IWidgetApiRequest>) => {
+        ev.preventDefault();
+
+        if (PlatformPeg.get().supportsDesktopCapturer()) {
+            const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
+            const [source] = await finished;
+
+            await this.messaging!.transport.reply(ev.detail, {
+                failed: !source,
+                desktopCapturerSourceId: source,
+            });
+        } else {
+            await this.messaging!.transport.reply(ev.detail, {});
+        }
+    };
 }
diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts
index 5e9451efa0..fa60b9ea82 100644
--- a/src/stores/widgets/ElementWidgetActions.ts
+++ b/src/stores/widgets/ElementWidgetActions.ts
@@ -29,6 +29,7 @@ export enum ElementWidgetActions {
     // Actions for switching layouts
     TileLayout = "io.element.tile_layout",
     SpotlightLayout = "io.element.spotlight_layout",
+    Screenshare = "io.element.screenshare",
 
     OpenIntegrationManager = "integration_manager_open",
 
diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts
index df57472638..3134adf111 100644
--- a/test/models/Call-test.ts
+++ b/test/models/Call-test.ts
@@ -38,6 +38,8 @@ import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingSt
 import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore";
 import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions";
 import SettingsStore from "../../src/settings/SettingsStore";
+import Modal, { IHandle } from "../../src/Modal";
+import PlatformPeg from "../../src/PlatformPeg";
 
 jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
     [MediaDeviceKindEnum.AudioInput]: [
@@ -807,6 +809,69 @@ describe("ElementCall", () => {
             call.off(CallEvent.Layout, onLayout);
         });
 
+        describe("screensharing", () => {
+            it("passes source id if we can get it", async () => {
+                const sourceId = "source_id";
+                jest.spyOn(Modal, "createDialog").mockReturnValue(
+                    { finished: new Promise((r) => r([sourceId])) } as IHandle<any[]>,
+                );
+                jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true);
+
+                await call.connect();
+
+                messaging.emit(
+                    `action:${ElementWidgetActions.Screenshare}`,
+                    new CustomEvent("widgetapirequest", { detail: {} }),
+                );
+
+                waitFor(() => {
+                    expect(messaging!.transport.reply).toHaveBeenCalledWith(
+                        expect.objectContaining({}),
+                        expect.objectContaining({ desktopCapturerSourceId: sourceId }),
+                    );
+                });
+            });
+
+            it("passes failed if we couldn't get a source id", async () => {
+                jest.spyOn(Modal, "createDialog").mockReturnValue(
+                    { finished: new Promise((r) => r([null])) } as IHandle<any[]>,
+                );
+                jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true);
+
+                await call.connect();
+
+                messaging.emit(
+                    `action:${ElementWidgetActions.Screenshare}`,
+                    new CustomEvent("widgetapirequest", { detail: {} }),
+                );
+
+                waitFor(() => {
+                    expect(messaging!.transport.reply).toHaveBeenCalledWith(
+                        expect.objectContaining({}),
+                        expect.objectContaining({ failed: true }),
+                    );
+                });
+            });
+
+            it("passes an empty object if we don't support desktop capturer", async () => {
+                jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(false);
+
+                await call.connect();
+
+                messaging.emit(
+                    `action:${ElementWidgetActions.Screenshare}`,
+                    new CustomEvent("widgetapirequest", { detail: {} }),
+                );
+
+                waitFor(() => {
+                    expect(messaging!.transport.reply).toHaveBeenCalledWith(
+                        expect.objectContaining({}),
+                        expect.objectContaining({}),
+                    );
+                });
+            });
+        });
+
         it("ends the call immediately if we're the last participant to leave", async () => {
             await call.connect();
             const onDestroy = jest.fn();