Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b929cbf74b | ||
|
|
8520de232d | ||
|
|
773e41311e |
Generated
+4
-4
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.13.0",
|
"@axe-core/playwright": "4.13.0",
|
||||||
"@lotusguild/element-call-embedded": "0.25.0-lotus.21",
|
"@lotusguild/element-call-embedded": "0.25.0-lotus.22",
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
@@ -2695,9 +2695,9 @@
|
|||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
},
|
},
|
||||||
"node_modules/@lotusguild/element-call-embedded": {
|
"node_modules/@lotusguild/element-call-embedded": {
|
||||||
"version": "0.25.0-lotus.21",
|
"version": "0.25.0-lotus.22",
|
||||||
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.21/element-call-embedded-0.25.0-lotus.21.tgz",
|
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.22/element-call-embedded-0.25.0-lotus.22.tgz",
|
||||||
"integrity": "sha512-Phtjop2qdKRrCLqAhzipRaJaVnU3MjK9CV4QwHkwU/Xlj8oguZBfjOlV7zTqOTzAr57OteOfLcyxLsmv5+u9tg==",
|
"integrity": "sha512-AimDKZA4xUKEEyl2XUFUSUDl03tBKFv88T/3igKyH2VqNSPMGnqcQJC5HXIkHLTWrOmg8kLcJ//FFbqlUZiLcA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||||
|
|||||||
+1
-1
@@ -108,7 +108,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.13.0",
|
"@axe-core/playwright": "4.13.0",
|
||||||
"@lotusguild/element-call-embedded": "0.25.0-lotus.21",
|
"@lotusguild/element-call-embedded": "0.25.0-lotus.22",
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
} from '../hooks/useCallEmbed';
|
} from '../hooks/useCallEmbed';
|
||||||
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
||||||
import { toastQueueAtom } from '../state/toast';
|
import { toastQueueAtom } from '../state/toast';
|
||||||
import { CallEmbed, useCallControlState } from '../plugins/call';
|
import { CallEmbed, useCallControlState, useScreensharePromptOpen } from '../plugins/call';
|
||||||
import { hangupCallAndWait } from '../plugins/call/hangup';
|
import { hangupCallAndWait } from '../plugins/call/hangup';
|
||||||
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||||
@@ -882,6 +882,7 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
const callActive = callEmbed && joined;
|
const callActive = callEmbed && joined;
|
||||||
const callVisible = inCallRoom && callActive && !chatOnlyView;
|
const callVisible = inCallRoom && callActive && !chatOnlyView;
|
||||||
const pipMode = callActive && !inCallRoom;
|
const pipMode = callActive && !inCallRoom;
|
||||||
|
const pipPromptOpen = useScreensharePromptOpen(callEmbed?.control);
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
const { screenshare: pipScreenshare } = useCallControlState(callEmbed?.control);
|
const { screenshare: pipScreenshare } = useCallControlState(callEmbed?.control);
|
||||||
@@ -1363,6 +1364,9 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
|
// [Gitea #43] The call's "Share your screen?" prompt is drawn
|
||||||
|
// inside the frame: let the click reach its Share button.
|
||||||
|
pointerEvents: pipPromptOpen ? 'none' : undefined,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CallEmbed,
|
CallEmbed,
|
||||||
useCallControlState,
|
useCallControlState,
|
||||||
useCallMicLevel,
|
useCallMicLevel,
|
||||||
useFrameScreenshare,
|
useScreenshareMode,
|
||||||
} from '../../plugins/call';
|
} from '../../plugins/call';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { callEmbedAtom } from '../../state/callEmbed';
|
import { callEmbedAtom } from '../../state/callEmbed';
|
||||||
@@ -197,10 +197,16 @@ export function CallControl({
|
|||||||
// Keep a forbidden control visible while its track is still live (so the user
|
// Keep a forbidden control visible while its track is still live (so the user
|
||||||
// can stop it); otherwise hide it entirely.
|
// can stop it); otherwise hide it entirely.
|
||||||
const showCamera = allowCamera || video;
|
const showCamera = allowCamera || video;
|
||||||
// [Gitea #43] Hidden where EC shows its own screenshare button in the frame.
|
// [Gitea #43] Hidden where EC shows its own screenshare button in the frame;
|
||||||
const frameScreenshare = useFrameScreenshare(callEmbed.control);
|
// where the click can't be delegated, starting asks for the in-frame prompt.
|
||||||
const showScreenshare = !frameScreenshare && (allowScreenshare || screenshare);
|
const screenshareMode = useScreenshareMode(callEmbed.control);
|
||||||
|
const showScreenshare = screenshareMode !== 'hidden' && (allowScreenshare || screenshare);
|
||||||
const [shareConfirm, setShareConfirm] = useState(false);
|
const [shareConfirm, setShareConfirm] = useState(false);
|
||||||
|
const handleScreenshareToggle = () => {
|
||||||
|
if (screenshare) callEmbed.control.toggleScreenshare();
|
||||||
|
else if (screenshareMode === 'prompt') callEmbed.control.promptScreenshare();
|
||||||
|
else setShareConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMicrophoneToggle = useCallback(
|
const handleMicrophoneToggle = useCallback(
|
||||||
() => callEmbed.control.toggleMicrophone(),
|
() => callEmbed.control.toggleMicrophone(),
|
||||||
@@ -255,9 +261,7 @@ export function CallControl({
|
|||||||
{!compact && showScreenshare && (
|
{!compact && showScreenshare && (
|
||||||
<ScreenShareButton
|
<ScreenShareButton
|
||||||
enabled={screenshare}
|
enabled={screenshare}
|
||||||
onToggle={() =>
|
onToggle={() => handleScreenshareToggle()}
|
||||||
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
|
|
||||||
}
|
|
||||||
disabled={!callJoined}
|
disabled={!callJoined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
CallEmbed,
|
CallEmbed,
|
||||||
useCallControlState,
|
useCallControlState,
|
||||||
useCallMicLevel,
|
useCallMicLevel,
|
||||||
useFrameScreenshare,
|
useScreenshareMode,
|
||||||
} from '../../plugins/call';
|
} from '../../plugins/call';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
@@ -112,9 +112,14 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
// can stop it); otherwise hide it entirely.
|
// can stop it); otherwise hide it entirely.
|
||||||
const showCamera = allowCamera || video;
|
const showCamera = allowCamera || video;
|
||||||
// [Gitea #43] Where EC shows its own screenshare button in the frame, ours
|
// [Gitea #43] Where EC shows its own screenshare button in the frame, ours
|
||||||
// is hidden (this engine can't start a share from the host); the
|
// is hidden (the screenshare-audio mute stays); where the click can't be
|
||||||
// screenshare-audio mute stays.
|
// delegated, starting asks for the fork's in-frame "Share your screen?".
|
||||||
const frameScreenshare = useFrameScreenshare(callEmbed.control);
|
const screenshareMode = useScreenshareMode(callEmbed.control);
|
||||||
|
const handleScreenshareToggle = () => {
|
||||||
|
if (screenshare) callEmbed.control.toggleScreenshare();
|
||||||
|
else if (screenshareMode === 'prompt') callEmbed.control.promptScreenshare();
|
||||||
|
else setShareConfirm(true);
|
||||||
|
};
|
||||||
const showScreenshare = allowScreenshare || screenshare;
|
const showScreenshare = allowScreenshare || screenshare;
|
||||||
const showVideoGroup = showCamera || showScreenshare || !!document.fullscreenEnabled;
|
const showVideoGroup = showCamera || showScreenshare || !!document.fullscreenEnabled;
|
||||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
@@ -225,12 +230,10 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
{showCamera && <VideoButton enabled={video} onToggle={handleVideoToggle} />}
|
{showCamera && <VideoButton enabled={video} onToggle={handleVideoToggle} />}
|
||||||
{showScreenshare && (
|
{showScreenshare && (
|
||||||
<>
|
<>
|
||||||
{!frameScreenshare && (
|
{screenshareMode !== 'hidden' && (
|
||||||
<ScreenShareButton
|
<ScreenShareButton
|
||||||
enabled={screenshare}
|
enabled={screenshare}
|
||||||
onToggle={() =>
|
onToggle={() => handleScreenshareToggle()}
|
||||||
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Mute-screenshare-audio sits directly next to the screenshare
|
{/* Mute-screenshare-audio sits directly next to the screenshare
|
||||||
|
|||||||
@@ -261,6 +261,15 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
|
|
||||||
private frameScreenshareListeners = new Set<() => void>();
|
private frameScreenshareListeners = new Set<() => void>();
|
||||||
|
|
||||||
|
// [Gitea #43] The fork draws the "Share your screen?" prompt inside the frame
|
||||||
|
// on request (io.lotus.prompt_screenshare), so where the click can't be
|
||||||
|
// delegated our call-bar button asks for that instead of starting the share.
|
||||||
|
private _screensharePrompt = false;
|
||||||
|
|
||||||
|
// [Gitea #43] The fork's in-frame prompt is showing: anything the host lays
|
||||||
|
// over the frame (the picture-in-picture overlay) must let clicks through.
|
||||||
|
private _screensharePromptOpen = false;
|
||||||
|
|
||||||
// [Gitea #43] Whether the room's call policy allows screensharing; the fork
|
// [Gitea #43] Whether the room's call policy allows screensharing; the fork
|
||||||
// hides its in-frame button when it doesn't.
|
// hides its in-frame button when it doesn't.
|
||||||
private frameScreenshareAllowed = true;
|
private frameScreenshareAllowed = true;
|
||||||
@@ -274,7 +283,32 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
return this._frameScreenshare;
|
return this._frameScreenshare;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Subscribe to `frameScreenshare` changes. Returns an unsubscribe. */
|
/**
|
||||||
|
* Starting a share must go through the fork's in-frame prompt: this engine
|
||||||
|
* can't delegate the click (Firefox, Safari, WebKitGTK) and the fork has
|
||||||
|
* the prompt. Stopping never needs it.
|
||||||
|
*/
|
||||||
|
public get screenshareNeedsPrompt(): boolean {
|
||||||
|
return this._screensharePrompt && !canDelegateCapability();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The fork's in-frame "Share your screen?" prompt is showing. */
|
||||||
|
public get screensharePromptOpen(): boolean {
|
||||||
|
return this._screensharePromptOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the fork to show "Share your screen?" inside the call frame; its Share
|
||||||
|
* button starts the share with the frame's own click.
|
||||||
|
*/
|
||||||
|
public promptScreenshare(): void {
|
||||||
|
this.sendForkAction('io.lotus.prompt_screenshare', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to `frameScreenshare` / `screenshareNeedsPrompt` /
|
||||||
|
* `screensharePromptOpen` changes. Returns an unsubscribe.
|
||||||
|
*/
|
||||||
public onFrameScreenshareChange(cb: () => void): () => void {
|
public onFrameScreenshareChange(cb: () => void): () => void {
|
||||||
this.frameScreenshareListeners.add(cb);
|
this.frameScreenshareListeners.add(cb);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -289,7 +323,8 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sendFrameScreenshareAllowed(): void {
|
private sendFrameScreenshareAllowed(): void {
|
||||||
if (!this.joined || !this.forkReady || !this._frameScreenshare) return;
|
if (!this.joined || !this.forkReady) return;
|
||||||
|
if (!this._frameScreenshare && !this._screensharePrompt) return;
|
||||||
this.sendForkAction('io.lotus.set_frame_screenshare', {
|
this.sendForkAction('io.lotus.set_frame_screenshare', {
|
||||||
visible: this.frameScreenshareAllowed,
|
visible: this.frameScreenshareAllowed,
|
||||||
});
|
});
|
||||||
@@ -324,18 +359,32 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
/** [Gitea #43] The fork's `io.lotus.controls_state` report. */
|
/** [Gitea #43] The fork's `io.lotus.controls_state` report. */
|
||||||
public onControlsState(data: unknown) {
|
public onControlsState(data: unknown) {
|
||||||
if (typeof data !== 'object' || data === null) return;
|
if (typeof data !== 'object' || data === null) return;
|
||||||
const { screensharing, layout, frameScreenshare } = data as {
|
const { screensharing, layout, frameScreenshare, screensharePrompt, screensharePromptOpen } =
|
||||||
|
data as {
|
||||||
screensharing?: unknown;
|
screensharing?: unknown;
|
||||||
layout?: unknown;
|
layout?: unknown;
|
||||||
frameScreenshare?: unknown;
|
frameScreenshare?: unknown;
|
||||||
|
screensharePrompt?: unknown;
|
||||||
|
screensharePromptOpen?: unknown;
|
||||||
};
|
};
|
||||||
const firstReport = !this.forkReady;
|
const firstReport = !this.forkReady;
|
||||||
this.forkReady = true;
|
this.forkReady = true;
|
||||||
const frame = frameScreenshare === true;
|
const frame = frameScreenshare === true;
|
||||||
if (frame !== this._frameScreenshare) {
|
const prompt = screensharePrompt === true;
|
||||||
this._frameScreenshare = frame;
|
const promptOpen = screensharePromptOpen === true;
|
||||||
|
if (promptOpen !== this._screensharePromptOpen) {
|
||||||
|
this._screensharePromptOpen = promptOpen;
|
||||||
|
if (frame === this._frameScreenshare && prompt === this._screensharePrompt) {
|
||||||
this.frameScreenshareListeners.forEach((l) => l());
|
this.frameScreenshareListeners.forEach((l) => l());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (frame !== this._frameScreenshare || prompt !== this._screensharePrompt) {
|
||||||
|
const promptTurnedOn = prompt && !this._screensharePrompt;
|
||||||
|
this._frameScreenshare = frame;
|
||||||
|
this._screensharePrompt = prompt;
|
||||||
|
this.frameScreenshareListeners.forEach((l) => l());
|
||||||
|
if (promptTurnedOn && !firstReport) this.sendFrameScreenshareAllowed();
|
||||||
|
}
|
||||||
if (firstReport) {
|
if (firstReport) {
|
||||||
this.sendHotkeyCodes();
|
this.sendHotkeyCodes();
|
||||||
this.sendFrameScreenshareAllowed();
|
this.sendFrameScreenshareAllowed();
|
||||||
|
|||||||
@@ -75,19 +75,46 @@ export const useCallMicLevel = (callEmbed: CallEmbed | undefined): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Gitea #43] True when EC's own screenshare button is shown inside the call
|
* [Gitea #43] How the call bar's screenshare button behaves:
|
||||||
* frame (no Capability Delegation in this engine), so the host bar hides its.
|
* - `hidden`: EC's own button is inside the frame (fork lotus.20/21), so ours hides;
|
||||||
|
* - `prompt`: starting asks the fork for its in-frame "Share your screen?"
|
||||||
|
* (no Capability Delegation in this engine);
|
||||||
|
* - `direct`: our own confirm, then a (delegated) set_screenshare.
|
||||||
*/
|
*/
|
||||||
export const useFrameScreenshare = (control: CallControl | undefined): boolean => {
|
export type ScreenshareMode = 'hidden' | 'prompt' | 'direct';
|
||||||
const [frame, setFrame] = useState(() => control?.frameScreenshare ?? false);
|
|
||||||
|
const screenshareMode = (control: CallControl): ScreenshareMode => {
|
||||||
|
if (control.frameScreenshare) return 'hidden';
|
||||||
|
return control.screenshareNeedsPrompt ? 'prompt' : 'direct';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScreenshareMode = (control: CallControl | undefined): ScreenshareMode => {
|
||||||
|
const [mode, setMode] = useState<ScreenshareMode>(() =>
|
||||||
|
control ? screenshareMode(control) : 'direct',
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!control) {
|
if (!control) {
|
||||||
setFrame(false);
|
setMode('direct');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const sync = () => setFrame(control.frameScreenshare);
|
const sync = () => setMode(screenshareMode(control));
|
||||||
sync();
|
sync();
|
||||||
return control.onFrameScreenshareChange(sync);
|
return control.onFrameScreenshareChange(sync);
|
||||||
}, [control]);
|
}, [control]);
|
||||||
return frame;
|
return mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** [Gitea #43] The fork's in-frame "Share your screen?" prompt is showing. */
|
||||||
|
export const useScreensharePromptOpen = (control: CallControl | undefined): boolean => {
|
||||||
|
const [open, setOpen] = useState(() => control?.screensharePromptOpen ?? false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!control) {
|
||||||
|
setOpen(false);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const sync = () => setOpen(control.screensharePromptOpen);
|
||||||
|
sync();
|
||||||
|
return control.onFrameScreenshareChange(sync);
|
||||||
|
}, [control]);
|
||||||
|
return open;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user