Merge pull request #2737 from robintown/footer-hide-show

Improve interactions to hide/show the footer
This commit is contained in:
Robin 2024-11-08 14:31:17 -05:00 committed by GitHub
commit 022367ec2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 23 deletions

View File

@ -66,7 +66,6 @@ Please see LICENSE in the repository root for full details.
.footer.overlay.hidden { .footer.overlay.hidden {
display: grid; display: grid;
opacity: 0; opacity: 0;
pointer-events: none;
} }
.footer.overlay:has(:focus-visible) { .footer.overlay:has(:focus-visible) {

View File

@ -266,12 +266,22 @@ export const InCallView: FC<InCallViewProps> = ({
}, [vm]); }, [vm]);
const onTouchCancel = useCallback(() => (touchStart.current = null), []); const onTouchCancel = useCallback(() => (touchStart.current = null), []);
// We also need to tell the layout toggle to prevent touch events from // We also need to tell the footer controls to prevent touch events from
// bubbling up, or else the controls will be dismissed before a change event // bubbling up, or else the footer will be dismissed before a click/change
// can be registered on the toggle // event can be registered on the control
const onLayoutToggleTouchEnd = useCallback( const onControlsTouchEnd = useCallback(
(e: TouchEvent) => e.stopPropagation(), (e: TouchEvent) => {
[], // Somehow applying pointer-events: none to the controls when the footer
// is hidden is not enough to stop clicks from happening as the footer
// becomes visible, so we check manually whether the footer is shown
if (showFooter) {
e.stopPropagation();
vm.tapControls();
} else {
e.preventDefault();
}
},
[vm, showFooter],
); );
const onPointerMove = useCallback( const onPointerMove = useCallback(
@ -539,6 +549,7 @@ export const InCallView: FC<InCallViewProps> = ({
key="audio" key="audio"
muted={!muteStates.audio.enabled} muted={!muteStates.audio.enabled}
onClick={toggleMicrophone} onClick={toggleMicrophone}
onTouchEnd={onControlsTouchEnd}
disabled={muteStates.audio.setEnabled === null} disabled={muteStates.audio.setEnabled === null}
data-testid="incall_mute" data-testid="incall_mute"
/>, />,
@ -546,6 +557,7 @@ export const InCallView: FC<InCallViewProps> = ({
key="video" key="video"
muted={!muteStates.video.enabled} muted={!muteStates.video.enabled}
onClick={toggleCamera} onClick={toggleCamera}
onTouchEnd={onControlsTouchEnd}
disabled={muteStates.video.setEnabled === null} disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute" data-testid="incall_videomute"
/>, />,
@ -556,6 +568,7 @@ export const InCallView: FC<InCallViewProps> = ({
key="switch_camera" key="switch_camera"
className={styles.switchCamera} className={styles.switchCamera}
onClick={switchCamera} onClick={switchCamera}
onTouchEnd={onControlsTouchEnd}
/>, />,
); );
if (canScreenshare && !hideScreensharing) { if (canScreenshare && !hideScreensharing) {
@ -565,6 +578,7 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.shareScreen} className={styles.shareScreen}
enabled={isScreenShareEnabled} enabled={isScreenShareEnabled}
onClick={toggleScreensharing} onClick={toggleScreensharing}
onTouchEnd={onControlsTouchEnd}
data-testid="incall_screenshare" data-testid="incall_screenshare"
/>, />,
); );
@ -576,11 +590,18 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.raiseHand} className={styles.raiseHand}
client={client} client={client}
rtcSession={rtcSession} rtcSession={rtcSession}
onTouchEnd={onControlsTouchEnd}
/>, />,
); );
} }
if (layout.type !== "pip") if (layout.type !== "pip")
buttons.push(<SettingsButton key="settings" onClick={openSettings} />); buttons.push(
<SettingsButton
key="settings"
onClick={openSettings}
onTouchEnd={onControlsTouchEnd}
/>,
);
buttons.push( buttons.push(
<EndCallButton <EndCallButton
@ -588,6 +609,7 @@ export const InCallView: FC<InCallViewProps> = ({
onClick={function (): void { onClick={function (): void {
onLeave(); onLeave();
}} }}
onTouchEnd={onControlsTouchEnd}
data-testid="incall_leave" data-testid="incall_leave"
/>, />,
); );
@ -615,7 +637,7 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.layout} className={styles.layout}
layout={gridMode} layout={gridMode}
setLayout={setGridMode} setLayout={setGridMode}
onTouchEnd={onLayoutToggleTouchEnd} onTouchEnd={onControlsTouchEnd}
/> />
)} )}
</div> </div>

View File

@ -85,6 +85,10 @@ const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
// on mobile. No spotlight tile should be shown below this threshold. // on mobile. No spotlight tile should be shown below this threshold.
const smallMobileCallThreshold = 3; const smallMobileCallThreshold = 3;
// How long the footer should be shown for when hovering over or interacting
// with the interface
const showFooterMs = 4000;
export interface GridLayoutMedia { export interface GridLayoutMedia {
type: "grid"; type: "grid";
spotlight?: MediaViewModel[]; spotlight?: MediaViewModel[];
@ -902,6 +906,7 @@ export class CallViewModel extends ViewModel {
); );
private readonly screenTap = new Subject<void>(); private readonly screenTap = new Subject<void>();
private readonly controlsTap = new Subject<void>();
private readonly screenHover = new Subject<void>(); private readonly screenHover = new Subject<void>();
private readonly screenUnhover = new Subject<void>(); private readonly screenUnhover = new Subject<void>();
@ -912,6 +917,13 @@ export class CallViewModel extends ViewModel {
this.screenTap.next(); this.screenTap.next();
} }
/**
* Callback for when the user taps the call's controls.
*/
public tapControls(): void {
this.controlsTap.next();
}
/** /**
* Callback for when the user hovers over the call view. * Callback for when the user hovers over the call view.
*/ */
@ -946,27 +958,38 @@ export class CallViewModel extends ViewModel {
if (isFirefox()) return of(true); if (isFirefox()) return of(true);
// Show/hide the footer in response to interactions // Show/hide the footer in response to interactions
return merge( return merge(
this.screenTap.pipe(map(() => "tap" as const)), this.screenTap.pipe(map(() => "tap screen" as const)),
this.controlsTap.pipe(map(() => "tap controls" as const)),
this.screenHover.pipe(map(() => "hover" as const)), this.screenHover.pipe(map(() => "hover" as const)),
).pipe( ).pipe(
switchScan( switchScan((state, interaction) => {
(state, interaction) => switch (interaction) {
interaction === "tap" case "tap screen":
? state return state
? // Toggle visibility on tap ? // Toggle visibility on tap
of(false) of(false)
: // Hide after a timeout : // Hide after a timeout
timer(6000).pipe( timer(showFooterMs).pipe(
map(() => false), map(() => false),
startWith(true), startWith(true),
) );
: // Show on hover and hide after a timeout case "tap controls":
race(timer(3000), this.screenUnhover.pipe(take(1))).pipe( // The user is interacting with things, so reset the timeout
return timer(showFooterMs).pipe(
map(() => false), map(() => false),
startWith(true), startWith(true),
), );
false, case "hover":
), // Show on hover and hide after a timeout
return race(
timer(showFooterMs),
this.screenUnhover.pipe(take(1)),
).pipe(
map(() => false),
startWith(true),
);
}
}, false),
startWith(false), startWith(false),
); );
} }