Create a proper one-on-one call layout for portrait screens

This commit is contained in:
Robin
2026-04-23 17:03:35 +02:00
parent b9f73e3e9a
commit b562a0f721
28 changed files with 911 additions and 403 deletions

View File

@@ -72,6 +72,10 @@ borders don't support gradients */
}
}
.tile.edgeToEdge {
--media-view-border-radius: 0;
}
.muteIcon[data-muted="true"] {
color: var(--cpd-color-icon-secondary);
}

View File

@@ -77,6 +77,7 @@ test("GridTile is accessible", async () => {
targetWidth={300}
targetHeight={200}
showSpeakingIndicators
showNameTags
focusable
/>
</ReactionsSenderProvider>,
@@ -109,6 +110,7 @@ test("GridTile displays ringing media", async () => {
targetWidth={300}
targetHeight={200}
showSpeakingIndicators
showNameTags
focusable
/>
</ReactionsSenderProvider>,

View File

@@ -62,6 +62,7 @@ interface TileProps {
targetHeight: number;
displayName: string;
mxcAvatarUrl: string | undefined;
showNameTags: boolean;
focusable: boolean;
}
@@ -398,6 +399,7 @@ interface GridTileProps {
className?: string;
style?: ComponentProps<typeof animated.div>["style"];
showSpeakingIndicators: boolean;
showNameTags: boolean;
focusable: boolean;
}
@@ -419,9 +421,9 @@ export const GridTile: FC<GridTileProps> = ({
<RingingMediaTile
ref={ref}
vm={media}
{...props}
displayName={displayName}
mxcAvatarUrl={mxcAvatarUrl}
{...props}
/>
);
} else if (media.local) {

View File

@@ -42,6 +42,7 @@ describe("MediaView", () => {
targetHeight: 200,
mirror: false,
unencryptedWarning: false,
showNameTags: true,
video: trackReference,
userId: "@alice:example.com",
mxcAvatarUrl: undefined,
@@ -107,6 +108,16 @@ describe("MediaView", () => {
expect(screen.getByRole("img", { name: "Not encrypted" })).toBeTruthy();
});
test("is shown and accessible even with name tag hidden", async () => {
const { container } = render(
<TooltipProvider>
<MediaView {...baseProps} unencryptedWarning showNameTags={false} />
</TooltipProvider>,
);
expect(await axe(container)).toHaveNoViolations();
screen.getByRole("img", { name: "Not encrypted" });
});
test("is not shown", () => {
render(
<TooltipProvider>

View File

@@ -44,6 +44,7 @@ interface Props extends ComponentProps<typeof animated.div> {
videoEnabled: boolean;
unencryptedWarning: boolean;
status?: { text: string; Icon: ComponentType<SVGAttributes<SVGElement>> };
showNameTags: boolean;
nameTagLeadingIcon?: ReactNode;
displayName: string;
mxcAvatarUrl: string | undefined;
@@ -72,6 +73,7 @@ export const MediaView: FC<Props> = ({
userId,
videoEnabled,
unencryptedWarning,
showNameTags,
nameTagLeadingIcon,
displayName,
mxcAvatarUrl,
@@ -94,6 +96,23 @@ export const MediaView: FC<Props> = ({
const avatarSize = Math.round(Math.min(targetWidth, targetHeight) / 2);
const warnings = unencryptedWarning && (
<Tooltip
label={t("common.unencrypted")}
placement="bottom"
isTriggerInteractive={false}
nonInteractiveTriggerTabIndex={focusable ? undefined : -1}
>
<ErrorSolidIcon
width={20}
height={20}
className={styles.errorIcon}
role="img"
aria-label={t("common.unencrypted")}
/>
</Tooltip>
);
return (
<animated.div
className={classNames(styles.media, className, {
@@ -184,34 +203,23 @@ export const MediaView: FC<Props> = ({
</Text>
</div>
)*/}
<div className={styles.nameTag}>
{nameTagLeadingIcon}
<Text
as="span"
size="sm"
weight="medium"
className={styles.name}
data-testid="name_tag"
>
{displayName}
</Text>
{unencryptedWarning && (
<Tooltip
label={t("common.unencrypted")}
placement="bottom"
isTriggerInteractive={false}
nonInteractiveTriggerTabIndex={focusable ? undefined : -1}
{showNameTags && targetWidth >= 100 ? (
<div className={styles.nameTag}>
{nameTagLeadingIcon}
<Text
as="span"
size="sm"
weight="medium"
className={styles.name}
data-testid="name_tag"
>
<ErrorSolidIcon
width={20}
height={20}
className={styles.errorIcon}
role="img"
aria-label={t("common.unencrypted")}
/>
</Tooltip>
)}
</div>
{displayName}
</Text>
{warnings}
</div>
) : (
warnings
)}
{primaryButton}
</div>
</animated.div>

View File

@@ -65,6 +65,7 @@ test("SpotlightTile is accessible", async () => {
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
showNameTags
focusable={true}
/>,
);
@@ -106,6 +107,7 @@ test("Screen share volume UI is shown when screen share has audio", async () =>
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
showNameTags
focusable
/>
</TooltipProvider>,
@@ -135,6 +137,7 @@ test("Screen share volume UI is hidden when screen share has no audio", async ()
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
showNameTags
focusable
/>,
);
@@ -171,6 +174,7 @@ test("SpotlightTile displays ringing media", async () => {
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
showNameTags
focusable={true}
/>,
);

View File

@@ -66,6 +66,7 @@ interface SpotlightItemBaseProps {
userId: string;
displayName: string;
mxcAvatarUrl: string | undefined;
showNameTags: boolean;
focusable: boolean;
"aria-hidden"?: boolean;
}
@@ -244,6 +245,7 @@ interface SpotlightItemProps {
* The height this tile will have once its animations have settled.
*/
targetHeight: number;
showNameTags: boolean;
focusable: boolean;
intersectionObserver$: Observable<IntersectionObserver>;
/**
@@ -258,6 +260,7 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
vm,
targetWidth,
targetHeight,
showNameTags,
focusable,
intersectionObserver$,
snap,
@@ -293,6 +296,7 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
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<typeof animated.div>["style"];
@@ -394,6 +399,7 @@ export const SpotlightTile: FC<Props> = ({
targetWidth,
targetHeight,
showIndicators,
showNameTags,
focusable = true,
className,
style,
@@ -504,6 +510,7 @@ export const SpotlightTile: FC<Props> = ({
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