Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession

This commit is contained in:
David Baker
2023-09-12 11:30:46 +01:00
42 changed files with 775 additions and 540 deletions

View File

@@ -0,0 +1,34 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.lock {
padding: var(--cpd-space-1x);
border-radius: var(--cpd-radius-pill-effect);
}
.lock[data-encrypted="true"] {
color: var(--cpd-color-icon-success-primary);
}
.lock[data-encrypted="false"] {
color: var(--cpd-color-icon-secondary);
}
@media (hover: hover) {
.lock:hover {
background: var(--cpd-color-bg-subtle-primary);
}
}

View File

@@ -0,0 +1,46 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FC } from "react";
import { Tooltip } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import { ReactComponent as LockIcon } from "@vector-im/compound-design-tokens/icons/lock.svg";
import { ReactComponent as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg";
import styles from "./EncryptionLock.module.css";
interface Props {
encrypted: boolean;
}
export const EncryptionLock: FC<Props> = ({ encrypted }) => {
const { t } = useTranslation();
const Icon = encrypted ? LockIcon : LockOffIcon;
return (
<Tooltip
label={encrypted ? t("Encrypted") : t("Not encrypted")}
side="right"
>
<Icon
width={16}
height={16}
className={styles.lock}
data-encrypted={encrypted}
/>
</Tooltip>
);
};

View File

@@ -1,82 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import menuStyles from "../Menu.module.css";
import { Menu } from "../Menu";
import { TooltipTrigger } from "../Tooltip";
export type Layout = "freedom" | "spotlight";
interface Props {
layout: Layout;
setLayout: (layout: Layout) => void;
}
export function GridLayoutMenu({ layout, setLayout }: Props) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Change layout"), [t]);
const onAction = useCallback(
(key: React.Key) => {
setLayout(key.toString() as Layout);
},
[setLayout]
);
const onClose = useCallback(() => {}, []);
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={tooltip}>
<Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu
{...props}
label={t("Grid layout menu")}
onAction={onAction}
onClose={onClose}
>
<Item key="freedom" textValue={t("Freedom")}>
<FreedomIcon />
<span>Freedom</span>
{layout === "freedom" && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
<Item key="spotlight" textValue={t("Spotlight")}>
<SpotlightIcon />
<span>Spotlight</span>
{layout === "spotlight" && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
</Menu>
)}
</PopoverMenuTrigger>
);
}

View File

@@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
import { Room } from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { JoinRule, RoomMember } from "matrix-js-sdk/src/matrix";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
@@ -42,6 +43,11 @@ import {
useIsRoomE2EE,
} from "../e2ee/sharedKeyManagement";
import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useModalTriggerState } from "../Modal";
import { useJoinRule } from "./useJoinRule";
import { ShareModal } from "./ShareModal";
declare global {
interface Window {
@@ -82,16 +88,43 @@ export function GroupCallView({
}, [rtcSession]);
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(rtcSession.room);
const roomAvatar = useRoomAvatar(rtcSession.room);
const roomEncrypted = useIsRoomE2EE(rtcSession.room.roomId)!;
const matrixInfo = useMemo((): MatrixInfo => {
return {
userId: client.getUserId()!,
displayName: displayName!,
avatarUrl: avatarUrl!,
roomId: rtcSession.room.roomId,
roomName: rtcSession.room.name,
roomName,
roomAlias: rtcSession.room.getCanonicalAlias(),
roomAvatar,
roomEncrypted,
};
}, [client, displayName, avatarUrl, rtcSession]);
}, [
displayName,
avatarUrl,
rtcSession,
roomName,
roomAvatar,
roomEncrypted,
client,
]);
const participatingMembers = useMemo(() => {
const members: RoomMember[] = [];
// Count each member only once, regardless of how many devices they use
const addedUserIds = new Set<string>();
for (const membership of memberships) {
if (!addedUserIds.has(membership.member.userId)) {
addedUserIds.add(membership.member.userId);
members.push(membership.member);
}
}
return members;
}, [memberships]);
const deviceContext = useMediaDevices();
const latestDevices = useRef<MediaDevices>();
@@ -251,6 +284,17 @@ export function GroupCallView({
enterRTCSession(rtcSession);
}, [rtcSession]);
const joinRule = useJoinRule(rtcSession.room);
const { modalState: shareModalState, modalProps: shareModalProps } =
useModalTriggerState();
const onShareClickFn = useCallback(
() => shareModalState.open(),
[shareModalState]
);
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
return (
<ErrorView
@@ -267,17 +311,27 @@ export function GroupCallView({
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
}
const shareModal = shareModalState.isOpen && (
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
);
if (isJoined) {
return (
<ActiveCall
client={client}
rtcSession={rtcSession}
onLeave={onLeave}
hideHeader={hideHeader}
muteStates={muteStates}
e2eeConfig={e2eeConfig}
//otelGroupCallMembership={otelGroupCallMembership}
/>
<>
{shareModal}
<ActiveCall
client={client}
matrixInfo={matrixInfo}
rtcSession={rtcSession}
participatingMembers={participatingMembers}
onLeave={onLeave}
hideHeader={hideHeader}
muteStates={muteStates}
e2eeConfig={e2eeConfig}
//otelGroupCallMembership={otelGroupCallMembership}
onShareClick={onShareClick}
/>
</>
);
} else if (left) {
// The call ended view is shown for two reasons: prompting guests to create
@@ -316,13 +370,19 @@ export function GroupCallView({
);
} else {
return (
<LobbyView
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => enterRTCSession(rtcSession)}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
/>
<>
{shareModal}
<LobbyView
client={client}
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => enterRTCSession(rtcSession)}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
participatingMembers={participatingMembers}
onShareClick={onShareClick}
/>
</>
);
}
}

