mirror of
https://github.com/vector-im/element-call.git
synced 2026-06-27 17:52:56 +00:00
Merge branch 'livekit' into toger5/view-model-call-footer-example
This commit is contained in:
@@ -26,6 +26,33 @@ Please see LICENSE in the repository root for full details.
|
||||
);
|
||||
}
|
||||
|
||||
.header.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header.overlay {
|
||||
/* Note that the header is still position: sticky in this case so that certain
|
||||
tiles can move down out of the way of the header when visible. */
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.header.overlay.hidden {
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
/* Switch to position: absolute so the header takes up no space in the layout
|
||||
when hidden. */
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
}
|
||||
|
||||
.header.overlay:has(:focus-visible) {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.header.filler {
|
||||
block-size: var(--cpd-space-6x);
|
||||
background: none;
|
||||
|
||||
@@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details.
|
||||
import { type MatrixClient, type Room as MatrixRoom } from "matrix-js-sdk";
|
||||
import {
|
||||
type FC,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -21,7 +20,7 @@ import {
|
||||
import useMeasure from "react-use-measure";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import classNames from "classnames";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
import { map } from "rxjs";
|
||||
import { useObservable } from "observable-hooks";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -50,12 +49,9 @@ import { SpotlightTile } from "../tile/SpotlightTile";
|
||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { makeGridLayout } from "../grid/GridLayout";
|
||||
import {
|
||||
type CallLayoutOutputs,
|
||||
defaultPipAlignment,
|
||||
defaultSpotlightAlignment,
|
||||
} from "../grid/CallLayout";
|
||||
import { makeOneOnOneLayout } from "../grid/OneOnOneLayout";
|
||||
import { type CallLayoutOutputs } from "../grid/CallLayout";
|
||||
import { makeOneOnOneLandscapeLayout } from "../grid/OneOnOneLandscapeLayout";
|
||||
import { makeOneOnOnePortraitLayout } from "../grid/OneOnOnePortraitLayout";
|
||||
import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout";
|
||||
import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout";
|
||||
import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout";
|
||||
@@ -90,6 +86,13 @@ import { SettingsIconButton } from "../button/Button.tsx";
|
||||
import { createCallFooterViewModel } from "../components/CallFooterViewModel.tsx";
|
||||
import { type ViewModel } from "../state/ViewModel.ts";
|
||||
|
||||
declare module "react" {
|
||||
interface CSSProperties {
|
||||
"--call-view-safe-area-inset-top"?: string;
|
||||
"--call-view-safe-area-inset-bottom"?: string;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = rootLogger.getChild("[InCallView]");
|
||||
|
||||
export interface ActiveCallProps extends Omit<
|
||||
@@ -267,8 +270,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const audioParticipants = useBehavior(vm.livekitRoomItems$);
|
||||
const participantCount = useBehavior(vm.participantCount$);
|
||||
const reconnecting = useBehavior(vm.reconnecting$);
|
||||
const windowMode = useBehavior(vm.windowMode$);
|
||||
const layout = useBehavior(vm.layout$);
|
||||
const edgeToEdge = useBehavior(vm.edgeToEdge$);
|
||||
const showNameTags = useBehavior(vm.showNameTags$);
|
||||
const showHeader = useBehavior(vm.showHeader$);
|
||||
const settingsOpen = useBehavior(vm.settingsOpen$);
|
||||
const setSettingsOpen = useBehavior(vm.setSettingsOpen$);
|
||||
@@ -298,10 +302,13 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
}
|
||||
}, [ringing, latestPickupPhaseAudio]);
|
||||
|
||||
const onViewClick = useCallback(
|
||||
(e: ReactMouseEvent) => {
|
||||
// iOS Safari doesn't reliably fire `click` on plain <div>s, so we listen
|
||||
// for `pointerup` instead. Scrolls end in `pointercancel`, not `pointerup`,
|
||||
// so this still only fires for taps.
|
||||
const onViewPointerUp = useCallback(
|
||||
(e: ReactPointerEvent) => {
|
||||
if (
|
||||
(e.nativeEvent as PointerEvent).pointerType === "touch" &&
|
||||
e.pointerType === "touch" &&
|
||||
// If an interactive element was tapped, don't count this as a tap on the screen
|
||||
(e.target as Element).closest?.("button, input") === null
|
||||
)
|
||||
@@ -340,15 +347,14 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
width: bounds.width,
|
||||
height:
|
||||
bounds.height -
|
||||
headerBounds.height -
|
||||
(windowMode === "flat" ? 0 : footerBounds.height),
|
||||
(edgeToEdge ? 0 : headerBounds.height + footerBounds.height),
|
||||
}),
|
||||
[
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
headerBounds.height,
|
||||
footerBounds.height,
|
||||
windowMode,
|
||||
edgeToEdge,
|
||||
],
|
||||
);
|
||||
const gridBoundsObservable$ = useObservable(
|
||||
@@ -361,54 +367,55 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
);
|
||||
const pipAlignment$ = useInitial(
|
||||
() => new BehaviorSubject(defaultPipAlignment),
|
||||
const setGridMode = useCallback(
|
||||
(mode: GridMode) => vm.setGridMode(mode),
|
||||
[vm],
|
||||
);
|
||||
|
||||
useAppBarHidden(!showHeader);
|
||||
|
||||
let header: ReactNode = null;
|
||||
if (showHeader) {
|
||||
switch (headerStyle) {
|
||||
case HeaderStyle.AppBar: {
|
||||
// dont build a header here. The AppBar will take care of it.
|
||||
break;
|
||||
}
|
||||
case HeaderStyle.None:
|
||||
// Cosmetic header to fill out space while still affecting the bounds of
|
||||
// the grid
|
||||
header = (
|
||||
<div
|
||||
className={classNames(styles.header, styles.filler)}
|
||||
ref={headerRef}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case HeaderStyle.Standard:
|
||||
header = (
|
||||
<Header
|
||||
className={styles.header}
|
||||
ref={headerRef}
|
||||
disconnectedBanner={false} // This screen has its own 'reconnecting' toast
|
||||
>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo
|
||||
id={matrixInfo.roomId}
|
||||
name={matrixInfo.roomName}
|
||||
avatarUrl={matrixInfo.roomAvatar}
|
||||
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||
participantCount={participantCount}
|
||||
/>
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
{showControls && onShareClick !== null && (
|
||||
<InviteButton
|
||||
className={styles.invite}
|
||||
onClick={onShareClick}
|
||||
/>
|
||||
)}
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
switch (headerStyle) {
|
||||
case HeaderStyle.AppBar: {
|
||||
// dont build a header here. The AppBar will take care of it.
|
||||
break;
|
||||
}
|
||||
case HeaderStyle.None:
|
||||
// Cosmetic header to fill out space while still affecting the bounds of
|
||||
// the grid
|
||||
header = showHeader && (
|
||||
<div
|
||||
className={classNames(styles.header, styles.filler)}
|
||||
ref={headerRef}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case HeaderStyle.Standard:
|
||||
header = (
|
||||
<Header
|
||||
className={classNames(styles.header, {
|
||||
[styles.overlay]: edgeToEdge,
|
||||
[styles.hidden]: !showHeader,
|
||||
})}
|
||||
ref={headerRef}
|
||||
disconnectedBanner={false} // This screen has its own 'reconnecting' toast
|
||||
>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo
|
||||
id={matrixInfo.roomId}
|
||||
name={matrixInfo.roomName}
|
||||
avatarUrl={matrixInfo.roomAvatar}
|
||||
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||
participantCount={participantCount}
|
||||
/>
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
{showControls && onShareClick !== null && (
|
||||
<InviteButton className={styles.invite} onClick={onShareClick} />
|
||||
)}
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
// The reconnecting toast cannot be dismissed
|
||||
@@ -455,12 +462,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
}: TileProps<TileViewModel, HTMLDivElement>): ReactNode {
|
||||
const spotlightExpanded = useBehavior(vm.spotlightExpanded$);
|
||||
const onToggleExpanded = useBehavior(vm.toggleSpotlightExpanded$);
|
||||
const showSpeakingIndicatorsValue = useBehavior(
|
||||
vm.showSpeakingIndicators$,
|
||||
);
|
||||
const showSpotlightIndicatorsValue = useBehavior(
|
||||
const showSpotlightIndicators = useBehavior(
|
||||
vm.showSpotlightIndicators$,
|
||||
);
|
||||
const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$);
|
||||
const showNameTags = useBehavior(vm.showNameTags$);
|
||||
|
||||
return model instanceof GridTileViewModel ? (
|
||||
<GridTile
|
||||
@@ -471,7 +477,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetHeight={targetHeight}
|
||||
className={classNames(className, styles.tile)}
|
||||
style={style}
|
||||
showSpeakingIndicators={showSpeakingIndicatorsValue}
|
||||
showSpeakingIndicators={showSpeakingIndicators}
|
||||
showNameTags={showNameTags}
|
||||
focusable={!contentObscured}
|
||||
/>
|
||||
) : (
|
||||
@@ -482,7 +489,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
onToggleExpanded={onToggleExpanded}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
showIndicators={showSpotlightIndicatorsValue}
|
||||
showIndicators={showSpotlightIndicators}
|
||||
showNameTags={showNameTags}
|
||||
focusable={!contentObscured}
|
||||
className={classNames(className, styles.tile)}
|
||||
style={style}
|
||||
@@ -493,19 +501,16 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
);
|
||||
|
||||
const layouts = useMemo(() => {
|
||||
const inputs = {
|
||||
minBounds$: gridBoundsObservable$,
|
||||
spotlightAlignment$,
|
||||
pipAlignment$,
|
||||
};
|
||||
const inputs = { minBounds$: gridBoundsObservable$ };
|
||||
return {
|
||||
grid: makeGridLayout(inputs),
|
||||
"spotlight-landscape": makeSpotlightLandscapeLayout(inputs),
|
||||
"spotlight-portrait": makeSpotlightPortraitLayout(inputs),
|
||||
"spotlight-expanded": makeSpotlightExpandedLayout(inputs),
|
||||
"one-on-one": makeOneOnOneLayout(inputs),
|
||||
"one-on-one-landscape": makeOneOnOneLandscapeLayout(inputs),
|
||||
"one-on-one-portrait": makeOneOnOnePortraitLayout(inputs),
|
||||
};
|
||||
}, [gridBoundsObservable$, spotlightAlignment$, pipAlignment$]);
|
||||
}, [gridBoundsObservable$]);
|
||||
|
||||
const renderContent = (): JSX.Element => {
|
||||
if (layout.type === "pip") {
|
||||
@@ -518,6 +523,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetWidth={gridBounds.width}
|
||||
targetHeight={gridBounds.height}
|
||||
showIndicators={false}
|
||||
showNameTags={showNameTags}
|
||||
focusable={!contentObscured}
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
@@ -531,8 +537,18 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
className={styles.fixedGrid}
|
||||
style={{
|
||||
insetBlockStart:
|
||||
headerBounds.height > 0 ? headerBounds.bottom : bounds.top,
|
||||
height: gridBounds.height,
|
||||
edgeToEdge || headerBounds.height === 0 ? 0 : headerBounds.bottom,
|
||||
height: edgeToEdge ? "100%" : gridBounds.height,
|
||||
// If edge-to-edge, compute new safe area insets that account for the
|
||||
// header and footer.
|
||||
"--call-view-safe-area-inset-top":
|
||||
edgeToEdge && header && showHeader
|
||||
? `calc(env(safe-area-inset-top) + ${headerBounds.height}px)`
|
||||
: undefined,
|
||||
"--call-view-safe-area-inset-bottom":
|
||||
edgeToEdge && showFooter
|
||||
? `calc(env(safe-area-inset-bottom) + ${footerBounds.height}px)`
|
||||
: undefined,
|
||||
}}
|
||||
model={layout}
|
||||
Layout={layers.fixed}
|
||||
@@ -550,19 +566,24 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
);
|
||||
// The grid tiles go *under* the spotlight in the portrait layout, but
|
||||
// *over* the spotlight in the expanded layout
|
||||
return layout.type === "spotlight-expanded" ? (
|
||||
<>
|
||||
{fixedGrid}
|
||||
{scrollingGrid}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{scrollingGrid}
|
||||
{fixedGrid}
|
||||
</>
|
||||
);
|
||||
|
||||
// Put the right layer in the foreground for the requested layout
|
||||
switch (layers.foreground) {
|
||||
case "fixed":
|
||||
return (
|
||||
<>
|
||||
{scrollingGrid}
|
||||
{fixedGrid}
|
||||
</>
|
||||
);
|
||||
case "scrolling":
|
||||
return (
|
||||
<>
|
||||
{fixedGrid}
|
||||
{scrollingGrid}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
||||
@@ -579,18 +600,19 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
|
||||
// Only hide the settings button if we have an AppBar header and we are showing the header
|
||||
const footer = footerVm !== null && (
|
||||
//asOverlay={edgeToEdge}
|
||||
<CallFooter ref={footerRef} vm={footerVm} />
|
||||
);
|
||||
const allConnections = useBehavior(vm.allConnections$);
|
||||
|
||||
return (
|
||||
// The onClick handler here exists to control the visibility of the footer,
|
||||
// The pointer handler here exists to control the visibility of the footer,
|
||||
// and the footer is also viewable by moving focus into it, so this is fine.
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={styles.inRoom}
|
||||
ref={containerRef}
|
||||
onClick={onViewClick}
|
||||
onPointerUp={onViewPointerUp}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerOut={onPointerOut}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user