Merge pull request 'feat(call): full-screen call when a phone is turned sideways (#126)' (#451) from feat/landscape-call into lotus
CI / Build & Quality Checks (push) Successful in 3m27s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 16s
CI / Trigger Desktop Build (push) Successful in 15s
CI / Playwright smoke (e2e) (push) Successful in 20m32s
CI / Build & Quality Checks (push) Successful in 3m27s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 16s
CI / Trigger Desktop Build (push) Successful in 15s
CI / Playwright smoke (e2e) (push) Successful in 20m32s
This commit was merged in pull request #451.
This commit is contained in:
@@ -37,7 +37,7 @@ import {
|
|||||||
useCallThemeSync,
|
useCallThemeSync,
|
||||||
useCallStart,
|
useCallStart,
|
||||||
} from '../hooks/useCallEmbed';
|
} from '../hooks/useCallEmbed';
|
||||||
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
import { callChatAtom, callEmbedAtom, immersiveCallAtom } from '../state/callEmbed';
|
||||||
import { toastQueueAtom } from '../state/toast';
|
import { toastQueueAtom } from '../state/toast';
|
||||||
import { CallEmbed, useCallControlState, useScreensharePromptOpen } from '../plugins/call';
|
import { CallEmbed, useCallControlState, useScreensharePromptOpen } from '../plugins/call';
|
||||||
import { hangupCallAndWait } from '../plugins/call/hangup';
|
import { hangupCallAndWait } from '../plugins/call/hangup';
|
||||||
@@ -954,6 +954,7 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
const inCallRoom = callEmbed && selectedRoom === callEmbed.roomId;
|
const inCallRoom = callEmbed && selectedRoom === callEmbed.roomId;
|
||||||
const callActive = callEmbed && joined;
|
const callActive = callEmbed && joined;
|
||||||
const callVisible = inCallRoom && callActive && !chatOnlyView;
|
const callVisible = inCallRoom && callActive && !chatOnlyView;
|
||||||
|
const immersiveCall = useAtomValue(immersiveCallAtom);
|
||||||
const pipMode = callActive && !inCallRoom;
|
const pipMode = callActive && !inCallRoom;
|
||||||
const pipPromptOpen = useScreensharePromptOpen(callEmbed?.control);
|
const pipPromptOpen = useScreensharePromptOpen(callEmbed?.control);
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
@@ -1458,7 +1459,13 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
...(pipMode
|
...(pipMode
|
||||||
? { background: color.Surface.Container }
|
? { background: color.Surface.Container }
|
||||||
: callVisible
|
: callVisible
|
||||||
? wallpaperStyle
|
? {
|
||||||
|
// [Gitea #126] Full screen: the frame is transparent, so give
|
||||||
|
// it an opaque base under the wallpaper or the room's
|
||||||
|
// sidebar / call bar show through.
|
||||||
|
...(immersiveCall ? { backgroundColor: color.Background.Container } : {}),
|
||||||
|
...wallpaperStyle,
|
||||||
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}}
|
}}
|
||||||
ref={callEmbedRef}
|
ref={callEmbedRef}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useVoiceChannelFull } from '../../hooks/useVoiceChannelFull';
|
|||||||
import { CallMemberRenderer } from './CallMemberCard';
|
import { CallMemberRenderer } from './CallMemberCard';
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
import { CallControls } from './CallControls';
|
import { CallControls } from './CallControls';
|
||||||
|
import { ImmersiveCallOverlay } from './ImmersiveCallOverlay';
|
||||||
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||||
import { CallsUnavailableMessage } from './CallsUnavailable';
|
import { CallsUnavailableMessage } from './CallsUnavailable';
|
||||||
import { webRTCSupported } from '../../utils/rtc';
|
import { webRTCSupported } from '../../utils/rtc';
|
||||||
@@ -198,10 +199,24 @@ function CallLoadErrorMessage() {
|
|||||||
type CallJoinedProps = {
|
type CallJoinedProps = {
|
||||||
containerRef: RefObject<HTMLDivElement>;
|
containerRef: RefObject<HTMLDivElement>;
|
||||||
joined: boolean;
|
joined: boolean;
|
||||||
|
/** [Gitea #126] Full screen: controls float over the call instead. */
|
||||||
|
onExitImmersive?: () => void;
|
||||||
};
|
};
|
||||||
function CallJoined({ joined, containerRef }: CallJoinedProps) {
|
function CallJoined({ joined, containerRef, onExitImmersive }: CallJoinedProps) {
|
||||||
const callEmbed = useCallEmbed();
|
const callEmbed = useCallEmbed();
|
||||||
|
|
||||||
|
if (onExitImmersive) {
|
||||||
|
return (
|
||||||
|
<Box grow="Yes" direction="Column" style={{ position: 'relative' }}>
|
||||||
|
<Box grow="Yes" ref={containerRef} />
|
||||||
|
{callEmbed && joined && (
|
||||||
|
<ImmersiveCallOverlay callEmbed={callEmbed} onExit={onExitImmersive} />
|
||||||
|
)}
|
||||||
|
{callEmbed && joined && <LotusDecorationPusher callEmbed={callEmbed} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
<Box grow="Yes" ref={containerRef} />
|
<Box grow="Yes" ref={containerRef} />
|
||||||
@@ -212,7 +227,7 @@ function CallJoined({ joined, containerRef }: CallJoinedProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallView() {
|
export function CallView({ onExitImmersive }: { onExitImmersive?: () => void } = {}) {
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
const callContainerRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
const callContainerRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
||||||
useCallEmbedPlacementSync(callContainerRef);
|
useCallEmbedPlacementSync(callContainerRef);
|
||||||
@@ -227,6 +242,26 @@ export function CallView() {
|
|||||||
// made it into the call (a hung iframe / blank spinner otherwise).
|
// made it into the call (a hung iframe / blank spinner otherwise).
|
||||||
const showLoadError = isCurrentRoom && !currentJoined && Boolean(loadError);
|
const showLoadError = isCurrentRoom && !currentJoined && Boolean(loadError);
|
||||||
|
|
||||||
|
if (onExitImmersive && currentJoined) {
|
||||||
|
// [Gitea #126] Cover the screen with a transparent layer above the call
|
||||||
|
// frame: the frame follows the placeholder (useCallEmbedPlacementSync), so
|
||||||
|
// it goes full screen too; the layer only takes touches on its own
|
||||||
|
// tap-catcher / controls, so the call underneath stays usable.
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
paddingTop: 'env(safe-area-inset-top)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CallJoined joined containerRef={callContainerRef} onExitImmersive={onExitImmersive} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={ContainerColor({ variant: 'Surface' })}
|
className={ContainerColor({ variant: 'Surface' })}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { Box, Button, Icon, Icons, Text, config, toRem } from 'folds';
|
||||||
|
import { CallEmbed } from '../../plugins/call';
|
||||||
|
import { CallControls } from './CallControls';
|
||||||
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
|
import { CONTROLS_AUTOHIDE_MS, wantsBrowserFullscreen } from './landscapeCall';
|
||||||
|
|
||||||
|
const isStandalone = (): boolean =>
|
||||||
|
window.matchMedia?.('(display-mode: standalone)').matches ||
|
||||||
|
(navigator as { standalone?: boolean }).standalone === true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Gitea #126] Full-screen call controls. Taps inside the call frame never
|
||||||
|
* reach us, so while the controls are hidden a transparent layer covers the
|
||||||
|
* call: the first tap shows them (and is otherwise swallowed, as in native
|
||||||
|
* video apps); with them up the layer is gone, so the call stays usable. They
|
||||||
|
* hide again after a few seconds without a touch. The first tap in an Android
|
||||||
|
* browser tab also enters the browser's own full screen (hides the address
|
||||||
|
* bar); it is left again when this unmounts (rotate back, hang up, exit).
|
||||||
|
*/
|
||||||
|
export function ImmersiveCallOverlay({
|
||||||
|
callEmbed,
|
||||||
|
onExit,
|
||||||
|
}: {
|
||||||
|
callEmbed: CallEmbed;
|
||||||
|
onExit: () => void;
|
||||||
|
}) {
|
||||||
|
const [shown, setShown] = useState(false);
|
||||||
|
const timer = useRef<number | undefined>(undefined);
|
||||||
|
const enteredFullscreen = useRef(false);
|
||||||
|
const setChat = useSetAtom(callChatAtom);
|
||||||
|
|
||||||
|
const armHide = useCallback(() => {
|
||||||
|
window.clearTimeout(timer.current);
|
||||||
|
timer.current = window.setTimeout(() => setShown(false), CONTROLS_AUTOHIDE_MS);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
window.clearTimeout(timer.current);
|
||||||
|
if (enteredFullscreen.current && document.fullscreenElement === document.documentElement) {
|
||||||
|
document.exitFullscreen().catch(() => undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reveal = () => {
|
||||||
|
if (
|
||||||
|
wantsBrowserFullscreen({
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
standalone: isStandalone(),
|
||||||
|
fullscreenEnabled: !!document.fullscreenEnabled,
|
||||||
|
alreadyFullscreen: !!document.fullscreenElement,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
document.documentElement
|
||||||
|
.requestFullscreen({ navigationUI: 'hide' })
|
||||||
|
.then(() => {
|
||||||
|
enteredFullscreen.current = true;
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
setShown(true);
|
||||||
|
armHide();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!shown) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Show call controls"
|
||||||
|
onClick={reveal}
|
||||||
|
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && reveal()}
|
||||||
|
style={{ position: 'absolute', inset: 0, pointerEvents: 'auto' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
direction="Column"
|
||||||
|
// Any touch on the controls keeps them up a little longer.
|
||||||
|
onPointerDown={armHide}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
paddingLeft: 'env(safe-area-inset-left)',
|
||||||
|
paddingRight: 'env(safe-area-inset-right)',
|
||||||
|
paddingBottom: 'env(safe-area-inset-bottom)',
|
||||||
|
background: 'linear-gradient(to top, rgba(0,0,0,0.7), rgba(0,0,0,0))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
justifyContent="End"
|
||||||
|
gap="200"
|
||||||
|
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
radii="Pill"
|
||||||
|
before={<Icon size="100" src={Icons.Message} />}
|
||||||
|
onClick={() => setChat(true)}
|
||||||
|
>
|
||||||
|
<Text size="B300">Chat</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
radii="Pill"
|
||||||
|
before={<Icon size="100" src={Icons.Cross} />}
|
||||||
|
onClick={onExit}
|
||||||
|
style={{ minWidth: toRem(40) }}
|
||||||
|
>
|
||||||
|
<Text size="B300">Exit full screen</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<CallControls callEmbed={callEmbed} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { shouldGoImmersive, wantsBrowserFullscreen } from './landscapeCall';
|
||||||
|
|
||||||
|
const base = { landscapePhone: true, joinedHere: true, chatOpen: false, dismissed: false };
|
||||||
|
|
||||||
|
test('[#126] full screen only for a joined call on a sideways phone', () => {
|
||||||
|
assert.equal(shouldGoImmersive(base), true);
|
||||||
|
assert.equal(shouldGoImmersive({ ...base, landscapePhone: false }), false, 'upright / tablet');
|
||||||
|
assert.equal(shouldGoImmersive({ ...base, joinedHere: false }), false, 'prescreen / other room');
|
||||||
|
assert.equal(shouldGoImmersive({ ...base, chatOpen: true }), false, 'chat open');
|
||||||
|
assert.equal(shouldGoImmersive({ ...base, dismissed: true }), false, 'user exited');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[#126] browser full screen only in an Android browser tab', () => {
|
||||||
|
const env = {
|
||||||
|
userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) Chrome/140 Mobile',
|
||||||
|
standalone: false,
|
||||||
|
fullscreenEnabled: true,
|
||||||
|
alreadyFullscreen: false,
|
||||||
|
};
|
||||||
|
assert.equal(wantsBrowserFullscreen(env), true);
|
||||||
|
assert.equal(wantsBrowserFullscreen({ ...env, standalone: true }), false, 'installed app');
|
||||||
|
assert.equal(
|
||||||
|
wantsBrowserFullscreen({
|
||||||
|
...env,
|
||||||
|
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0) Safari',
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'iPhone',
|
||||||
|
);
|
||||||
|
assert.equal(wantsBrowserFullscreen({ ...env, alreadyFullscreen: true }), false);
|
||||||
|
assert.equal(wantsBrowserFullscreen({ ...env, fullscreenEnabled: false }), false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* [Gitea #126] Phone turned sideways in a call: the call takes the whole
|
||||||
|
* screen. A phone in landscape is ~840px wide (the "Tablet" size class), so
|
||||||
|
* the trigger is the shape of the screen, not the size class: landscape, at
|
||||||
|
* most 500px tall, touch input. Tablets and desktops never match.
|
||||||
|
*/
|
||||||
|
export const LANDSCAPE_PHONE_QUERY =
|
||||||
|
'(orientation: landscape) and (max-height: 500px) and (pointer: coarse)';
|
||||||
|
|
||||||
|
/** Controls hide again after this long without a touch on them. */
|
||||||
|
export const CONTROLS_AUTOHIDE_MS = 4000;
|
||||||
|
|
||||||
|
export type ImmersiveInputs = {
|
||||||
|
landscapePhone: boolean;
|
||||||
|
/** This room's call is the one we are in, and we have joined it. */
|
||||||
|
joinedHere: boolean;
|
||||||
|
/** The in-call chat is open (it replaces the call on small screens). */
|
||||||
|
chatOpen: boolean;
|
||||||
|
/** "Exit full screen" was pressed since the phone was last upright. */
|
||||||
|
dismissed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Pure: whether the call should be full screen. */
|
||||||
|
export const shouldGoImmersive = (i: ImmersiveInputs): boolean =>
|
||||||
|
i.landscapePhone && i.joinedHere && !i.chatOpen && !i.dismissed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure: use the browser's own full-screen mode too? Only where an address bar
|
||||||
|
* takes room: an Android browser tab (not the installed app / iOS, which have
|
||||||
|
* none or don't support it), when the page isn't already full screen.
|
||||||
|
*/
|
||||||
|
export const wantsBrowserFullscreen = (env: {
|
||||||
|
userAgent: string;
|
||||||
|
standalone: boolean;
|
||||||
|
fullscreenEnabled: boolean;
|
||||||
|
alreadyFullscreen: boolean;
|
||||||
|
}): boolean =>
|
||||||
|
/Android/i.test(env.userAgent) &&
|
||||||
|
!env.standalone &&
|
||||||
|
env.fullscreenEnabled &&
|
||||||
|
!env.alreadyFullscreen;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Box, Line } from 'folds';
|
import { Box, Line } from 'folds';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
@@ -11,7 +11,6 @@ import { WidgetsPanel } from './widgets/WidgetsPanel';
|
|||||||
import { widgetsPanelAtom } from '../../state/widgetsPanel';
|
import { widgetsPanelAtom } from '../../state/widgetsPanel';
|
||||||
import { mobileMembersPanelAtom } from '../../state/mobileMembersPanel';
|
import { mobileMembersPanelAtom } from '../../state/mobileMembersPanel';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { useMediaQuery } from '../../hooks/useMediaQuery';
|
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
||||||
@@ -21,10 +20,12 @@ import { markAsRead } from '../../utils/notifications';
|
|||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||||
import { CallView } from '../call/CallView';
|
import { CallView } from '../call/CallView';
|
||||||
|
import { LANDSCAPE_PHONE_QUERY, shouldGoImmersive } from '../call/landscapeCall';
|
||||||
|
import { useMediaQuery } from '../../hooks/useMediaQuery';
|
||||||
import { RoomViewHeader } from './RoomViewHeader';
|
import { RoomViewHeader } from './RoomViewHeader';
|
||||||
import { callChatAtom } from '../../state/callEmbed';
|
import { callChatAtom, immersiveCallAtom } from '../../state/callEmbed';
|
||||||
import { CallChatView } from './CallChatView';
|
import { CallChatView } from './CallChatView';
|
||||||
import { useCallEmbed } from '../../hooks/useCallEmbed';
|
import { useCallEmbed, useCallJoined } from '../../hooks/useCallEmbed';
|
||||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||||
import { roomIdToActiveThreadIdAtomFamily } from '../../state/room/thread';
|
import { roomIdToActiveThreadIdAtomFamily } from '../../state/room/thread';
|
||||||
import { threadsListAtom } from '../../state/threadsList';
|
import { threadsListAtom } from '../../state/threadsList';
|
||||||
@@ -57,6 +58,25 @@ export function Room() {
|
|||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
const members = useRoomMembers(mx, room.roomId);
|
const members = useRoomMembers(mx, room.roomId);
|
||||||
const chat = useAtomValue(callChatAtom);
|
const chat = useAtomValue(callChatAtom);
|
||||||
|
// [Gitea #126] Phone turned sideways in this room's call: full screen.
|
||||||
|
const landscapePhone = useMediaQuery(LANDSCAPE_PHONE_QUERY);
|
||||||
|
const joinedAnywhere = useCallJoined(callEmbed);
|
||||||
|
const [immersiveDismissed, setImmersiveDismissed] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
// "Exit full screen" lasts until the phone is upright again.
|
||||||
|
if (!landscapePhone) setImmersiveDismissed(false);
|
||||||
|
}, [landscapePhone]);
|
||||||
|
const setImmersiveCall = useSetAtom(immersiveCallAtom);
|
||||||
|
const immersive = shouldGoImmersive({
|
||||||
|
landscapePhone,
|
||||||
|
joinedHere: joinedAnywhere && callEmbed?.roomId === room.roomId,
|
||||||
|
chatOpen: chat,
|
||||||
|
dismissed: immersiveDismissed,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
setImmersiveCall(immersive);
|
||||||
|
return () => setImmersiveCall(false);
|
||||||
|
}, [immersive, setImmersiveCall]);
|
||||||
// Hoisted here (instead of inside RoomTimeline) so ThreadPanel/ThreadTimeline —
|
// Hoisted here (instead of inside RoomTimeline) so ThreadPanel/ThreadTimeline —
|
||||||
// rendered as siblings of RoomView, not descendants — share the same computed
|
// rendered as siblings of RoomView, not descendants — share the same computed
|
||||||
// positions instead of falling back to the context default (Gitea #38).
|
// positions instead of falling back to the context default (Gitea #38).
|
||||||
@@ -170,7 +190,8 @@ export function Room() {
|
|||||||
<PowerLevelsContextProvider value={powerLevels}>
|
<PowerLevelsContextProvider value={powerLevels}>
|
||||||
<ReadPositionsContext.Provider value={readPositions}>
|
<ReadPositionsContext.Provider value={readPositions}>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
{callView && (screenSize === ScreenSize.Desktop || !chat) && (
|
{immersive && <CallView onExitImmersive={() => setImmersiveDismissed(true)} />}
|
||||||
|
{!immersive && callView && (screenSize === ScreenSize.Desktop || !chat) && (
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
<RoomViewHeader callView />
|
<RoomViewHeader callView />
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { immersiveCallAtom } from '../state/callEmbed';
|
||||||
import { useCallEmbed } from '../hooks/useCallEmbed';
|
import { useCallEmbed } from '../hooks/useCallEmbed';
|
||||||
import { CallStatus } from '../features/call-status';
|
import { CallStatus } from '../features/call-status';
|
||||||
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||||
@@ -9,8 +11,10 @@ export function CallStatusRenderer() {
|
|||||||
const selectedRoom = useSelectedRoom();
|
const selectedRoom = useSelectedRoom();
|
||||||
|
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
|
// [Gitea #126] The full-screen call has its own controls.
|
||||||
|
const immersive = useAtomValue(immersiveCallAtom);
|
||||||
|
|
||||||
if (!callEmbed) return null;
|
if (!callEmbed || immersive) return null;
|
||||||
|
|
||||||
if (screenSize === ScreenSize.Mobile && callEmbed.roomId === selectedRoom) return null;
|
if (screenSize === ScreenSize.Mobile && callEmbed.roomId === selectedRoom) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,6 @@ export const callEmbedAtom = atom<CallEmbed | undefined, [CallEmbed | undefined]
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const callChatAtom = atom<boolean>(false);
|
export const callChatAtom = atom<boolean>(false);
|
||||||
|
|
||||||
|
/** [Gitea #126] The call is full screen (phone turned sideways). */
|
||||||
|
export const immersiveCallAtom = atom<boolean>(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user