View File

@@ -49,30 +49,38 @@ limitations under the License.
left: 0;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-areas: "logo buttons layout";
align-items: center;
gap: 12px;
padding: var(--footerPadding) 0;
/* TODO: Un-hardcode these colors */
gap: var(--cpd-space-3x);
padding: var(--footerPadding) var(--inline-content-inset);
background: linear-gradient(
360deg,
#15191e 0%,
rgba(16, 19, 23, 0.9) 37%,
rgba(16, 19, 23, 0.8) 49.68%,
rgba(16, 19, 23, 0.7) 56.68%,
rgba(16, 19, 23, 0.427397) 72.92%,
rgba(16, 19, 23, 0.257534) 81.06%,
rgba(16, 19, 23, 0.136986) 87.29%,
rgba(16, 19, 23, 0.0658079) 92.4%,
rgba(16, 19, 23, 0) 100%
180deg,
rgba(0, 0, 0, 0) 0%,
var(--cpd-color-bg-canvas-default) 100%
);
}
.maximised .footer {
position: absolute;
width: 100%;
bottom: 0;
.logo {
grid-area: 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;
display: flex;
gap: var(--cpd-space-3x);
}
.layout {
grid-area: layout;
justify-self: end;
}
@media (min-height: 300px) {
@@ -86,7 +94,7 @@ limitations under the License.
--footerPadding: 60px;
}
.footer {
gap: 16px;
.buttons {
gap: var(--cpd-space-4x);
}
}

View File

@@ -23,8 +23,7 @@ import {
useTracks,
} from "@livekit/components-react";
import { usePreventScroll } from "@react-aria/overlays";
import classNames from "classnames";
import { Room, Track, ConnectionState } from "livekit-client";
import { ConnectionState, Room, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
@@ -32,10 +31,11 @@ import { Ref, useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { ReactComponent as LogoMark } from "../icons/LogoMark.svg";
import { ReactComponent as LogoType } from "../icons/LogoType.svg";
import type { IWidgetApiRequest } from "matrix-widget-api";
import {
HangupButton,
@@ -43,7 +43,6 @@ import {
VideoButton,
ScreenshareButton,
SettingsButton,
InviteButton,
} from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import {
@@ -58,26 +57,24 @@ import { useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu";
import styles from "./InCallView.module.css";
import { useJoinRule } from "./useJoinRule";
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout";
import { E2EELock } from "../E2EELock";
import { useWakeLock } from "../useWakeLock";
import { useMergedRefs } from "../useMergedRefs";
import { MuteStates } from "./MuteStates";
import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement";
import { useOpenIDSFU } from "../livekit/openIDSFU";
import { MatrixInfo } from "./VideoPreview";
import { ShareButton } from "../button/ShareButton";
import { LayoutToggle } from "./LayoutToggle";
import { ECConnectionState } from "../livekit/useECConnectionState";
import { useOpenIDSFU } from "../livekit/openIDSFU";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -115,24 +112,30 @@ export function ActiveCall(props: ActiveCallProps) {
export interface InCallViewProps {
client: MatrixClient;
matrixInfo: MatrixInfo;
rtcSession: MatrixRTCSession;
livekitRoom: Room;
muteStates: MuteStates;
participatingMembers: RoomMember[];
onLeave: (error?: Error) => void;
hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership;
connState: ECConnectionState;
onShareClick: (() => void) | null;
}
export function InCallView({
client,
matrixInfo,
rtcSession,
livekitRoom,
muteStates,
participatingMembers,
onLeave,
hideHeader,
otelGroupCallMembership,
connState,
onShareClick,
}: InCallViewProps) {
const { t } = useTranslation();
usePreventScroll();
@@ -146,8 +149,6 @@ export function InCallView({
}
}, [connState, onLeave]);
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
const boundsValid = bounds.height > 0;
@@ -182,8 +183,6 @@ export function InCallView({
[muteStates]
);
const joinRule = useJoinRule(rtcSession.room);
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
useCallViewKeyboardShortcuts(
@@ -199,7 +198,7 @@ export function InCallView({
useEffect(() => {
widget?.api.transport.send(
layout === "freedom"
layout === "grid"
? ElementWidgetActions.TileLayout
: ElementWidgetActions.SpotlightLayout,
{}
@@ -209,7 +208,7 @@ export function InCallView({
useEffect(() => {
if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("freedom");
setLayout("grid");
await widget!.api.transport.reply(ev.detail, {});
};
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
@@ -233,7 +232,8 @@ export function InCallView({
}
}, [setLayout]);
const reducedControls = boundsValid && bounds.width <= 400;
const mobile = boundsValid && bounds.width <= 660;
const reducedControls = boundsValid && bounds.width <= 340;
const noControls = reducedControls && bounds.height <= 400;
const items = useParticipantTiles(livekitRoom, rtcSession.room);
@@ -253,7 +253,7 @@ export function InCallView({
);
const Grid =
items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid;
items.length > 12 && layout === "grid" ? NewVideoGrid : VideoGrid;
const prefersReducedMotion = usePrefersReducedMotion();
@@ -327,25 +327,6 @@ export function InCallView({
settingsModalState.open();
}, [settingsModalState]);
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openInvite = useCallback(() => {
inviteModalState.open();
}, [inviteModalState]);
const containerClasses = classNames(styles.inRoom, {
[styles.maximised]: undefined,
});
const toggleScreensharing = useCallback(async () => {
exitFullscreen();
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
@@ -397,21 +378,43 @@ export function InCallView({
buttons.push(
<HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" />
);
footer = <div className={styles.footer}>{buttons}</div>;
footer = (
<div className={styles.footer}>
{!mobile && !hideHeader && (
<div className={styles.logo}>
<LogoMark width={24} height={24} />
<LogoType width={80} height={11} />
</div>
)}
<div className={styles.buttons}>{buttons}</div>
{!mobile && !hideHeader && (
<LayoutToggle
className={styles.layout}
layout={layout}
setLayout={setLayout}
/>
)}
</div>
);
}
return (
<div className={containerClasses} ref={containerRef}>
<div className={styles.inRoom} ref={containerRef}>
{!hideHeader && maximisedParticipant === null && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={rtcSession.room.name} />
{!isRoomE2EE && <E2EELock />}
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
/>
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
{joinRule === JoinRule.Public && (
<InviteButton variant="icon" onClick={openInvite} />
{!reducedControls && onShareClick !== null && (
<ShareButton onClick={onShareClick} />
)}
</RightNav>
</Header>
@@ -442,9 +445,6 @@ export function InCallView({
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && (
<InviteModal roomId={rtcSession.room.roomId} {...inviteModalProps} />
)}
</div>
);
}

View File

@@ -0,0 +1,77 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.toggle {
padding: 2px;
border: 1px solid var(--cpd-color-border-interactive-secondary);
border-radius: var(--cpd-radius-pill-effect);
box-shadow: 0px 0px 40px 0px rgba(0, 0, 0, 0.5);
display: flex;
}
.toggle input {
appearance: none;
outline: none !important;
}
.toggle label {
display: block;
padding: calc(2.5 * var(--cpd-space-1x));
cursor: pointer;
border-radius: var(--cpd-radius-pill-effect);
color: var(--cpd-color-icon-primary);
background: var(--cpd-color-bg-action-secondary-rest);
box-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
}
@media (hover: hover) {
.toggle label:hover {
background: var(--cpd-color-bg-action-secondary-hovered);
box-shadow: none;
}
}
.toggle label:active {
background: var(--cpd-color-bg-action-secondary-hovered);
box-shadow: none;
}
.toggle input:checked + label {
color: var(--cpd-color-icon-on-solid-primary);
background: var(--cpd-color-bg-action-primary-rest);
}
@media (hover: hover) {
.toggle input:checked + label:hover {
background: var(--cpd-color-bg-action-primary-hovered);
}
}
.toggle input:checked + label:active {
background: var(--cpd-color-bg-action-primary-hovered);
}
.toggle label > svg {
display: block;
}
.toggle label:last-child {
margin-inline-start: 5px;
}
.toggle input:focus-visible + label {
outline: auto;
}

75
src/room/LayoutToggle.tsx Normal file
View File

@@ -0,0 +1,75 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ChangeEvent, FC, useCallback, useId } from "react";
import { useTranslation } from "react-i18next";
import { Tooltip } from "@vector-im/compound-web";
import { ReactComponent as SpotlightViewIcon } from "@vector-im/compound-design-tokens/icons/spotlight-view.svg";
import { ReactComponent as GridViewIcon } from "@vector-im/compound-design-tokens/icons/grid-view.svg";
import classNames from "classnames";
import styles from "./LayoutToggle.module.css";
export type Layout = "spotlight" | "grid";
interface Props {
layout: Layout;
setLayout: (layout: Layout) => void;
className?: string;
}
export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
const { t } = useTranslation();
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
[setLayout]
);
const spotlightId = useId();
const gridId = useId();
return (
<div className={classNames(styles.toggle, className)}>
<input
id={spotlightId}
type="radio"
name="layout"
value="spotlight"
checked={layout === "spotlight"}
onChange={onChange}
/>
<Tooltip label={t("Spotlight")}>
<label htmlFor={spotlightId}>
<SpotlightViewIcon aria-label={t("Spotlight")} />
</label>
</Tooltip>
<input
id={gridId}
type="radio"
name="layout"
value="grid"
checked={layout === "grid"}
onChange={onChange}
/>
<Tooltip label={t("Grid")}>
<label htmlFor={gridId}>
<GridViewIcon aria-label={t("Grid")} />
</label>
</Tooltip>
</div>
);
};

View File

@@ -16,32 +16,39 @@ limitations under the License.
import { useRef, useEffect, FC } from "react";
import { Trans, useTranslation } from "react-i18next";
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
import styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { getRoomUrl } from "../matrix-utils";
import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography";
import { useLocationNavigation } from "../useLocationNavigation";
import { MatrixInfo, VideoPreview } from "./VideoPreview";
import { MuteStates } from "./MuteStates";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { ShareButton } from "../button/ShareButton";
interface Props {
client: MatrixClient;
matrixInfo: MatrixInfo;
muteStates: MuteStates;
onEnter: () => void;
isEmbedded: boolean;
hideHeader: boolean;
participatingMembers: RoomMember[];
onShareClick: (() => void) | null;
}
export const LobbyView: FC<Props> = ({
client,
matrixInfo,
muteStates,
onEnter,
isEmbedded,
hideHeader,
participatingMembers,
onShareClick,
}) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
@@ -59,10 +66,17 @@ export const LobbyView: FC<Props> = ({
{!hideHeader && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={matrixInfo.roomName} />
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
/>
</LeftNav>
<RightNav>
<UserMenuContainer />
{onShareClick !== null && <ShareButton onClick={onShareClick} />}
</RightNav>
</Header>
)}

View File

@@ -20,20 +20,20 @@ import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
import styles from "./ShareModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface Props extends Omit<ModalProps, "title" | "children"> {
roomId: string;
}
export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
export const ShareModal: FC<Props> = ({ roomId, ...rest }) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(roomId);
return (
<Modal
title={t("Invite people")}
title={t("Share this call")}
isDismissable
className={styles.inviteModal}
{...rest}

View File

@@ -41,6 +41,8 @@ export type MatrixInfo = {
roomId: string;
roomName: string;
roomAlias: string | null;
roomAvatar: string | null;
roomEncrypted: boolean;
};
interface Props {

27
src/room/useRoomName.ts Normal file
View File

@@ -0,0 +1,27 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { useState } from "react";
import { useTypedEventEmitter } from "../useEvents";
export function useRoomName(room: Room): string {
const [, setNumUpdates] = useState(0);
// Whenever the name changes, force an update
useTypedEventEmitter(room, RoomEvent.Name, () => setNumUpdates((n) => n + 1));
return room.name;
}