diff --git a/src/AppBar.module.css b/src/AppBar.module.css index 5f7888d2..13e3b759 100644 --- a/src/AppBar.module.css +++ b/src/AppBar.module.css @@ -1,5 +1,21 @@ .bar { flex-shrink: 0; + position: relative; +} + +/* Pseudo-element for the gradient background */ +.bar::before { + content: ""; + position: absolute; + inset-inline: 0; + /* Extend the gradient beyond the bottom of the header for readability */ + inset-block: -24px; + z-index: var(--call-view-header-footer-layer); + background: linear-gradient( + 0deg, + rgba(0, 0, 0, 0) 0%, + var(--cpd-color-bg-canvas-default) 100% + ); } .bar > header { diff --git a/src/grid/OneOnOnePortraitLayout.tsx b/src/grid/OneOnOnePortraitLayout.tsx index 2f302349..a4c30462 100644 --- a/src/grid/OneOnOnePortraitLayout.tsx +++ b/src/grid/OneOnOnePortraitLayout.tsx @@ -31,7 +31,7 @@ export const makeOneOnOnePortraitLayout: CallLayout<
diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 7ee9b589..0f6d38f2 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -240,6 +240,7 @@ export const InCallView: FC = ({ const edgeToEdge = useBehavior(vm.edgeToEdge$); const tileStoreGeneration = useBehavior(vm.tileStoreGeneration$); const [debugTileLayout] = useSetting(debugTileLayoutSetting); + const showNameTags = useBehavior(vm.showNameTags$); const gridMode = useBehavior(vm.gridMode$); const showHeader = useBehavior(vm.showHeader$); const showFooter = useBehavior(vm.showFooter$); @@ -432,12 +433,11 @@ export const InCallView: FC = ({ }: TileProps): 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 ? ( = ({ targetHeight={targetHeight} className={classNames(className, styles.tile)} style={style} - showSpeakingIndicators={showSpeakingIndicatorsValue} + showSpeakingIndicators={showSpeakingIndicators} + showNameTags={showNameTags} focusable={!contentObscured} /> ) : ( @@ -459,7 +460,8 @@ export const InCallView: FC = ({ onToggleExpanded={onToggleExpanded} targetWidth={targetWidth} targetHeight={targetHeight} - showIndicators={showSpotlightIndicatorsValue} + showIndicators={showSpotlightIndicators} + showNameTags={showNameTags} focusable={!contentObscured} className={classNames(className, styles.tile)} style={style} @@ -492,6 +494,7 @@ export const InCallView: FC = ({ targetWidth={gridBounds.width} targetHeight={gridBounds.height} showIndicators={false} + showNameTags={showNameTags} focusable={!contentObscured} aria-hidden={contentObscured} /> @@ -505,10 +508,8 @@ export const InCallView: FC = ({ className={styles.fixedGrid} style={{ insetBlockStart: - edgeToEdge || headerBounds.height === 0 - ? bounds.top - : headerBounds.bottom, - height: gridBounds.height, + edgeToEdge || headerBounds.height === 0 ? 0 : headerBounds.bottom, + height: edgeToEdge ? "100%" : gridBounds.height, }} model={layout} Layout={layers.fixed} diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index a9fd18fc..1b795d7d 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -341,6 +341,7 @@ export interface CallViewModel { tileStoreGeneration$: Behavior; showSpotlightIndicators$: Behavior; showSpeakingIndicators$: Behavior; + showNameTags$: Behavior; spotlightExpanded$: Behavior; toggleSpotlightExpanded$: Behavior<(() => void) | null>; gridMode$: Behavior; @@ -1273,6 +1274,31 @@ export function createCallViewModel$( ), ); + const showNameTags$ = scope.behavior( + layoutMedia$.pipe( + switchMap((l) => + l.type === "pip" || l.type === "one-on-one-portrait" + ? matrixRoomMembers$.pipe( + map( + (members) => + // Hide name tags by default in these layouts. For safety we + // still need to show them in case it wouldn't be clear who + // the spotlight media belongs to. + // TODO: Respect io.element.functional_members + // TODO: Only hide name tags in DMs, not group chats that just + // happen to have only 2 users + members.size > 2 || + !(l.type === "pip" + ? l.spotlight.length <= 1 && + members.has(l.spotlight[0].userId) + : members.has(l.spotlight.userId)), + ), + ) + : of(true), + ), + ), + ); + const toggleSpotlightExpanded$ = scope.behavior<(() => void) | null>( windowMode$.pipe( switchMap((mode) => @@ -1716,6 +1742,7 @@ export function createCallViewModel$( tileStoreGeneration$: tileStoreGeneration$, showSpotlightIndicators$: showSpotlightIndicators$, showSpeakingIndicators$: showSpeakingIndicators$, + showNameTags$, showHeader$: showHeader$, showFooter$: showFooter$, edgeToEdge$, diff --git a/src/state/OneOnOnePortraitLayout.ts b/src/state/OneOnOnePortraitLayout.ts index 8f4e3b8c..10365f45 100644 --- a/src/state/OneOnOnePortraitLayout.ts +++ b/src/state/OneOnOnePortraitLayout.ts @@ -25,16 +25,16 @@ export function oneOnOnePortraitLayout( pipAlignment$: BehaviorSubject, prevTiles: TileStore, ): [OneOnOnePortraitLayout, TileStore] { - const update = prevTiles.from(media.pip === undefined ? 1 : 2); - if (media.pip !== undefined) update.registerGridTile(media.pip, false); - update.registerGridTile(media.spotlight, false, true); + const update = prevTiles.from(media.pip === undefined ? 0 : 1); + update.registerSpotlight([media.spotlight], true); + if (media.pip !== undefined) update.registerGridTile(media.pip); const tiles = update.build(); return [ { type: media.type, foreground: "fixed", - spotlight: tiles.gridTilesByMedia.get(media.spotlight)!, + spotlight: tiles.spotlightTile!, pip: media.pip && tiles.gridTilesByMedia.get(media.pip), pipSize$, pipAlignment$, diff --git a/src/state/TileStore.ts b/src/state/TileStore.ts index ca9381bc..300e6bd2 100644 --- a/src/state/TileStore.ts +++ b/src/state/TileStore.ts @@ -59,37 +59,11 @@ class GridTileData { this.media$.next(value); } - private readonly showNameTag$: BehaviorSubject; - public get showNameTag(): boolean { - return this.showNameTag$.value; - } - public set showNameTag(value: boolean) { - this.showNameTag$.next(value); - } - - private readonly edgeToEdge$: BehaviorSubject; - public get edgeToEdge(): boolean { - return this.edgeToEdge$.value; - } - public set edgeToEdge(value: boolean) { - this.edgeToEdge$.next(value); - } - public readonly vm: GridTileViewModel; - public constructor( - media: UserMediaViewModel | RingingMediaViewModel, - showNameTag: boolean, - edgeToEdge: boolean, - ) { + public constructor(media: UserMediaViewModel | RingingMediaViewModel) { this.media$ = new BehaviorSubject(media); - this.showNameTag$ = new BehaviorSubject(showNameTag); - this.edgeToEdge$ = new BehaviorSubject(edgeToEdge); - this.vm = new GridTileViewModel( - this.media$, - this.showNameTag$, - this.edgeToEdge$, - ); + this.vm = new GridTileViewModel(this.media$); } } @@ -209,8 +183,6 @@ export class TileStoreBuilder { */ public registerGridTile( media: UserMediaViewModel | RingingMediaViewModel, - showNameTag = true, - edgeToEdge = false, ): void { if (DEBUG_ENABLED) logger.debug( @@ -248,8 +220,6 @@ export class TileStoreBuilder { this.stationaryGridEntries[prevIndex] = entry; // Do the media swap entry.media = media; - entry.showNameTag = showNameTag; - entry.edgeToEdge = edgeToEdge; this.prevGridByMedia.delete(this.spotlight.media[0]); this.prevGridByMedia.set(media, prev); } else { @@ -258,7 +228,7 @@ export class TileStoreBuilder { (nowVisible ? this.visibleGridEntries : this.invisibleGridEntries - ).push(new GridTileData(media, showNameTag, edgeToEdge)); + ).push(new GridTileData(media)); } this.numGridEntries++; @@ -274,12 +244,10 @@ export class TileStoreBuilder { (this.numGridEntries < this.visibleTiles ? this.visibleGridEntries : this.invisibleGridEntries - ).push(new GridTileData(media, showNameTag, edgeToEdge)); + ).push(new GridTileData(media)); } else { // Reuse the existing tile const [entry, prevIndex] = prev; - entry.showNameTag = showNameTag; - entry.edgeToEdge = edgeToEdge; const previouslyVisible = prevIndex < this.visibleTiles; const nowVisible = this.numGridEntries < this.visibleTiles; // If it doesn't need to move between the visible/invisible sections of @@ -302,7 +270,7 @@ export class TileStoreBuilder { * method will more eagerly try to reuse an existing tile, replacing its * media, than registerGridTile would. */ - public registerPipTile(media: UserMediaViewModel, showNameTag = true): void { + public registerPipTile(media: UserMediaViewModel): void { if (DEBUG_ENABLED) logger.debug( `[TileStore, ${this.generation}] register PiP tile: ${media.displayName$.value}`, @@ -314,12 +282,10 @@ export class TileStoreBuilder { this.stationaryGridEntries[0] = entry; // Do the media swap entry.media = media; - entry.showNameTag = showNameTag; - entry.edgeToEdge = false; this.prevGridByMedia.delete(entry.media); this.prevGridByMedia.set(media, [entry, 0]); } else { - this.visibleGridEntries.push(new GridTileData(media, showNameTag, false)); + this.visibleGridEntries.push(new GridTileData(media)); } this.numGridEntries++; diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index 39b0f5a7..eeec0c88 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -22,8 +22,6 @@ export class GridTileViewModel { public readonly media$: Behavior< UserMediaViewModel | RingingMediaViewModel >, - public readonly showNameTag$: Behavior, - public readonly edgeToEdge$: Behavior, ) {} } diff --git a/src/state/layout-types.ts b/src/state/layout-types.ts index 90130865..04905efd 100644 --- a/src/state/layout-types.ts +++ b/src/state/layout-types.ts @@ -123,7 +123,7 @@ export interface OneOnOneLandscapeLayout { export interface OneOnOnePortraitLayout { type: "one-on-one-portrait"; foreground: "fixed"; - spotlight: GridTileViewModel; + spotlight: SpotlightTileViewModel; pip?: GridTileViewModel; pipSize$: Behavior<"sm" | "lg">; pipAlignment$: BehaviorSubject; diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index be4c7828..88754b9d 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -62,7 +62,7 @@ interface TileProps { targetHeight: number; displayName: string; mxcAvatarUrl: string | undefined; - showNameTag: boolean; + showNameTags: boolean; focusable: boolean; } @@ -399,6 +399,7 @@ interface GridTileProps { className?: string; style?: ComponentProps["style"]; showSpeakingIndicators: boolean; + showNameTags: boolean; focusable: boolean; } @@ -407,7 +408,6 @@ export const GridTile: FC = ({ vm, showSpeakingIndicators, onOpenProfile, - className, ...props }) => { const ourRef = useRef(null); @@ -415,20 +415,14 @@ export const GridTile: FC = ({ const media = useBehavior(vm.media$); const displayName = useBehavior(media.displayName$); const mxcAvatarUrl = useBehavior(media.mxcAvatarUrl$); - const showNameTag = useBehavior(vm.showNameTag$); - const edgeToEdge = useBehavior(vm.edgeToEdge$); - - const classes = classNames(className, { [styles.edgeToEdge]: edgeToEdge }); if (media.type === "ringing") { return ( ); @@ -436,13 +430,11 @@ export const GridTile: FC = ({ return ( ); @@ -450,12 +442,10 @@ export const GridTile: FC = ({ return ( ); diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index 508c027d..6ff97f7a 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -44,11 +44,7 @@ interface Props extends ComponentProps { videoEnabled: boolean; unencryptedWarning: boolean; status?: { text: string; Icon: ComponentType> }; - /** - * Whether to show the participant's name tag. - * @default true - */ - showNameTag?: boolean; + showNameTags: boolean; nameTagLeadingIcon?: ReactNode; displayName: string; mxcAvatarUrl: string | undefined; @@ -77,7 +73,7 @@ export const MediaView: FC = ({ userId, videoEnabled, unencryptedWarning, - showNameTag = true, + showNameTags, nameTagLeadingIcon, displayName, mxcAvatarUrl, @@ -207,7 +203,7 @@ export const MediaView: FC = ({ )*/} - {showNameTag ? ( + {showNameTags && targetWidth >= 100 ? (
{nameTagLeadingIcon} ; /** @@ -258,6 +260,7 @@ const SpotlightItem: FC = ({ vm, targetWidth, targetHeight, + showNameTags, focusable, intersectionObserver$, snap, @@ -293,6 +296,7 @@ const SpotlightItem: FC = ({ userId: vm.userId, displayName, mxcAvatarUrl, + showNameTags, focusable, "aria-hidden": ariaHidden, }; @@ -381,6 +385,7 @@ interface Props { targetWidth: number; targetHeight: number; showIndicators: boolean; + showNameTags: boolean; focusable: boolean; className?: string; style?: ComponentProps["style"]; @@ -394,6 +399,7 @@ export const SpotlightTile: FC = ({ targetWidth, targetHeight, showIndicators, + showNameTags, focusable = true, className, style, @@ -504,6 +510,7 @@ export const SpotlightTile: FC = ({ vm={vm} targetWidth={targetWidth} targetHeight={targetHeight} + showNameTags={showNameTags} focusable={focusable} intersectionObserver$={intersectionObserver$} // This is how we get the container to scroll to the right media