mirror of
https://github.com/vector-im/element-call.git
synced 2026-05-01 09:54:37 +00:00
Merge pull request #3885 from element-hq/toger5/bottom-bar-storybook
[Corrected merge target] Footer component -> Storybook
This commit is contained in:
@@ -49,7 +49,6 @@ import { LazyEventEmitter } from "../LazyEventEmitter";
|
||||
import { MatrixRTCTransportMissingError } from "../utils/errors";
|
||||
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
|
||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||
import { HeaderStyle } from "../UrlParams";
|
||||
import { constant } from "../state/Behavior";
|
||||
import { type MuteStates } from "../state/MuteStates.ts";
|
||||
|
||||
@@ -173,7 +172,6 @@ function createGroupCallView(
|
||||
confineToRoom={false}
|
||||
preload={false}
|
||||
skipLobby={false}
|
||||
header={HeaderStyle.Standard}
|
||||
rtcSession={rtcSession.asMockedSession()}
|
||||
muteStates={muteState}
|
||||
widget={widget}
|
||||
|
||||
@@ -93,7 +93,6 @@ interface Props {
|
||||
confineToRoom: boolean;
|
||||
preload: UrlParams["preload"];
|
||||
skipLobby: UrlParams["skipLobby"];
|
||||
header: HeaderStyle;
|
||||
rtcSession: MatrixRTCSession;
|
||||
joined: boolean;
|
||||
setJoined: (value: boolean) => void;
|
||||
@@ -107,7 +106,6 @@ export const GroupCallView: FC<Props> = ({
|
||||
confineToRoom,
|
||||
preload,
|
||||
skipLobby,
|
||||
header,
|
||||
rtcSession,
|
||||
joined,
|
||||
setJoined,
|
||||
@@ -182,6 +180,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
perParticipantE2EE,
|
||||
returnToLobby,
|
||||
password: passwordFromUrl,
|
||||
header,
|
||||
} = useUrlParams();
|
||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||
|
||||
@@ -437,7 +436,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
muteStates={muteStates}
|
||||
onEnter={() => setJoined(true)}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={header === HeaderStyle.None}
|
||||
hideHeader={header !== HeaderStyle.Standard}
|
||||
participantCount={participantCount}
|
||||
onShareClick={onShareClick}
|
||||
/>
|
||||
@@ -463,7 +462,6 @@ export const GroupCallView: FC<Props> = ({
|
||||
rtcSession={rtcSession as MatrixRTCSession}
|
||||
matrixRoom={room}
|
||||
onLeft={onLeft}
|
||||
header={header}
|
||||
muteStates={muteStates}
|
||||
e2eeSystem={e2eeSystem}
|
||||
//otelGroupCallMembership={otelGroupCallMembership}
|
||||
|
||||
@@ -31,144 +31,12 @@ Please see LICENSE in the repository root for full details.
|
||||
background: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
inset-block-end: 0;
|
||||
z-index: var(--call-view-header-footer-layer);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
grid-template-areas: ". buttons layout";
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-3x);
|
||||
padding: var(--cpd-space-10x) var(--cpd-space-6x);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
var(--cpd-color-bg-canvas-default) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.footer.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer.overlay {
|
||||
/* Note that the footer is still position: sticky in this case so that certain
|
||||
tiles can move up out of the way of the footer when visible. */
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.footer.overlay.hidden {
|
||||
display: grid;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
/* Switch to position: absolute so the footer takes up no space in the layout
|
||||
when hidden. */
|
||||
position: absolute;
|
||||
inset-block-end: 0;
|
||||
inset-inline: 0;
|
||||
}
|
||||
|
||||
.footer.overlay:has(:focus-visible) {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.settingsLogoContainer {
|
||||
display: flex;
|
||||
gap: var(--cpd-space-4x);
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
justify-self: start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-2x);
|
||||
padding-inline-start: var(--cpd-space-1x);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
grid-area: buttons;
|
||||
justify-self: center;
|
||||
display: flex;
|
||||
gap: var(--cpd-space-3x);
|
||||
}
|
||||
|
||||
.layout {
|
||||
grid-area: layout;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
/*First hide the logo*/
|
||||
@media (max-width: 750px) {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
With the logo hidden >500px is enough space to show overflow, buttons, layout.
|
||||
Once we exceed 500 we hide everything except the buttons.
|
||||
*/
|
||||
@media (max-width: 500px) {
|
||||
.footer {
|
||||
grid-template-areas: "buttons buttons buttons";
|
||||
}
|
||||
|
||||
/*.settingsLogoContainer {
|
||||
display: none;
|
||||
}*/
|
||||
|
||||
.layout {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 800px) {
|
||||
.footer {
|
||||
padding-block: var(--cpd-space-8x);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 400px) {
|
||||
.footer {
|
||||
padding-block: var(--cpd-space-4x);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 370px) {
|
||||
.shareScreen {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* PIP custom css */
|
||||
@media (max-height: 400px) {
|
||||
.shareScreen {
|
||||
display: flex;
|
||||
}
|
||||
.footer {
|
||||
padding-block-start: var(--cpd-space-3x);
|
||||
padding-block-end: var(--cpd-space-2x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.invite,
|
||||
.raiseHand {
|
||||
.invite {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.buttons {
|
||||
gap: var(--cpd-space-4x);
|
||||
}
|
||||
}
|
||||
|
||||
.fixedGrid {
|
||||
position: absolute;
|
||||
inline-size: 100%;
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { render, type RenderResult } from "@testing-library/react";
|
||||
import { type LocalParticipant } from "livekit-client";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { BrowserRouter, MemoryRouter } from "react-router-dom";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
@@ -34,13 +34,15 @@ import {
|
||||
} from "../utils/test";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
import { type CallViewModelOptions } from "../state/CallViewModel/CallViewModel";
|
||||
import { alice, local } from "../utils/test-fixtures";
|
||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||
import { HeaderStyle } from "../UrlParams";
|
||||
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
||||
import { constant } from "../state/Behavior";
|
||||
import { AppBar } from "../AppBar";
|
||||
import { initializeWidget } from "../widget";
|
||||
|
||||
initializeWidget();
|
||||
@@ -97,6 +99,11 @@ beforeEach(() => {
|
||||
});
|
||||
interface CreateInCallViewArgs {
|
||||
mediaDevices?: ECMediaDevices;
|
||||
callViewModelOptions?: Partial<CallViewModelOptions>;
|
||||
/** If set, uses a MemoryRouter with this as the initial entry instead of BrowserRouter */
|
||||
initialRoute?: string;
|
||||
/** If true, wraps the rendered tree in an AppBar provider */
|
||||
withAppBar?: boolean;
|
||||
}
|
||||
function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
|
||||
rtcSession: MockRTCSession;
|
||||
@@ -115,47 +122,59 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
|
||||
[local, alice],
|
||||
undefined,
|
||||
mediaDevices,
|
||||
{},
|
||||
args.callViewModelOptions,
|
||||
);
|
||||
|
||||
rtcSession.joined = true;
|
||||
const room = rtcSession.room;
|
||||
const client = room.client;
|
||||
|
||||
const Router = args.initialRoute
|
||||
? ({ children }: { children: React.ReactNode }): React.ReactNode => (
|
||||
<MemoryRouter initialEntries={[args.initialRoute!]}>
|
||||
{children}
|
||||
</MemoryRouter>
|
||||
)
|
||||
: BrowserRouter;
|
||||
|
||||
const inCallView = (
|
||||
<InCallView
|
||||
client={client}
|
||||
rtcSession={rtcSession.asMockedSession()}
|
||||
muteStates={muteState}
|
||||
vm={vm}
|
||||
matrixInfo={{
|
||||
userId: "",
|
||||
displayName: "",
|
||||
avatarUrl: "",
|
||||
roomId: "",
|
||||
roomName: "",
|
||||
roomAlias: null,
|
||||
roomAvatar: null,
|
||||
e2eeSystem: {
|
||||
kind: E2eeType.NONE,
|
||||
},
|
||||
}}
|
||||
matrixRoom={room}
|
||||
onShareClick={null}
|
||||
/>
|
||||
);
|
||||
|
||||
const content = args.withAppBar ? <AppBar>{inCallView}</AppBar> : inCallView;
|
||||
|
||||
const renderResult = render(
|
||||
<BrowserRouter>
|
||||
<Router>
|
||||
<MediaDevicesContext value={mediaDevices}>
|
||||
<ReactionsSenderProvider
|
||||
vm={vm}
|
||||
rtcSession={rtcSession.asMockedSession()}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<RoomContext value={livekitRoom}>
|
||||
<InCallView
|
||||
client={client}
|
||||
header={HeaderStyle.Standard}
|
||||
rtcSession={rtcSession.asMockedSession()}
|
||||
muteStates={muteState}
|
||||
vm={vm}
|
||||
matrixInfo={{
|
||||
userId: "",
|
||||
displayName: "",
|
||||
avatarUrl: "",
|
||||
roomId: "",
|
||||
roomName: "",
|
||||
roomAlias: null,
|
||||
roomAvatar: null,
|
||||
e2eeSystem: {
|
||||
kind: E2eeType.NONE,
|
||||
},
|
||||
}}
|
||||
matrixRoom={room}
|
||||
onShareClick={null}
|
||||
/>
|
||||
</RoomContext>
|
||||
<RoomContext value={livekitRoom}>{content}</RoomContext>
|
||||
</TooltipProvider>
|
||||
</ReactionsSenderProvider>
|
||||
</MediaDevicesContext>
|
||||
</BrowserRouter>,
|
||||
</Router>,
|
||||
);
|
||||
return {
|
||||
...renderResult,
|
||||
@@ -170,6 +189,57 @@ describe("InCallView", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe("settings button with AppBar header", () => {
|
||||
it("mobile landscape, is accessible when showHeader is false", () => {
|
||||
// windowSize with height <= 600 results in "flat" windowMode,
|
||||
// which means showHeader$ emits false.
|
||||
const { getAllByRole } = createInCallView({
|
||||
initialRoute: "/?header=app_bar",
|
||||
withAppBar: true,
|
||||
callViewModelOptions: {
|
||||
// Set windowMode$ to "flat" (height <= 600)
|
||||
windowSize$: constant({ width: 1000, height: 500 }),
|
||||
},
|
||||
});
|
||||
// When showHeader is false, hideSettingsButton is false,
|
||||
// so the settings button is visible in the footer.
|
||||
const settingsBtn = getAllByRole("button", { name: "Settings" });
|
||||
// here we check for two settings buttons because there are two buttons in the bottom bar. One for the
|
||||
// the narrow layout and another one for the wide layout.
|
||||
// Their visibility uses @media css queries, which cannot be tested in JSDOM,
|
||||
// but we can at least check that both buttons are rendered and have the correct classes.
|
||||
expect(settingsBtn.length).toBe(2);
|
||||
expect(settingsBtn[0]).toHaveAttribute(
|
||||
"data-testid",
|
||||
"settings-bottom-left",
|
||||
);
|
||||
expect(settingsBtn[0]).toBeVisible();
|
||||
});
|
||||
|
||||
it("mobile portrait, is accessible when showHeader is true", () => {
|
||||
// windowSize with height > 600 and width > 600 results in "normal" windowMode,
|
||||
// which means showHeader$ emits true.
|
||||
const { getAllByRole } = createInCallView({
|
||||
initialRoute: "/?header=app_bar",
|
||||
withAppBar: true,
|
||||
callViewModelOptions: {
|
||||
// Set windowMode$ to "normal" (height >= 600)
|
||||
windowSize$: constant({ width: 1000, height: 800 }),
|
||||
},
|
||||
});
|
||||
// When showHeader is true and headerStyle is AppBar,
|
||||
// hideSettingsButton is true in the footer, but the settings
|
||||
// button is rendered in the AppBar via useAppBarSecondaryButton.
|
||||
const settingsBtns = getAllByRole("button", { name: "Settings" });
|
||||
|
||||
expect(settingsBtns.length).toBe(1);
|
||||
expect(settingsBtns[0]).toHaveAttribute(
|
||||
"data-testid",
|
||||
"settings-app-bar",
|
||||
);
|
||||
expect(settingsBtns[0]).toBeVisible();
|
||||
});
|
||||
});
|
||||
describe("audioOutputSwitcher", () => {
|
||||
it("is visible and can be clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
@@ -183,9 +253,10 @@ describe("InCallView", () => {
|
||||
["earpiece-id", { type: "earpiece" }],
|
||||
]),
|
||||
);
|
||||
const selected$ = new BehaviorSubject<
|
||||
{ id: string; virtualEarpiece: boolean } | undefined
|
||||
>({ id: "speaker-id", virtualEarpiece: false });
|
||||
const selected$ = new BehaviorSubject({
|
||||
id: "speaker-id",
|
||||
virtualEarpiece: false,
|
||||
});
|
||||
|
||||
const mediaDevices = mockMediaDevices({
|
||||
audioOutput: {
|
||||
@@ -197,8 +268,7 @@ describe("InCallView", () => {
|
||||
|
||||
const { getByRole } = createInCallView({ mediaDevices });
|
||||
// The button should be visible. When current output is "speaker",
|
||||
// the switcher targets "earpiece", so the tooltip label is "Handset".
|
||||
const audioOutputBtn = getByRole("button", { name: "Handset" });
|
||||
const audioOutputBtn = getByRole("button", { name: "Loudspeaker" });
|
||||
expect(audioOutputBtn).toBeVisible();
|
||||
|
||||
await user.click(audioOutputBtn);
|
||||
|
||||
@@ -5,7 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import { type MatrixClient, type Room as MatrixRoom } from "matrix-js-sdk";
|
||||
import {
|
||||
type FC,
|
||||
@@ -25,22 +24,8 @@ import classNames from "classnames";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
import { useObservable } from "observable-hooks";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import {
|
||||
VoiceCallSolidIcon,
|
||||
VolumeOnSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import LogoMark from "../icons/LogoMark.svg?react";
|
||||
import LogoType from "../icons/LogoType.svg?react";
|
||||
import {
|
||||
EndCallButton,
|
||||
MicButton,
|
||||
VideoButton,
|
||||
ShareScreenButton,
|
||||
ReactionToggleButton,
|
||||
SettingsIconButton,
|
||||
} from "../button";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import { HeaderStyle, useUrlParams } from "../UrlParams";
|
||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||
@@ -55,7 +40,6 @@ import { useMergedRefs } from "../useMergedRefs";
|
||||
import { type MuteStates } from "../state/MuteStates";
|
||||
import { type MatrixInfo } from "./VideoPreview";
|
||||
import { InviteButton } from "../button/InviteButton";
|
||||
import { LayoutToggle } from "./LayoutToggle";
|
||||
import {
|
||||
type CallViewModel,
|
||||
createCallViewModel$,
|
||||
@@ -106,6 +90,8 @@ import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.t
|
||||
import { type Layout } from "../state/layout-types.ts";
|
||||
import { ObservableScope } from "../state/ObservableScope.ts";
|
||||
import { useLatest } from "../useLatest.ts";
|
||||
import { CallFooter } from "../components/CallFooter.tsx";
|
||||
import { SettingsIconButton } from "../button/Button.tsx";
|
||||
|
||||
const logger = rootLogger.getChild("[InCallView]");
|
||||
|
||||
@@ -185,7 +171,6 @@ export interface InCallViewProps {
|
||||
rtcSession: MatrixRTCSession;
|
||||
matrixRoom: MatrixRoom;
|
||||
muteStates: MuteStates;
|
||||
header: HeaderStyle;
|
||||
onShareClick: (() => void) | null;
|
||||
}
|
||||
|
||||
@@ -195,8 +180,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
matrixInfo,
|
||||
matrixRoom,
|
||||
muteStates,
|
||||
|
||||
header: headerStyle,
|
||||
onShareClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -220,7 +203,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
// Merge the refs so they can attach to the same element
|
||||
const containerRef = useMergedRefs(containerRef1, containerRef2);
|
||||
|
||||
const { showControls } = useUrlParams();
|
||||
const { showControls, header: headerStyle } = useUrlParams();
|
||||
|
||||
const muteAllAudio = useBehavior(muteAllAudio$);
|
||||
|
||||
@@ -378,7 +361,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
let header: ReactNode = null;
|
||||
if (showHeader) {
|
||||
switch (headerStyle) {
|
||||
case "none":
|
||||
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 = (
|
||||
@@ -388,7 +375,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "standard":
|
||||
case HeaderStyle.Standard:
|
||||
header = (
|
||||
<Header
|
||||
className={styles.header}
|
||||
@@ -575,138 +562,46 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
matrixRoom.roomId,
|
||||
);
|
||||
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
const buttonSize = layout.type === "pip" ? "sm" : "lg";
|
||||
buttons.push(
|
||||
<MicButton
|
||||
size={buttonSize}
|
||||
key="audio"
|
||||
enabled={audioEnabled}
|
||||
onClick={toggleAudio ?? undefined}
|
||||
disabled={toggleAudio === null}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
<VideoButton
|
||||
size={buttonSize}
|
||||
key="video"
|
||||
enabled={videoEnabled}
|
||||
onClick={toggleVideo ?? undefined}
|
||||
disabled={toggleVideo === null}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
if (vm.toggleScreenSharing !== null) {
|
||||
buttons.push(
|
||||
<ShareScreenButton
|
||||
size={buttonSize}
|
||||
key="share_screen"
|
||||
className={styles.shareScreen}
|
||||
enabled={sharingScreen}
|
||||
onClick={vm.toggleScreenSharing}
|
||||
data-testid="incall_screenshare"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
if (supportsReactions) {
|
||||
buttons.push(
|
||||
<ReactionToggleButton
|
||||
size={buttonSize}
|
||||
vm={vm}
|
||||
key="raise_hand"
|
||||
className={styles.raiseHand}
|
||||
identifier={`${client.getUserId()}:${client.getDeviceId()}`}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
// In this PR we just move the button ot the bottom bar. We do not yet update its apperance
|
||||
const audioOutputButton = useMemo(() => {
|
||||
if (audioOutputSwitcher === null) return null;
|
||||
const isEarpieceTarget = audioOutputSwitcher.targetOutput === "earpiece";
|
||||
const Icon = isEarpieceTarget ? VoiceCallSolidIcon : VolumeOnSolidIcon;
|
||||
const label = isEarpieceTarget
|
||||
? t("settings.devices.handset")
|
||||
: t("settings.devices.loudspeaker");
|
||||
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<IconButton
|
||||
key="audio_output_switcher"
|
||||
onClick={(e) => {
|
||||
audioOutputSwitcher.switch();
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}, [t, audioOutputSwitcher]);
|
||||
if (audioOutputButton) buttons.push(audioOutputButton);
|
||||
|
||||
const settingsButtonInAppBar =
|
||||
headerStyle === HeaderStyle.AppBar && showHeader;
|
||||
useAppBarSecondaryButton(
|
||||
<SettingsIconButton key="settings" onClick={openSettings} />,
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
<EndCallButton
|
||||
size={buttonSize}
|
||||
key="end_call"
|
||||
onClick={function (): void {
|
||||
vm.hangup();
|
||||
}}
|
||||
data-testid="incall_leave"
|
||||
<SettingsIconButton
|
||||
key="settings"
|
||||
onClick={openSettings}
|
||||
data-testid="settings-app-bar"
|
||||
/>,
|
||||
);
|
||||
|
||||
const logo = (
|
||||
<div className={styles.logo}>
|
||||
<LogoMark width={24} height={24} aria-hidden />
|
||||
<LogoType
|
||||
width={80}
|
||||
height={11}
|
||||
aria-label={import.meta.env.VITE_PRODUCT_NAME || "Element Call"}
|
||||
/>
|
||||
{/* Don't mind this odd placement, it's just a little debug label */}
|
||||
{debugTileLayout ? `Tiles generation: ${tileStoreGeneration}` : undefined}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Only hide the settings button if we have an AppBar header and we are showing the header
|
||||
const footer = (
|
||||
<div
|
||||
<CallFooter
|
||||
ref={footerRef}
|
||||
className={classNames(styles.footer, {
|
||||
[styles.overlay]: windowMode === "flat",
|
||||
[styles.hidden]:
|
||||
!showFooter || (!showControls && headerStyle === "none"),
|
||||
})}
|
||||
>
|
||||
<div className={styles.settingsLogoContainer}>
|
||||
{showControls &&
|
||||
// Settings button is also shown in the app bar if present
|
||||
headerStyle !== HeaderStyle.AppBar &&
|
||||
layout.type !== "pip" && (
|
||||
<SettingsIconButton
|
||||
kind="secondary"
|
||||
key="settings"
|
||||
onClick={openSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{headerStyle !== "none" && logo}
|
||||
</div>
|
||||
|
||||
{showControls && <div className={styles.buttons}>{buttons}</div>}
|
||||
{showControls && (
|
||||
<LayoutToggle
|
||||
className={styles.layout}
|
||||
layout={gridMode}
|
||||
setLayout={setGridMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
hidden={!showFooter}
|
||||
hideControls={!showControls}
|
||||
asOverlay={windowMode === "flat"}
|
||||
asPip={layout.type === "pip"}
|
||||
// Hide the logo for both embedded solutions. mobile: HeaderStyle.AppBar and desktop: HeaderStyle.None.
|
||||
hideLogo={headerStyle !== HeaderStyle.Standard}
|
||||
layoutMode={gridMode}
|
||||
setLayoutMode={setGridMode}
|
||||
audioEnabled={audioEnabled}
|
||||
toggleAudio={toggleAudio ?? undefined}
|
||||
videoEnabled={videoEnabled}
|
||||
toggleVideo={toggleVideo ?? undefined}
|
||||
sharingScreen={sharingScreen}
|
||||
toggleScreenSharing={vm.toggleScreenSharing ?? undefined}
|
||||
reactionIdentifier={`${client.getUserId()}:${client.getDeviceId()}`}
|
||||
reactionData={supportsReactions ? vm : undefined}
|
||||
audioOutputSwitcher={audioOutputSwitcher ?? undefined}
|
||||
// Only pass the openSettings function if the settings button is not in the app bar.
|
||||
// If there is no fn the button will be hidden in the footer.
|
||||
openSettings={settingsButtonInAppBar ? undefined : openSettings}
|
||||
hangup={vm.hangup}
|
||||
//Debug props
|
||||
debugTileLayout={debugTileLayout}
|
||||
tileStoreGeneration={tileStoreGeneration}
|
||||
/>
|
||||
);
|
||||
|
||||
const allConnections = useBehavior(vm.allConnections$);
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,7 +33,7 @@ export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.toggle, className)}>
|
||||
<form className={classNames(styles.toggle, className)}>
|
||||
<Tooltip label={t("layout_spotlight_label")}>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -54,6 +54,6 @@ export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
<GridIcon aria-hidden width={24} height={24} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,12 +33,6 @@ import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import { type MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||
import { type MuteStates } from "../state/MuteStates";
|
||||
import { InviteButton } from "../button/InviteButton";
|
||||
import {
|
||||
EndCallButton,
|
||||
MicButton,
|
||||
SettingsIconButton,
|
||||
VideoButton,
|
||||
} from "../button/Button";
|
||||
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
||||
import { useMediaQuery } from "../useMediaQuery";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
@@ -52,6 +46,7 @@ import {
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { getValue } from "../utils/observable";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
import { CallFooter } from "../components/CallFooter";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@@ -226,23 +221,18 @@ export const LobbyView: FC<Props> = ({
|
||||
</VideoPreview>
|
||||
{!recentsButtonInFooter && recentsButton}
|
||||
</div>
|
||||
<div className={inCallStyles.footer}>
|
||||
<CallFooter
|
||||
audioEnabled={audioEnabled}
|
||||
videoEnabled={videoEnabled}
|
||||
toggleAudio={toggleAudio ?? undefined}
|
||||
toggleVideo={toggleVideo ?? undefined}
|
||||
openSettings={openSettings}
|
||||
hangup={!confineToRoom ? onLeaveClick : undefined}
|
||||
// Logo and header are connected. We will only show the logo in SPA with header.
|
||||
hideLogo={hideHeader}
|
||||
>
|
||||
{recentsButtonInFooter && recentsButton}
|
||||
<SettingsIconButton kind="secondary" onClick={openSettings} />
|
||||
<div className={inCallStyles.buttons}>
|
||||
<MicButton
|
||||
enabled={audioEnabled}
|
||||
onClick={toggleAudio ?? undefined}
|
||||
disabled={toggleAudio === null}
|
||||
/>
|
||||
<VideoButton
|
||||
enabled={videoEnabled}
|
||||
onClick={toggleVideo ?? undefined}
|
||||
disabled={toggleVideo === null}
|
||||
/>
|
||||
{!confineToRoom && <EndCallButton onClick={onLeaveClick} />}
|
||||
</div>
|
||||
</div>
|
||||
</CallFooter>
|
||||
</div>
|
||||
{client && (
|
||||
<SettingsModal
|
||||
|
||||
@@ -132,7 +132,6 @@ export const RoomPage: FC = (): ReactNode => {
|
||||
confineToRoom={confineToRoom}
|
||||
preload={preload}
|
||||
skipLobby={skipLobby || wasInWaitForInviteState.current}
|
||||
header={header}
|
||||
muteStates={muteStates}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -170,8 +170,9 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_8_"
|
||||
class="_icon-button_1215g_8"
|
||||
class="_icon-button_1215g_8 settingsOnlyShowWide"
|
||||
data-kind="secondary"
|
||||
data-testid="settings-bottom-left"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
@@ -302,10 +303,32 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
<div
|
||||
class="buttons"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="_r_d_"
|
||||
class="_button_13vu4_8 settingsOnlyShowNarrow _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
data-testid="settings-bottom-center"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-disabled="true"
|
||||
aria-labelledby="_r_d_"
|
||||
aria-labelledby="_r_i_"
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
@@ -329,7 +352,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-disabled="true"
|
||||
aria-labelledby="_r_i_"
|
||||
aria-labelledby="_r_n_"
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
@@ -351,7 +374,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="_r_n_"
|
||||
aria-labelledby="_r_s_"
|
||||
class="_button_13vu4_8 endCall _has-icon_13vu4_60 _icon-only_13vu4_53 _destructive_13vu4_110"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -373,11 +396,11 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
<form
|
||||
class="toggle layout"
|
||||
>
|
||||
<input
|
||||
aria-labelledby="_r_s_"
|
||||
aria-labelledby="_r_11_"
|
||||
name="layout"
|
||||
type="radio"
|
||||
value="spotlight"
|
||||
@@ -395,7 +418,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
aria-labelledby="_r_11_"
|
||||
aria-labelledby="_r_16_"
|
||||
checked=""
|
||||
name="layout"
|
||||
type="radio"
|
||||
@@ -413,7 +436,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
d="M4 11a.97.97 0 0 1-.712-.287A.97.97 0 0 1 3 10V4q0-.424.288-.712A.97.97 0 0 1 4 3h6q.424 0 .713.288Q11 3.575 11 4v6q0 .424-.287.713A.97.97 0 0 1 10 11zm5-2V5H5v4zm5 12a.97.97 0 0 1-.713-.288A.97.97 0 0 1 13 20v-6q0-.424.287-.713A.97.97 0 0 1 14 13h6q.424 0 .712.287.288.288.288.713v6q0 .424-.288.712A.97.97 0 0 1 20 21zm5-2v-4h-4v4zM4 21a.97.97 0 0 1-.712-.288A.97.97 0 0 1 3 20v-6q0-.424.288-.713A.97.97 0 0 1 4 13h6q.424 0 .713.287.287.288.287.713v6q0 .424-.287.712A.97.97 0 0 1 10 21zm5-2v-4H5v4zm5-8a.97.97 0 0 1-.713-.287A.97.97 0 0 1 13 10V4q0-.424.287-.712A.97.97 0 0 1 14 3h6q.424 0 .712.288Q21 3.575 21 4v6q0 .424-.288.713A.97.97 0 0 1 20 11zm5-2V5h-4v4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user