Merge branch 'livekit' into eslint-upgrade

This commit is contained in:
Robin
2023-10-11 10:30:57 -04:00
100 changed files with 2600 additions and 9682 deletions

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { FC, MouseEvent, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Text } from "@vector-im/compound-web";
import { ReactComponent as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg";
import PopOutIcon from "@vector-im/compound-design-tokens/icons/pop-out.svg?react";
import { logger } from "matrix-js-sdk/src/logger";
import { Modal } from "../Modal";

View File

@@ -17,8 +17,8 @@ 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 LockIcon from "@vector-im/compound-design-tokens/icons/lock.svg?react";
import LockOffIcon from "@vector-im/compound-design-tokens/icons/lock-off.svg?react";
import styles from "./EncryptionLock.module.css";

View File

@@ -1,41 +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.
*/
.inspector {
background-color: var(--cpd-color-bg-subtle-secondary);
}
.scrollContainer {
height: 100%;
overflow-y: auto;
}
.sequenceDiagramViewer {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.selectInput {
align-self: flex-start;
}
.sequenceDiagramViewer :global(.messageText) {
font-size: var(--font-size-caption);
fill: var(--cpd-color-text-primary) !important;
stroke: var(--cpd-color-text-primary) !important;
}

View File

@@ -1,543 +0,0 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/*
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 * as Sentry from "@sentry/react";
import { Resizable } from "re-resizable";
import {
useEffect,
useState,
useReducer,
useRef,
createContext,
useContext,
Dispatch,
SetStateAction,
FC,
PropsWithChildren,
} from "react";
import ReactJson, { CollapsedFieldProps } from "react-json-view";
import mermaid from "mermaid";
import { Item } from "@react-stately/collections";
import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event";
import {
GroupCall,
GroupCallError,
GroupCallEvent,
} from "matrix-js-sdk/src/webrtc/groupCall";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import {
CallEvent,
CallState,
CallError,
MatrixCall,
VoipEvent,
} from "matrix-js-sdk/src/webrtc/call";
import styles from "./GroupCallInspector.module.css";
import { SelectInput } from "../input/SelectInput";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
interface InspectorContextState {
eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] };
remoteUserIds?: string[];
localUserId?: string;
localSessionId?: string;
}
const defaultCollapsedFields = [
"org.matrix.msc3401.call",
"org.matrix.msc3401.call.member",
"calls",
"callStats",
"hangupCalls",
"toDeviceEvents",
"sentVoipEvents",
"content",
];
function shouldCollapse({ name }: CollapsedFieldProps): boolean {
return name ? defaultCollapsedFields.includes(name) : false;
}
function getUserName(userId: string): string {
const match = userId.match(/@([^:]+):/);
return match && match.length > 0
? match[1].replace("-", " ").replace(/\W/g, "")
: userId.replace(/\W/g, "");
}
function formatContent(type: string, content: CallEventContent): string {
if (type === "m.call.hangup") {
return `callId: ${content.call_id.slice(-4)} reason: ${
content.reason
} senderSID: ${content.sender_session_id} destSID: ${
content.dest_session_id
}`;
}
if (type.startsWith("m.call.")) {
return `callId: ${content.call_id?.slice(-4)} senderSID: ${
content.sender_session_id
} destSID: ${content.dest_session_id}`;
} else if (type === "org.matrix.msc3401.call.member") {
const call =
content["m.calls"] &&
content["m.calls"].length > 0 &&
content["m.calls"][0];
const device =
call &&
call["m.devices"] &&
call["m.devices"].length > 0 &&
call["m.devices"][0];
return `conf_id: ${call && call["m.call_id"].slice(-4)} sessionId: ${
device && device.session_id
}`;
} else {
return "";
}
}
const dateFormatter = new Intl.DateTimeFormat([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore the linter does not know about this property of the DataTimeFormatOptions
fractionalSecondDigits: 3,
});
function formatTimestamp(timestamp: number | Date): string {
return dateFormatter.format(timestamp);
}
function formatType(event: SequenceDiagramMatrixEvent): string {
if (event.content.msgtype === "m.bad.encrypted") return "Undecryptable";
return event.type;
}
function lineForEvent(event: SequenceDiagramMatrixEvent): string {
return `${getUserName(event.from)} ${
event.ignored ? "-x" : "->>"
} ${getUserName(event.to)}: ${formatTimestamp(event.timestamp)} ${formatType(
event
)} ${formatContent(event.type, event.content)}`;
}
export const InspectorContext =
createContext<
[InspectorContextState, Dispatch<SetStateAction<InspectorContextState>>]
>(undefined);
export const InspectorContextProvider: FC<PropsWithChildren<{}>> = ({
children,
}) => {
// We take the tuple of [currentState, setter] and stick
// it straight into the context for other things to call
// the setState method... this feels like a fairly severe
// contortion of the hooks API - is this really the best way
// to do this?
const context = useState<InspectorContextState>({});
return (
<InspectorContext.Provider value={context}>
{children}
</InspectorContext.Provider>
);
};
type CallEventContent = {
["m.calls"]: {
["m.devices"]: { session_id: string; [x: string]: unknown }[];
["m.call_id"]: string;
}[];
} & {
call_id: string;
reason: string;
sender_session_id: string;
dest_session_id: string;
} & IContent;
export type SequenceDiagramMatrixEvent = {
to: string;
from: string;
timestamp: number;
type: string;
content: CallEventContent;
ignored: boolean;
};
interface SequenceDiagramViewerProps {
localUserId: string;
remoteUserIds: string[];
selectedUserId: string;
onSelectUserId: (userId: string) => void;
events: SequenceDiagramMatrixEvent[];
}
export const SequenceDiagramViewer: FC<SequenceDiagramViewerProps> = ({
localUserId,
remoteUserIds,
selectedUserId,
onSelectUserId,
events,
}) => {
const mermaidElRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: true,
theme: "dark",
sequence: {
showSequenceNumbers: true,
},
});
}, []);
useEffect(() => {
const graphDefinition = `sequenceDiagram
participant ${getUserName(localUserId)}
participant Room
participant ${selectedUserId ? getUserName(selectedUserId) : "unknown"}
${events ? events.map(lineForEvent).join("\n ") : ""}
`;
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
if (!mermaidElRef.current) return;
mermaidElRef.current.innerHTML = svgCode;
});
}, [events, localUserId, selectedUserId]);
return (
<div className={styles.scrollContainer}>
<div className={styles.sequenceDiagramViewer}>
<SelectInput
className={styles.selectInput}
label="Remote User"
selectedKey={selectedUserId}
onSelectionChange={(key): void => onSelectUserId(key.toString())}
>
{remoteUserIds.map((userId) => (
<Item key={userId}>{userId}</Item>
))}
</SelectInput>
<div id="mermaid" />
<div ref={mermaidElRef} />
</div>
</div>
);
};
function reducer(
state: InspectorContextState,
action: {
type?: CallEvent | ClientEvent | RoomStateEvent;
event?: MatrixEvent;
rawEvent?: VoipEvent;
callStateEvent?: MatrixEvent;
memberStateEvents?: MatrixEvent[];
}
): InspectorContextState {
switch (action.type) {
case RoomStateEvent.Events: {
const { event, callStateEvent, memberStateEvents } = action;
let eventsByUserId = state.eventsByUserId;
let remoteUserIds = state.remoteUserIds;
if (event) {
const fromId = event.getStateKey();
remoteUserIds =
fromId === state.localUserId || eventsByUserId[fromId]
? state.remoteUserIds
: [...state.remoteUserIds, fromId];
eventsByUserId = { ...state.eventsByUserId };
if (event.getStateKey() === state.localUserId) {
for (const userId in eventsByUserId) {
eventsByUserId[userId] = [
...(eventsByUserId[userId] || []),
{
from: fromId,
to: "Room",
type: event.getType(),
content: event.getContent(),
timestamp: event.getTs() || Date.now(),
ignored: false,
},
];
}
} else {
eventsByUserId[fromId] = [
...(eventsByUserId[fromId] || []),
{
from: fromId,
to: "Room",
type: event.getType(),
content: event.getContent(),
timestamp: event.getTs() || Date.now(),
ignored: false,
},
];
}
}
return {
...state,
eventsByUserId,
remoteUserIds,
callStateEvent: callStateEvent.getContent(),
memberStateEvents: Object.fromEntries(
memberStateEvents.map((e) => [e.getStateKey(), e.getContent()])
),
};
}
case ClientEvent.ReceivedVoipEvent: {
const event = action.event;
const eventsByUserId = { ...state.eventsByUserId };
const fromId = event.getSender();
const toId = state.localUserId;
const content = event.getContent<CallEventContent>();
const remoteUserIds = eventsByUserId[fromId]
? state.remoteUserIds
: [...state.remoteUserIds, fromId];
eventsByUserId[fromId] = [
...(eventsByUserId[fromId] || []),
{
from: fromId,
to: toId,
type: event.getType(),
content,
timestamp: event.getTs() || Date.now(),
ignored: state.localSessionId !== content.dest_session_id,
},
];
return { ...state, eventsByUserId, remoteUserIds };
}
case CallEvent.SendVoipEvent: {
const event = action.rawEvent;
const eventsByUserId = { ...state.eventsByUserId };
const fromId = state.localUserId;
const toId = event.userId as string;
const remoteUserIds = eventsByUserId[toId]
? state.remoteUserIds
: [...state.remoteUserIds, toId];
eventsByUserId[toId] = [
...(eventsByUserId[toId] || []),
{
from: fromId,
to: toId,
type: event.eventType as string,
content: event.content as CallEventContent,
timestamp: Date.now(),
ignored: false,
},
];
return { ...state, eventsByUserId, remoteUserIds };
}
default:
return state;
}
}
function useGroupCallState(
client: MatrixClient,
groupCall: GroupCall,
otelGroupCallMembership: OTelGroupCallMembership
): InspectorContextState {
const [state, dispatch] = useReducer(reducer, {
localUserId: client.getUserId(),
localSessionId: client.getSessionId(),
eventsByUserId: {},
remoteUserIds: [],
callStateEvent: null,
memberStateEvents: {},
});
useEffect(() => {
function onUpdateRoomState(event?: MatrixEvent): void {
const callStateEvent = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call",
groupCall.groupCallId
);
const memberStateEvents = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call.member"
);
dispatch({
type: RoomStateEvent.Events,
event,
callStateEvent,
memberStateEvents,
});
otelGroupCallMembership?.onUpdateRoomState(event);
}
function onReceivedVoipEvent(event: MatrixEvent): void {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
otelGroupCallMembership?.onReceivedVoipEvent(event);
}
function onSendVoipEvent(event: VoipEvent, call: MatrixCall): void {
dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event });
otelGroupCallMembership?.onSendEvent(call, event);
}
function onCallStateChange(
newState: CallState,
_: CallState,
call: MatrixCall
): void {
otelGroupCallMembership?.onCallStateChange(call, newState);
}
function onCallError(error: CallError, call: MatrixCall): void {
otelGroupCallMembership.onCallError(error, call);
}
function onGroupCallError(error: GroupCallError): void {
otelGroupCallMembership.onGroupCallError(error);
}
function onUndecryptableToDevice(event: MatrixEvent): void {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
Sentry.captureMessage("Undecryptable to-device Event");
// probably unnecessary if it's now captured via otel?
PosthogAnalytics.instance.eventUndecryptableToDevice.track(
groupCall.groupCallId
);
otelGroupCallMembership.onUndecryptableToDevice(event);
}
client.on(RoomStateEvent.Events, onUpdateRoomState);
groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent);
groupCall.on(CallEvent.State, onCallStateChange);
groupCall.on(CallEvent.Error, onCallError);
groupCall.on(GroupCallEvent.Error, onGroupCallError);
//client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup);
client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice);
onUpdateRoomState();
return () => {
client.removeListener(RoomStateEvent.Events, onUpdateRoomState);
groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent);
groupCall.removeListener(CallEvent.State, onCallStateChange);
groupCall.removeListener(CallEvent.Error, onCallError);
groupCall.removeListener(GroupCallEvent.Error, onGroupCallError);
//client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup);
client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
client.removeListener(
ClientEvent.UndecryptableToDeviceEvent,
onUndecryptableToDevice
);
};
}, [client, groupCall, otelGroupCallMembership]);
return state;
}
interface GroupCallInspectorProps {
client: MatrixClient;
groupCall: GroupCall;
otelGroupCallMembership: OTelGroupCallMembership;
show: boolean;
}
export const GroupCallInspector: FC<GroupCallInspectorProps> = ({
client,
groupCall,
otelGroupCallMembership,
show,
}) => {
const [currentTab, setCurrentTab] = useState("sequence-diagrams");
const [selectedUserId, setSelectedUserId] = useState<string>();
const state = useGroupCallState(client, groupCall, otelGroupCallMembership);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setState] = useContext(InspectorContext);
useEffect(() => {
setState(state);
}, [setState, state]);
if (!show) {
return null;
}
return (
<Resizable
enable={{ top: true }}
defaultSize={{ height: 200, width: 0 }}
className={styles.inspector}
>
<div className={styles.toolbar}>
<button onClick={(): void => setCurrentTab("sequence-diagrams")}>
Sequence Diagrams
</button>
<button onClick={(): void => setCurrentTab("inspector")}>
Inspector
</button>
</div>
{currentTab === "sequence-diagrams" &&
state.localUserId &&
selectedUserId &&
state.eventsByUserId &&
state.remoteUserIds && (
<SequenceDiagramViewer
localUserId={state.localUserId}
selectedUserId={selectedUserId}
onSelectUserId={setSelectedUserId}
remoteUserIds={state.remoteUserIds}
events={state.eventsByUserId[selectedUserId]}
/>
)}
{currentTab === "inspector" && (
<ReactJson
theme="monokai"
src={state}
name={null}
indentWidth={2}
shouldCollapse={shouldCollapse}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard
style={{ height: "100%", overflowY: "scroll" }}
/>
)}
</Resizable>
);
};

View File

@@ -20,7 +20,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room, isE2EESupported } 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 { JoinRule } from "matrix-js-sdk/src/matrix";
import { Heading, Link, Text } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
@@ -39,15 +39,12 @@ import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
import {
useManageRoomSharedKey,
useIsRoomE2EE,
} from "../e2ee/sharedKeyManagement";
import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useJoinRule } from "./useJoinRule";
import { ShareModal } from "./ShareModal";
import { InviteModal } from "./InviteModal";
declare global {
interface Window {
@@ -75,7 +72,7 @@ export const GroupCallView: FC<Props> = ({
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
const e2eeSharedKey = useManageRoomSharedKey(rtcSession.room.roomId);
const e2eeSharedKey = useRoomSharedKey(rtcSession.room.roomId);
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
useEffect(() => {
@@ -111,18 +108,11 @@ export const GroupCallView: FC<Props> = ({
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]);
// Count each member only once, regardless of how many devices they use
const participantCount = useMemo(
() => new Set<string>(memberships.map((m) => m.sender!)).size,
[memberships]
);
const deviceContext = useMediaDevices();
const latestDevices = useRef<MediaDevices>();
@@ -226,13 +216,16 @@ export const GroupCallView: FC<Props> = ({
sendInstantly
);
leaveRTCSession(rtcSession);
await leaveRTCSession(rtcSession);
if (widget) {
// we need to wait until the callEnded event is tracked on posthog.
// Otherwise the iFrame gets killed before the callEnded event got tracked.
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
widget.api.setAlwaysOnScreen(false);
PosthogAnalytics.instance.logout();
// we will always send the hangup event after the memberships have been updated
// calling leaveRTCSession.
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
}
@@ -278,15 +271,15 @@ export const GroupCallView: FC<Props> = ({
const joinRule = useJoinRule(rtcSession.room);
const [shareModalOpen, setShareModalOpen] = useState(false);
const onDismissShareModal = useCallback(
() => setShareModalOpen(false),
[setShareModalOpen]
const [shareModalOpen, setInviteModalOpen] = useState(false);
const onDismissInviteModal = useCallback(
() => setInviteModalOpen(false),
[setInviteModalOpen]
);
const onShareClickFn = useCallback(
() => setShareModalOpen(true),
[setShareModalOpen]
() => setInviteModalOpen(true),
[setInviteModalOpen]
);
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
@@ -329,10 +322,10 @@ export const GroupCallView: FC<Props> = ({
}
const shareModal = (
<ShareModal
<InviteModal
room={rtcSession.room}
open={shareModalOpen}
onDismiss={onDismissShareModal}
onDismiss={onDismissInviteModal}
/>
);
@@ -344,7 +337,7 @@ export const GroupCallView: FC<Props> = ({
client={client}
matrixInfo={matrixInfo}
rtcSession={rtcSession}
participatingMembers={participatingMembers}
participantCount={participantCount}
onLeave={onLeave}
hideHeader={hideHeader}
muteStates={muteStates}
@@ -395,7 +388,7 @@ export const GroupCallView: FC<Props> = ({
onEnter={(): void => enterRTCSession(rtcSession)}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
participatingMembers={participatingMembers}
participantCount={participantCount}
onShareClick={onShareClick}
/>
</>

View File

@@ -17,18 +17,18 @@ limitations under the License.
.inRoom {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 100%;
height: 100%;
width: 100%;
--footerPadding: var(--cpd-space-4x);
--footerHeight: calc(50px + 2 * var(--footerPadding));
}
.controlsOverlay {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
overflow-inline: hidden;
contain: strict;
}
.centerMessage {
@@ -45,17 +45,15 @@ limitations under the License.
}
.footer {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
box-sizing: border-box;
position: sticky;
inset-block-end: 0;
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-areas: "logo buttons layout";
align-items: center;
gap: var(--cpd-space-3x);
padding: var(--footerPadding) var(--inline-content-inset);
padding-block: var(--cpd-space-4x);
padding-inline: var(--inline-content-inset);
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
@@ -84,14 +82,14 @@ limitations under the License.
}
@media (min-height: 400px) {
.inRoom {
--footerPadding: var(--cpd-space-10x);
.footer {
padding-block: var(--cpd-space-10x);
}
}
@media (min-height: 800px) {
.inRoom {
--footerPadding: var(--cpd-space-15x);
.footer {
padding-block: var(--cpd-space-15x);
}
}

View File

@@ -42,8 +42,8 @@ import useMeasure from "react-use-measure";
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 LogoMark from "../icons/LogoMark.svg?react";
import LogoType from "../icons/LogoType.svg?react";
import type { IWidgetApiRequest } from "matrix-widget-api";
import {
HangupButton,
@@ -78,7 +78,7 @@ import { useWakeLock } from "../useWakeLock";
import { useMergedRefs } from "../useMergedRefs";
import { MuteStates } from "./MuteStates";
import { MatrixInfo } from "./VideoPreview";
import { ShareButton } from "../button/ShareButton";
import { InviteButton } from "../button/InviteButton";
import { LayoutToggle } from "./LayoutToggle";
import {
ECAddonConnectionState,
@@ -129,7 +129,7 @@ export interface InCallViewProps {
rtcSession: MatrixRTCSession;
livekitRoom: Room;
muteStates: MuteStates;
participatingMembers: RoomMember[];
participantCount: number;
onLeave: (error?: Error) => void;
hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership;
@@ -143,7 +143,7 @@ export const InCallView: FC<InCallViewProps> = ({
rtcSession,
livekitRoom,
muteStates,
participatingMembers,
participantCount,
onLeave,
hideHeader,
otelGroupCallMembership,
@@ -178,7 +178,6 @@ export const InCallView: FC<InCallViewProps> = ({
screenSharingTracks.length > 0
);
//const [showInspector] = useShowInspector();
const [showConnectionStats] = useShowConnectionStats();
const { hideScreensharing } = useUrlParams();
@@ -353,19 +352,19 @@ export const InCallView: FC<InCallViewProps> = ({
const buttons: JSX.Element[] = [];
buttons.push(
<VideoButton
key="2"
muted={!muteStates.video.enabled}
onPress={toggleCamera}
disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute"
/>,
<MicButton
key="1"
muted={!muteStates.audio.enabled}
onPress={toggleMicrophone}
disabled={muteStates.audio.setEnabled === null}
data-testid="incall_mute"
/>,
<VideoButton
key="2"
muted={!muteStates.video.enabled}
onPress={toggleCamera}
disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute"
/>
);
@@ -420,13 +419,12 @@ export const InCallView: FC<InCallViewProps> = ({
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
participantCount={participantCount}
/>
</LeftNav>
<RightNav>
{!reducedControls && onShareClick !== null && (
<ShareButton onClick={onShareClick} />
<InviteButton onClick={onShareClick} />
)}
</RightNav>
</Header>
@@ -436,14 +434,6 @@ export const InCallView: FC<InCallViewProps> = ({
{renderContent()}
{footer}
</div>
{/*otelGroupCallMembership && (
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
)*/}
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
<SettingsModal
client={client}

View File

@@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.copyButton {
.url {
text-align: center;
color: var(--cpd-color-text-secondary);
margin-block-end: var(--cpd-space-8x);
}
.button {
width: 100%;
}

84
src/room/InviteModal.tsx Normal file
View File

@@ -0,0 +1,84 @@
/*
Copyright 2022 - 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, MouseEvent, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Room } from "matrix-js-sdk";
import { Button, Text } from "@vector-im/compound-web";
import LinkIcon from "@vector-im/compound-design-tokens/icons/link.svg?react";
import CheckIcon from "@vector-im/compound-design-tokens/icons/check.svg?react";
import useClipboard from "react-use-clipboard";
import { Modal } from "../Modal";
import { getAbsoluteRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { Toast } from "../Toast";
interface Props {
room: Room;
open: boolean;
onDismiss: () => void;
}
export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(room.roomId);
const url = useMemo(
() =>
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
[room, roomSharedKey]
);
const [, setCopied] = useClipboard(url);
const [toastOpen, setToastOpen] = useState(false);
const onToastDismiss = useCallback(() => setToastOpen(false), [setToastOpen]);
const onButtonClick = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
setCopied();
onDismiss();
setToastOpen(true);
},
[setCopied, onDismiss]
);
return (
<>
<Modal title={t("Invite to this call")} open={open} onDismiss={onDismiss}>
<Text className={styles.url} size="sm" weight="semibold">
{url}
</Text>
<Button
className={styles.button}
Icon={LinkIcon}
onClick={onButtonClick}
data-testid="modal_inviteLink"
>
{t("Copy link")}
</Button>
</Modal>
<Toast
open={toastOpen}
onDismiss={onToastDismiss}
autoDismiss={2000}
Icon={CheckIcon}
>
{t("Link copied to clipboard")}
</Toast>
</>
);
};

View File

@@ -18,6 +18,7 @@ limitations under the License.
padding: 2px;
border: 1px solid var(--cpd-color-border-interactive-secondary);
border-radius: var(--cpd-radius-pill-effect);
background: var(--cpd-color-bg-canvas-default);
box-shadow: 0px 0px 40px 0px rgba(0, 0, 0, 0.5);
display: flex;
}

View File

@@ -17,8 +17,8 @@ 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 SpotlightViewIcon from "@vector-im/compound-design-tokens/icons/spotlight-view.svg?react";
import GridViewIcon from "@vector-im/compound-design-tokens/icons/grid-view.svg?react";
import classNames from "classnames";
import styles from "./LayoutToggle.module.css";

View File

@@ -23,7 +23,6 @@ limitations under the License.
flex: 1;
overflow: hidden;
height: 100%;
padding-block-end: var(--footerHeight);
}
@media (max-width: 500px) {

View File

@@ -16,7 +16,7 @@ limitations under the License.
import { FC, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Button, Link } from "@vector-im/compound-web";
import classNames from "classnames";
import { useHistory } from "react-router-dom";
@@ -27,7 +27,7 @@ import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { useLocationNavigation } from "../useLocationNavigation";
import { MatrixInfo, VideoPreview } from "./VideoPreview";
import { MuteStates } from "./MuteStates";
import { ShareButton } from "../button/ShareButton";
import { InviteButton } from "../button/InviteButton";
import {
HangupButton,
MicButton,
@@ -44,7 +44,7 @@ interface Props {
onEnter: () => void;
confineToRoom: boolean;
hideHeader: boolean;
participatingMembers: RoomMember[];
participantCount: number;
onShareClick: (() => void) | null;
}
@@ -55,7 +55,7 @@ export const LobbyView: FC<Props> = ({
onEnter,
confineToRoom,
hideHeader,
participatingMembers,
participantCount,
onShareClick,
}) => {
const { t } = useTranslation();
@@ -104,12 +104,11 @@ export const LobbyView: FC<Props> = ({
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
participantCount={participantCount}
/>
</LeftNav>
<RightNav>
{onShareClick !== null && <ShareButton onClick={onShareClick} />}
{onShareClick !== null && <InviteButton onClick={onShareClick} />}
</RightNav>
</Header>
)}
@@ -129,16 +128,16 @@ export const LobbyView: FC<Props> = ({
<div className={inCallStyles.footer}>
{recentsButtonInFooter && recentsButton}
<div className={inCallStyles.buttons}>
<VideoButton
muted={!muteStates.video.enabled}
onPress={onVideoPress}
disabled={muteStates.video.setEnabled === null}
/>
<MicButton
muted={!muteStates.audio.enabled}
onPress={onAudioPress}
disabled={muteStates.audio.setEnabled === null}
/>
<VideoButton
muted={!muteStates.video.enabled}
onPress={onVideoPress}
disabled={muteStates.video.setEnabled === null}
/>
<SettingsButton onPress={openSettings} />
{!confineToRoom && <HangupButton onPress={onLeaveClick} />}
</div>

View File

@@ -17,6 +17,7 @@ limitations under the License.
import { FC, useCallback, useState } from "react";
import { useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger";
import styles from "./RoomAuthView.module.css";
import { Button } from "../button";
@@ -46,7 +47,7 @@ export const RoomAuthView: FC = () => {
typeof dataForDisplayName === "string" ? dataForDisplayName : "";
registerPasswordlessUser(displayName).catch((error) => {
console.error("Failed to register passwordless user", e);
logger.error("Failed to register passwordless user", e);
setLoading(false);
setError(error);
});

View File

@@ -16,6 +16,7 @@ limitations under the License.
import { FC, useEffect, useState, useCallback, ReactNode } from "react";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { logger } from "matrix-js-sdk/src/logger";
import { useClientLegacy } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
@@ -37,7 +38,7 @@ export const RoomPage: FC = () => {
const roomIdOrAlias = roomId ?? roomAlias;
if (!roomIdOrAlias) {
console.error("No room specified");
logger.error("No room specified");
}
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();

View File

@@ -1,51 +0,0 @@
/*
Copyright 2022 - 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 { useTranslation } from "react-i18next";
import { Room } from "matrix-js-sdk";
import { Modal } from "../Modal";
import { CopyButton } from "../button";
import { getAbsoluteRoomUrl } from "../matrix-utils";
import styles from "./ShareModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface Props {
room: Room;
open: boolean;
onDismiss: () => void;
}
export const ShareModal: FC<Props> = ({ room, open, onDismiss }) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(room.roomId);
return (
<Modal title={t("Share this call")} open={open} onDismiss={onDismiss}>
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getAbsoluteRoomUrl(
room.roomId,
room.name,
roomSharedKey ?? undefined
)}
data-testid="modal_inviteLink"
/>
</Modal>
);
};

View File

@@ -24,6 +24,7 @@ import {
Track,
} from "livekit-client";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import { Avatar } from "../Avatar";
import styles from "./VideoPreview.module.css";
@@ -80,7 +81,7 @@ export const VideoPreview: FC<Props> = ({
},
},
(error) => {
console.error("Error while creating preview Tracks:", error);
logger.error("Error while creating preview Tracks:", error);
}
);
const videoTrack = useMemo(