Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve interactions to hide/show the footer #2737

Merged
merged 1 commit into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/room/InCallView.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ Please see LICENSE in the repository root for full details.
.footer.overlay.hidden {
display: grid;
opacity: 0;
pointer-events: none;
}

.footer.overlay:has(:focus-visible) {
Expand Down
38 changes: 30 additions & 8 deletions src/room/InCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,22 @@ export const InCallView: FC<InCallViewProps> = ({
}, [vm]);
const onTouchCancel = useCallback(() => (touchStart.current = null), []);

// We also need to tell the layout toggle to prevent touch events from
// bubbling up, or else the controls will be dismissed before a change event
// can be registered on the toggle
const onLayoutToggleTouchEnd = useCallback(
(e: TouchEvent) => e.stopPropagation(),
[],
// We also need to tell the footer controls to prevent touch events from
// bubbling up, or else the footer will be dismissed before a click/change
// event can be registered on the control
const onControlsTouchEnd = useCallback(
(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(
Expand Down Expand Up @@ -528,13 +538,15 @@ export const InCallView: FC<InCallViewProps> = ({
key="audio"
muted={!muteStates.audio.enabled}
onClick={toggleMicrophone}
onTouchEnd={onControlsTouchEnd}
disabled={muteStates.audio.setEnabled === null}
data-testid="incall_mute"
/>,
<VideoButton
key="video"
muted={!muteStates.video.enabled}
onClick={toggleCamera}
onTouchEnd={onControlsTouchEnd}
disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute"
/>,
Expand All @@ -545,6 +557,7 @@ export const InCallView: FC<InCallViewProps> = ({
key="switch_camera"
className={styles.switchCamera}
onClick={switchCamera}
onTouchEnd={onControlsTouchEnd}
/>,
);
if (canScreenshare && !hideScreensharing) {
Expand All @@ -554,6 +567,7 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.shareScreen}
enabled={isScreenShareEnabled}
onClick={toggleScreensharing}
onTouchEnd={onControlsTouchEnd}
data-testid="incall_screenshare"
/>,
);
Expand All @@ -565,18 +579,26 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.raiseHand}
client={client}
rtcSession={rtcSession}
onTouchEnd={onControlsTouchEnd}
/>,
);
}
if (layout.type !== "pip")
buttons.push(<SettingsButton key="settings" onClick={openSettings} />);
buttons.push(
<SettingsButton
key="settings"
onClick={openSettings}
onTouchEnd={onControlsTouchEnd}
/>,
);

buttons.push(
<EndCallButton
key="end_call"
onClick={function (): void {
onLeave();
}}
onTouchEnd={onControlsTouchEnd}
data-testid="incall_leave"
/>,
);
Expand Down Expand Up @@ -604,7 +626,7 @@ export const InCallView: FC<InCallViewProps> = ({
className={styles.layout}
layout={gridMode}
setLayout={setGridMode}
onTouchEnd={onLayoutToggleTouchEnd}
onTouchEnd={onControlsTouchEnd}
/>
)}
</div>
Expand Down
51 changes: 37 additions & 14 deletions src/state/CallViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
// on mobile. No spotlight tile should be shown below this threshold.
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 {
type: "grid";
spotlight?: MediaViewModel[];
Expand Down Expand Up @@ -902,6 +906,7 @@ export class CallViewModel extends ViewModel {
);

private readonly screenTap = new Subject<void>();
private readonly controlsTap = new Subject<void>();
private readonly screenHover = new Subject<void>();
private readonly screenUnhover = new Subject<void>();

Expand All @@ -912,6 +917,13 @@ export class CallViewModel extends ViewModel {
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.
*/
Expand Down Expand Up @@ -946,27 +958,38 @@ export class CallViewModel extends ViewModel {
if (isFirefox()) return of(true);
// Show/hide the footer in response to interactions
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)),
).pipe(
switchScan(
(state, interaction) =>
interaction === "tap"
? state
switchScan((state, interaction) => {
switch (interaction) {
case "tap screen":
return state
? // Toggle visibility on tap
of(false)
: // Hide after a timeout
timer(6000).pipe(
timer(showFooterMs).pipe(
map(() => false),
startWith(true),
)
: // Show on hover and hide after a timeout
race(timer(3000), this.screenUnhover.pipe(take(1))).pipe(
map(() => false),
startWith(true),
),
false,
),
);
case "tap controls":
// The user is interacting with things, so reset the timeout
return timer(showFooterMs).pipe(
map(() => false),
startWith(true),
);
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),
);
}
Expand Down