feat: screenshare fullscreen button + pip spotlight, fix screenshare view
- Remove revert-to-grid logic that was overriding EC's natural screenshare spotlight, causing fullscreen to show user avatars instead of the screen - Add fullscreen button to call controls (visible when screensharing) that requests fullscreen on the call embed container - Add FullscreenButton component with enter/exit SVG icons to Controls.tsx - PIP mode: sync setPipMode to CallControl; auto-enable spotlight when screenshare is active in pip so the screenshare fills the window - Make useCallControlState accept undefined control for safe use in CallEmbedProvider - Add package-lock.json to .gitignore (generated by local npm install) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -4,4 +4,4 @@ node_modules
|
|||||||
devAssets
|
devAssets
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.ideapackage-lock.json
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
useCallStart,
|
useCallStart,
|
||||||
} from '../hooks/useCallEmbed';
|
} from '../hooks/useCallEmbed';
|
||||||
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
||||||
import { CallEmbed } from '../plugins/call';
|
import { CallEmbed, useCallControlState } from '../plugins/call';
|
||||||
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
@@ -421,6 +421,23 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
const pipMode = callActive && !inCallRoom;
|
const pipMode = callActive && !inCallRoom;
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
|
const { screenshare: pipScreenshare } = useCallControlState(callEmbed?.control);
|
||||||
|
|
||||||
|
// Sync pip mode into CallControl so it can adjust behavior accordingly
|
||||||
|
useEffect(() => {
|
||||||
|
if (!callEmbed) return;
|
||||||
|
callEmbed.control.setPipMode(!!pipMode);
|
||||||
|
}, [pipMode, callEmbed]);
|
||||||
|
|
||||||
|
// When entering pip with screenshare active (or screenshare starts while in pip),
|
||||||
|
// enable spotlight so the screenshare fills the pip window
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pipMode || !callEmbed || !pipScreenshare) return;
|
||||||
|
if (!callEmbed.control.spotlight) {
|
||||||
|
callEmbed.control.toggleSpotlight();
|
||||||
|
}
|
||||||
|
}, [pipMode, pipScreenshare, callEmbed]);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.kind === ThemeKind.Dark;
|
const isDark = theme.kind === ThemeKind.Dark;
|
||||||
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
|
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import * as css from './styles.css';
|
|||||||
import {
|
import {
|
||||||
ChatButton,
|
ChatButton,
|
||||||
ControlDivider,
|
ControlDivider,
|
||||||
|
FullscreenButton,
|
||||||
MicrophoneButton,
|
MicrophoneButton,
|
||||||
ScreenShareButton,
|
ScreenShareButton,
|
||||||
SoundButton,
|
SoundButton,
|
||||||
@@ -32,13 +33,16 @@ import { settingsAtom } from '../../state/settings';
|
|||||||
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
import { useCallEmbedRef } from '../../hooks/useCallEmbed';
|
||||||
|
|
||||||
type CallControlsProps = {
|
type CallControlsProps = {
|
||||||
callEmbed: CallEmbed;
|
callEmbed: CallEmbed;
|
||||||
};
|
};
|
||||||
export function CallControls({ callEmbed }: CallControlsProps) {
|
export function CallControls({ callEmbed }: CallControlsProps) {
|
||||||
const controlRef = useRef<HTMLDivElement>(null);
|
const controlRef = useRef<HTMLDivElement>(null);
|
||||||
|
const callEmbedRef = useCallEmbedRef();
|
||||||
const [compact, setCompact] = useState(document.body.clientWidth < 500);
|
const [compact, setCompact] = useState(document.body.clientWidth < 500);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
useResizeObserver(
|
useResizeObserver(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -49,6 +53,20 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
useCallback(() => controlRef.current, []),
|
useCallback(() => controlRef.current, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onFullscreenChange = () => setIsFullscreen(!!document.fullscreenElement);
|
||||||
|
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFullscreen = useCallback(() => {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
callEmbedRef.current?.requestFullscreen();
|
||||||
|
}
|
||||||
|
}, [callEmbedRef]);
|
||||||
|
|
||||||
const { microphone, video, sound, screenshare, spotlight } = useCallControlState(
|
const { microphone, video, sound, screenshare, spotlight } = useCallControlState(
|
||||||
callEmbed.control,
|
callEmbed.control,
|
||||||
);
|
);
|
||||||
@@ -338,6 +356,9 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
|
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{screenshare && (
|
||||||
|
<FullscreenButton isFullscreen={isFullscreen} onToggle={handleFullscreen} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{!compact && <ControlDivider />}
|
{!compact && <ControlDivider />}
|
||||||
|
|||||||
@@ -158,6 +158,56 @@ export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FullscreenIcon = () => (
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ExitFullscreenIcon = () => (
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M9 3H7v4H3v2h6V3zm6 0v6h6V7h-4V3h-2zM3 13v2h4v4h2v-6H3zm14 4v-4h2v6h-6v-2h4z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
type FullscreenButtonProps = {
|
||||||
|
isFullscreen: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
export function FullscreenButton({ isFullscreen, onToggle }: FullscreenButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
delay={500}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant="Surface"
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={onToggle}
|
||||||
|
aria-label={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
aria-pressed={isFullscreen}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
{isFullscreen ? (
|
||||||
|
<ExitFullscreenIcon />
|
||||||
|
) : (
|
||||||
|
<FullscreenIcon />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ChatButton() {
|
export function ChatButton() {
|
||||||
const [chat, setChat] = useAtom(callChatAtom);
|
const [chat, setChat] = useAtom(callChatAtom);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
|
|
||||||
private controlMutationObserver: MutationObserver;
|
private controlMutationObserver: MutationObserver;
|
||||||
|
|
||||||
|
private _pipMode = false;
|
||||||
|
|
||||||
private get document(): Document | undefined {
|
private get document(): Document | undefined {
|
||||||
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
|
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
|
||||||
}
|
}
|
||||||
@@ -183,12 +185,6 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
);
|
);
|
||||||
this.emitStateUpdate();
|
this.emitStateUpdate();
|
||||||
|
|
||||||
// EC auto-switches to spotlight when screenshare starts — revert to grid
|
|
||||||
if (!prevScreenshare && screenshare) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.spotlight) this.gridButton?.click();
|
|
||||||
}, 600);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMicrophone(enabled: boolean) {
|
public setMicrophone(enabled: boolean) {
|
||||||
@@ -247,6 +243,10 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.spotlightButton?.click();
|
this.spotlightButton?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setPipMode(pip: boolean) {
|
||||||
|
this._pipMode = pip;
|
||||||
|
}
|
||||||
|
|
||||||
public toggleReactions() {
|
public toggleReactions() {
|
||||||
this.reactionsButton?.click();
|
this.reactionsButton?.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,18 @@ export const useSendClientWidgetApiAction = (api: ClientWidgetApi) => {
|
|||||||
return sendWidgetAction;
|
return sendWidgetAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCallControlState = (control: CallControl): CallControlState => {
|
const DEFAULT_CONTROL_STATE = new CallControlState(false, false, false);
|
||||||
const [state, setState] = useState(control.getState());
|
|
||||||
|
export const useCallControlState = (control: CallControl | undefined): CallControlState => {
|
||||||
|
const [state, setState] = useState(control?.getState() ?? DEFAULT_CONTROL_STATE);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleUpdate = () => {
|
if (!control) {
|
||||||
setState(control.getState());
|
setState(DEFAULT_CONTROL_STATE);
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
setState(control.getState());
|
||||||
|
const handleUpdate = () => setState(control.getState());
|
||||||
control.on(CallControlEvent.StateUpdate, handleUpdate);
|
control.on(CallControlEvent.StateUpdate, handleUpdate);
|
||||||
return () => {
|
return () => {
|
||||||
control.off(CallControlEvent.StateUpdate, handleUpdate);
|
control.off(CallControlEvent.StateUpdate, handleUpdate);
|
||||||
|
|||||||
Reference in New Issue
Block a user