forked from github/cinny
Add own control buttons for element-call (#2744)
* add mutation observer hok * add hook to read speaking member by observing iframe content * display speaking member name in call status bar and improve layout * fix shrining * add joined call control bar * remove chat toggle from room header * change member speaking icon to mic * fix joined call control appear in other * show spinner on end call button * hide call statusbar for mobile view when room is selected * make call statusbar more mobile friendly * fix call status bar item align
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import React, { ReactNode, useCallback, useRef } from 'react';
|
import React, { ReactNode, useCallback, useRef } from 'react';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { config } from 'folds';
|
||||||
import {
|
import {
|
||||||
CallEmbedContextProvider,
|
CallEmbedContextProvider,
|
||||||
CallEmbedRefContextProvider,
|
CallEmbedRefContextProvider,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Box, Chip, Icon, IconButton, Icons, Text, Tooltip, TooltipProvider } from 'folds';
|
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
import React, { useState } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { StatusDivider } from './components';
|
import { StatusDivider } from './components';
|
||||||
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
|
||||||
type MicrophoneButtonProps = {
|
type MicrophoneButtonProps = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -104,9 +105,11 @@ function VideoButton({ enabled, onToggle }: VideoButtonProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScreenShareButton() {
|
type ScreenShareButtonProps = {
|
||||||
const [enabled, setEnabled] = useState(false);
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
@@ -123,7 +126,7 @@ function ScreenShareButton() {
|
|||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="300"
|
radii="300"
|
||||||
size="300"
|
size="300"
|
||||||
onClick={() => setEnabled(!enabled)}
|
onClick={onToggle}
|
||||||
outlined
|
outlined
|
||||||
>
|
>
|
||||||
<Icon size="100" src={Icons.ScreenShare} filled={enabled} />
|
<Icon size="100" src={Icons.ScreenShare} filled={enabled} />
|
||||||
@@ -133,8 +136,14 @@ function ScreenShareButton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallControl({ callEmbed }: { callEmbed: CallEmbed }) {
|
export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; compact: boolean }) {
|
||||||
const { microphone, video, sound } = useCallControlState(callEmbed.control);
|
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
||||||
|
|
||||||
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
|
useCallback(() => callEmbed.hangup(), [callEmbed])
|
||||||
|
);
|
||||||
|
const exiting =
|
||||||
|
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box shrink="No" alignItems="Center" gap="300">
|
<Box shrink="No" alignItems="Center" gap="300">
|
||||||
@@ -144,21 +153,36 @@ export function CallControl({ callEmbed }: { callEmbed: CallEmbed }) {
|
|||||||
onToggle={() => callEmbed.control.toggleMicrophone()}
|
onToggle={() => callEmbed.control.toggleMicrophone()}
|
||||||
/>
|
/>
|
||||||
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
||||||
|
{!compact && <StatusDivider />}
|
||||||
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
||||||
{false && <ScreenShareButton />}
|
{!compact && (
|
||||||
|
<ScreenShareButton
|
||||||
|
enabled={screenshare}
|
||||||
|
onToggle={() => callEmbed.control.toggleScreenshare()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<StatusDivider />
|
<StatusDivider />
|
||||||
<Chip
|
<Chip
|
||||||
variant="Critical"
|
variant="Critical"
|
||||||
radii="300"
|
radii="Pill"
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
before={<Icon size="50" src={Icons.PhoneDown} filled />}
|
before={
|
||||||
|
exiting ? (
|
||||||
|
<Spinner variant="Critical" fill="Soft" size="50" />
|
||||||
|
) : (
|
||||||
|
<Icon size="50" src={Icons.PhoneDown} filled />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={exiting}
|
||||||
outlined
|
outlined
|
||||||
onClick={() => callEmbed.hangup()}
|
onClick={hangup}
|
||||||
>
|
>
|
||||||
|
{!compact && (
|
||||||
<Text as="span" size="L400">
|
<Text as="span" size="L400">
|
||||||
End
|
End
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,20 +47,19 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Spinner variant="Secondary" size="200" />
|
<Spinner variant="Secondary" size="200" />
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box grow="Yes" alignItems="Center" gap="Inherit">
|
||||||
grow="Yes"
|
{!compact && (
|
||||||
alignItems="Center"
|
<>
|
||||||
gap="Inherit"
|
|
||||||
justifyContent={compact ? 'Center' : undefined}
|
|
||||||
>
|
|
||||||
<CallRoomName room={room} />
|
<CallRoomName room={room} />
|
||||||
{speakers.size > 0 && !compact && (
|
{speakers.size > 0 && (
|
||||||
<>
|
<>
|
||||||
<StatusDivider />
|
<StatusDivider />
|
||||||
<span data-spacing-node />
|
<span data-spacing-node />
|
||||||
<MemberSpeaking room={room} speakers={speakers} />
|
<MemberSpeaking room={room} speakers={speakers} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{memberVisible && (
|
{memberVisible && (
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
@@ -69,8 +68,13 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{memberVisible && !compact && <StatusDivider />}
|
{memberVisible && !compact && <StatusDivider />}
|
||||||
<Box shrink="No" alignItems="Center" justifyContent="Center" gap="Inherit">
|
<Box shrink="No" alignItems="Center" gap="Inherit">
|
||||||
<CallControl callEmbed={callEmbed} />
|
{compact && (
|
||||||
|
<Box grow="Yes">
|
||||||
|
<CallRoomName room={room} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<CallControl compact={compact} callEmbed={callEmbed} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function LiveChip({ count, room, members }: LiveChipProps) {
|
|||||||
radii="Pill"
|
radii="Pill"
|
||||||
onClick={handleOpenMenu}
|
onClick={handleOpenMenu}
|
||||||
>
|
>
|
||||||
<Text className={css.LiveChipText} as="span" size="L400">
|
<Text className={css.LiveChipText} as="span" size="L400" truncate>
|
||||||
{count} Live
|
{count} Live
|
||||||
</Text>
|
</Text>
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function MemberSpeaking({ room, speakers }: MemberSpeakingProps) {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box alignItems="Center" gap="100">
|
<Box alignItems="Center" gap="100">
|
||||||
<Icon size="100" src={Icons.Phone} filled />
|
<Icon size="100" src={Icons.Mic} filled />
|
||||||
<Text size="T200" truncate>
|
<Text size="T200" truncate>
|
||||||
{speakingNames.length === 1 && (
|
{speakingNames.length === 1 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
203
src/app/features/call/CallControls.tsx
Normal file
203
src/app/features/call/CallControls.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React, { MouseEventHandler, useCallback, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
config,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
PopOut,
|
||||||
|
RectCords,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
toRem,
|
||||||
|
} from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { SequenceCard } from '../../components/sequence-card';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
import {
|
||||||
|
ChatButton,
|
||||||
|
ControlDivider,
|
||||||
|
MicrophoneButton,
|
||||||
|
ScreenShareButton,
|
||||||
|
SoundButton,
|
||||||
|
VideoButton,
|
||||||
|
} from './Controls';
|
||||||
|
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
||||||
|
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
|
||||||
|
type CallControlsProps = {
|
||||||
|
callEmbed: CallEmbed;
|
||||||
|
};
|
||||||
|
export function CallControls({ callEmbed }: CallControlsProps) {
|
||||||
|
const controlRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [compact, setCompact] = useState(document.body.clientWidth < 500);
|
||||||
|
|
||||||
|
useResizeObserver(
|
||||||
|
useCallback(() => {
|
||||||
|
const element = controlRef.current;
|
||||||
|
if (!element) return;
|
||||||
|
setCompact(element.clientWidth < 500);
|
||||||
|
}, []),
|
||||||
|
useCallback(() => controlRef.current, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { microphone, video, sound, screenshare, spotlight } = useCallControlState(
|
||||||
|
callEmbed.control
|
||||||
|
);
|
||||||
|
|
||||||
|
const [cords, setCords] = useState<RectCords>();
|
||||||
|
|
||||||
|
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpotlightClick = () => {
|
||||||
|
callEmbed.control.toggleSpotlight();
|
||||||
|
setCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReactionsClick = () => {
|
||||||
|
callEmbed.control.toggleReactions();
|
||||||
|
setCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsClick = () => {
|
||||||
|
callEmbed.control.toggleSettings();
|
||||||
|
setCords(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
|
useCallback(() => callEmbed.hangup(), [callEmbed])
|
||||||
|
);
|
||||||
|
const exiting =
|
||||||
|
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={controlRef}
|
||||||
|
className={css.CallControlContainer}
|
||||||
|
justifyContent="Center"
|
||||||
|
alignItems="Center"
|
||||||
|
>
|
||||||
|
<SequenceCard
|
||||||
|
className={css.ControlCard}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
gap="400"
|
||||||
|
radii="500"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="SpaceBetween"
|
||||||
|
>
|
||||||
|
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
|
<MicrophoneButton
|
||||||
|
enabled={microphone}
|
||||||
|
onToggle={() => callEmbed.control.toggleMicrophone()}
|
||||||
|
/>
|
||||||
|
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
||||||
|
</Box>
|
||||||
|
{!compact && <ControlDivider />}
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
|
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
||||||
|
<ScreenShareButton
|
||||||
|
enabled={screenshare}
|
||||||
|
onToggle={() => callEmbed.control.toggleScreenshare()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{!compact && <ControlDivider />}
|
||||||
|
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
||||||
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
|
<ChatButton />
|
||||||
|
<PopOut
|
||||||
|
anchor={cords}
|
||||||
|
position="Top"
|
||||||
|
align="Center"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setCords(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
|
<Box direction="Column" style={{ padding: config.space.S100 }}>
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleSpotlightClick}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
{spotlight ? 'Grid View' : 'Spotlight View'}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleReactionsClick}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Reactions
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleSettingsClick}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant="Surface"
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
outlined
|
||||||
|
aria-pressed={!!cords}
|
||||||
|
>
|
||||||
|
<Icon size="400" src={Icons.VerticalDots} />
|
||||||
|
</IconButton>
|
||||||
|
</PopOut>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No" direction="Column">
|
||||||
|
<Button
|
||||||
|
style={{ minWidth: toRem(88) }}
|
||||||
|
variant="Critical"
|
||||||
|
fill="Solid"
|
||||||
|
onClick={hangup}
|
||||||
|
before={
|
||||||
|
exiting ? (
|
||||||
|
<Spinner variant="Critical" fill="Solid" size="200" />
|
||||||
|
) : (
|
||||||
|
<Icon src={Icons.PhoneDown} size="200" filled />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={exiting}
|
||||||
|
>
|
||||||
|
<Text size="B400">End</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SequenceCard>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { RefObject, useRef } from 'react';
|
||||||
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
|
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
|
||||||
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
|
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
|
||||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
@@ -12,6 +12,7 @@ import { StateEvent } from '../../../types/matrix/room';
|
|||||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||||
import { CallMemberRenderer } from './CallMemberCard';
|
import { CallMemberRenderer } from './CallMemberCard';
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
|
import { CallControls } from './CallControls';
|
||||||
|
|
||||||
function JoinMessage({ hasParticipant }: { hasParticipant?: boolean }) {
|
function JoinMessage({ hasParticipant }: { hasParticipant?: boolean }) {
|
||||||
if (hasParticipant) return null;
|
if (hasParticipant) return null;
|
||||||
@@ -39,13 +40,10 @@ function AlreadyInCallMessage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallView() {
|
function CallPrescreen() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
|
|
||||||
const callViewRef = useRef<HTMLDivElement>(null);
|
|
||||||
useCallEmbedPlacementSync(callViewRef);
|
|
||||||
|
|
||||||
const powerLevels = usePowerLevelsContext();
|
const powerLevels = usePowerLevelsContext();
|
||||||
const creators = useRoomCreators(room);
|
const creators = useRoomCreators(room);
|
||||||
|
|
||||||
@@ -57,19 +55,9 @@ export function CallView() {
|
|||||||
const hasParticipant = callMembers.length > 0;
|
const hasParticipant = callMembers.length > 0;
|
||||||
|
|
||||||
const callEmbed = useCallEmbed();
|
const callEmbed = useCallEmbed();
|
||||||
const callJoined = useCallJoined(callEmbed);
|
|
||||||
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
|
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
|
||||||
|
|
||||||
const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
|
||||||
ref={callViewRef}
|
|
||||||
className={ContainerColor({ variant: 'Surface' })}
|
|
||||||
style={{ minWidth: toRem(280) }}
|
|
||||||
grow="Yes"
|
|
||||||
>
|
|
||||||
{!currentJoined && (
|
|
||||||
<Scroll variant="Surface" hideTrack>
|
<Scroll variant="Surface" hideTrack>
|
||||||
<Box className={css.CallViewContent} alignItems="Center" justifyContent="Center">
|
<Box className={css.CallViewContent} alignItems="Center" justifyContent="Center">
|
||||||
<Box style={{ maxWidth: toRem(382), width: '100%' }} direction="Column" gap="100">
|
<Box style={{ maxWidth: toRem(382), width: '100%' }} direction="Column" gap="100">
|
||||||
@@ -89,17 +77,48 @@ export function CallView() {
|
|||||||
<PrescreenControls canJoin={canJoin} />
|
<PrescreenControls canJoin={canJoin} />
|
||||||
<Header size="300">
|
<Header size="300">
|
||||||
{!inOtherCall &&
|
{!inOtherCall &&
|
||||||
(canJoin ? (
|
(canJoin ? <JoinMessage hasParticipant={hasParticipant} /> : <NoPermissionMessage />)}
|
||||||
<JoinMessage hasParticipant={hasParticipant} />
|
|
||||||
) : (
|
|
||||||
<NoPermissionMessage />
|
|
||||||
))}
|
|
||||||
{inOtherCall && <AlreadyInCallMessage />}
|
{inOtherCall && <AlreadyInCallMessage />}
|
||||||
</Header>
|
</Header>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallJoinedProps = {
|
||||||
|
containerRef: RefObject<HTMLDivElement>;
|
||||||
|
joined: boolean;
|
||||||
|
};
|
||||||
|
function CallJoined({ joined, containerRef }: CallJoinedProps) {
|
||||||
|
const callEmbed = useCallEmbed();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Box grow="Yes" ref={containerRef} />
|
||||||
|
{callEmbed && joined && <CallControls callEmbed={callEmbed} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CallView() {
|
||||||
|
const room = useRoom();
|
||||||
|
const callContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useCallEmbedPlacementSync(callContainerRef);
|
||||||
|
|
||||||
|
const callEmbed = useCallEmbed();
|
||||||
|
const callJoined = useCallJoined(callEmbed);
|
||||||
|
|
||||||
|
const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={ContainerColor({ variant: 'Surface' })}
|
||||||
|
style={{ minWidth: toRem(280) }}
|
||||||
|
grow="Yes"
|
||||||
|
>
|
||||||
|
{!currentJoined && <CallPrescreen />}
|
||||||
|
<CallJoined joined={currentJoined} containerRef={callContainerRef} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,38 @@ export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScreenShareButtonProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
};
|
||||||
|
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
delay={500}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={anchorRef}
|
||||||
|
variant={enabled ? 'Success' : 'Surface'}
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={() => onToggle()}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<Icon size="400" src={Icons.ScreenShare} filled={enabled} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ChatButton() {
|
export function ChatButton() {
|
||||||
const [chat, setChat] = useAtom(callChatAtom);
|
const [chat, setChat] = useAtom(callChatAtom);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton
|
|||||||
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||||
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
|
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
|
||||||
import { useCallPreferences } from '../../state/hooks/callPreferences';
|
import { useCallPreferences } from '../../state/hooks/callPreferences';
|
||||||
import { CallControlState } from '../../plugins/call/CallControlState';
|
|
||||||
|
|
||||||
type PrescreenControlsProps = {
|
type PrescreenControlsProps = {
|
||||||
canJoin?: boolean;
|
canJoin?: boolean;
|
||||||
@@ -50,7 +49,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant={disabled ? 'Secondary' : 'Success'}
|
variant={disabled ? 'Secondary' : 'Success'}
|
||||||
fill={disabled ? 'Soft' : 'Solid'}
|
fill={disabled ? 'Soft' : 'Solid'}
|
||||||
onClick={() => startCall(room, new CallControlState(microphone, video, sound))}
|
onClick={() => startCall(room, { microphone, video, sound })}
|
||||||
disabled={disabled || joining}
|
disabled={disabled || joining}
|
||||||
before={
|
before={
|
||||||
joining ? (
|
joining ? (
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ export const ControlDivider = style({
|
|||||||
export const CallMemberCard = style({
|
export const CallMemberCard = style({
|
||||||
padding: config.space.S300,
|
padding: config.space.S300,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const CallControlContainer = style({
|
||||||
|
padding: config.space.S400,
|
||||||
|
});
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
|||||||
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
||||||
import { callChatAtom } from '../../state/callEmbed';
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
|
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
|
||||||
import { CallControlState } from '../../plugins/call/CallControlState';
|
|
||||||
|
|
||||||
type RoomNavItemMenuProps = {
|
type RoomNavItemMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -292,7 +291,7 @@ export function RoomNavItem({
|
|||||||
// Start call in second click
|
// Start call in second click
|
||||||
if (selected) {
|
if (selected) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
startCall(room, new CallControlState(callPref.microphone, callPref.video, callPref.sound));
|
startCall(room, callPref);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||||
import { PageHeader } from '../../components/page';
|
import { PageHeader } from '../../components/page';
|
||||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||||
@@ -68,7 +67,6 @@ import { useRoomCreators } from '../../hooks/useRoomCreators';
|
|||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
import { callChatAtom } from '../../state/callEmbed';
|
|
||||||
import { RoomSettingsPage } from '../../state/roomSettings';
|
import { RoomSettingsPage } from '../../state/roomSettings';
|
||||||
|
|
||||||
type RoomMenuProps = {
|
type RoomMenuProps = {
|
||||||
@@ -266,8 +264,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||||||
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
|
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
|
||||||
const direct = useIsDirectRoom();
|
const direct = useIsDirectRoom();
|
||||||
|
|
||||||
const [chat, setChat] = useAtom(callChatAtom);
|
|
||||||
|
|
||||||
const pinnedEvents = useRoomPinnedEvents(room);
|
const pinnedEvents = useRoomPinnedEvents(room);
|
||||||
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
|
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
|
||||||
const encryptedRoom = !!encryptionEvent;
|
const encryptedRoom = !!encryptionEvent;
|
||||||
@@ -476,24 +472,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{callView && (
|
|
||||||
<TooltipProvider
|
|
||||||
position="Bottom"
|
|
||||||
offset={4}
|
|
||||||
tooltip={
|
|
||||||
<Tooltip>
|
|
||||||
<Text>Chat</Text>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(triggerRef) => (
|
|
||||||
<IconButton fill="None" ref={triggerRef} onClick={() => setChat(!chat)}>
|
|
||||||
<Icon size="400" src={Icons.Message} filled={chat} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Bottom"
|
position="Bottom"
|
||||||
align="End"
|
align="End"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { callEmbedAtom } from '../state/callEmbed';
|
|||||||
import { useResizeObserver } from './useResizeObserver';
|
import { useResizeObserver } from './useResizeObserver';
|
||||||
import { CallControlState } from '../plugins/call/CallControlState';
|
import { CallControlState } from '../plugins/call/CallControlState';
|
||||||
import { useCallMembersChange, useCallSession } from './useCall';
|
import { useCallMembersChange, useCallSession } from './useCall';
|
||||||
|
import { CallPreferences } from '../state/callPreferences';
|
||||||
|
|
||||||
const CallEmbedContext = createContext<CallEmbed | undefined>(undefined);
|
const CallEmbedContext = createContext<CallEmbed | undefined>(undefined);
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ export const createCallEmbed = (
|
|||||||
dm: boolean,
|
dm: boolean,
|
||||||
themeKind: ElementCallThemeKind,
|
themeKind: ElementCallThemeKind,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
controlState?: CallControlState
|
pref?: CallPreferences
|
||||||
): CallEmbed => {
|
): CallEmbed => {
|
||||||
const rtcSession = mx.matrixRTC.getRoomSession(room);
|
const rtcSession = mx.matrixRTC.getRoomSession(room);
|
||||||
const ongoing =
|
const ongoing =
|
||||||
@@ -49,6 +50,8 @@ export const createCallEmbed = (
|
|||||||
|
|
||||||
const intent = CallEmbed.getIntent(dm, ongoing);
|
const intent = CallEmbed.getIntent(dm, ongoing);
|
||||||
const widget = CallEmbed.getWidget(mx, room, intent, themeKind);
|
const widget = CallEmbed.getWidget(mx, room, intent, themeKind);
|
||||||
|
const controlState = pref && new CallControlState(pref.microphone, pref.video, pref.sound);
|
||||||
|
|
||||||
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
@@ -61,12 +64,12 @@ export const useCallStart = (dm = false) => {
|
|||||||
const callEmbedRef = useCallEmbedRef();
|
const callEmbedRef = useCallEmbedRef();
|
||||||
|
|
||||||
const startCall = useCallback(
|
const startCall = useCallback(
|
||||||
(room: Room, controlState?: CallControlState) => {
|
(room: Room, pref?: CallPreferences) => {
|
||||||
const container = callEmbedRef.current;
|
const container = callEmbedRef.current;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
throw new Error('Failed to start call, No embed container element found!');
|
throw new Error('Failed to start call, No embed container element found!');
|
||||||
}
|
}
|
||||||
const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, controlState);
|
const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref);
|
||||||
|
|
||||||
setCallEmbed(callEmbed);
|
setCallEmbed(callEmbed);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useCallEmbed } from '../hooks/useCallEmbed';
|
import { useCallEmbed } from '../hooks/useCallEmbed';
|
||||||
import { CallStatus } from '../features/call-status';
|
import { CallStatus } from '../features/call-status';
|
||||||
|
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||||
|
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||||
|
|
||||||
export function CallStatusRenderer() {
|
export function CallStatusRenderer() {
|
||||||
const callEmbed = useCallEmbed();
|
const callEmbed = useCallEmbed();
|
||||||
|
const selectedRoom = useSelectedRoom();
|
||||||
|
|
||||||
|
const screenSize = useScreenSizeContext();
|
||||||
|
|
||||||
if (!callEmbed) return null;
|
if (!callEmbed) return null;
|
||||||
|
|
||||||
|
if (screenSize === ScreenSize.Mobile && callEmbed.roomId === selectedRoom) return null;
|
||||||
|
|
||||||
return <CallStatus callEmbed={callEmbed} />;
|
return <CallStatus callEmbed={callEmbed} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,58 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
|
|
||||||
private iframe: HTMLIFrameElement;
|
private iframe: HTMLIFrameElement;
|
||||||
|
|
||||||
|
private controlMutationObserver: MutationObserver;
|
||||||
|
|
||||||
|
private get document(): Document | undefined {
|
||||||
|
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get screenshareButton(): HTMLElement | undefined {
|
||||||
|
const screenshareBtn = this.document?.querySelector(
|
||||||
|
'[data-testid="incall_screenshare"]'
|
||||||
|
) as HTMLElement | null;
|
||||||
|
|
||||||
|
return screenshareBtn ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get settingsButton(): HTMLElement | undefined {
|
||||||
|
const leaveBtn = this.document?.querySelector('[data-testid="incall_leave"]');
|
||||||
|
|
||||||
|
const settingsButton = leaveBtn?.previousElementSibling as HTMLElement | null;
|
||||||
|
|
||||||
|
return settingsButton ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get reactionsButton(): HTMLElement | undefined {
|
||||||
|
const reactionsButton = this.settingsButton?.previousElementSibling as HTMLElement | null;
|
||||||
|
|
||||||
|
return reactionsButton ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get spotlightButton(): HTMLInputElement | undefined {
|
||||||
|
const spotlightButton = this.document?.querySelector(
|
||||||
|
'input[value="spotlight"]'
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
|
||||||
|
return spotlightButton ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get gridButton(): HTMLInputElement | undefined {
|
||||||
|
const gridButton = this.document?.querySelector(
|
||||||
|
'input[value="grid"]'
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
|
||||||
|
return gridButton ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(state: CallControlState, call: ClientWidgetApi, iframe: HTMLIFrameElement) {
|
constructor(state: CallControlState, call: ClientWidgetApi, iframe: HTMLIFrameElement) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.call = call;
|
this.call = call;
|
||||||
this.iframe = iframe;
|
this.iframe = iframe;
|
||||||
|
|
||||||
|
this.controlMutationObserver = new MutationObserver(this.onControlMutation.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getState(): CallControlState {
|
public getState(): CallControlState {
|
||||||
@@ -38,6 +84,14 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
return this.state.sound;
|
return this.state.sound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get screenshare(): boolean {
|
||||||
|
return this.state.screenshare;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get spotlight(): boolean {
|
||||||
|
return this.state.spotlight;
|
||||||
|
}
|
||||||
|
|
||||||
public async applyState() {
|
public async applyState() {
|
||||||
await this.setMediaState({
|
await this.setMediaState({
|
||||||
audio_enabled: this.microphone,
|
audio_enabled: this.microphone,
|
||||||
@@ -47,6 +101,26 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.emitStateUpdate();
|
this.emitStateUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public startObserving() {
|
||||||
|
this.controlMutationObserver.disconnect();
|
||||||
|
|
||||||
|
const screenshareBtn = this.screenshareButton;
|
||||||
|
if (screenshareBtn) {
|
||||||
|
this.controlMutationObserver.observe(screenshareBtn, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-kind'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const spotlightBtn = this.spotlightButton;
|
||||||
|
if (spotlightBtn) {
|
||||||
|
this.controlMutationObserver.observe(spotlightBtn, {
|
||||||
|
attributes: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onControlMutation();
|
||||||
|
}
|
||||||
|
|
||||||
public applySound() {
|
public applySound() {
|
||||||
this.setSound(this.sound);
|
this.setSound(this.sound);
|
||||||
}
|
}
|
||||||
@@ -72,7 +146,9 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
const state = new CallControlState(
|
const state = new CallControlState(
|
||||||
data.audio_enabled ?? this.microphone,
|
data.audio_enabled ?? this.microphone,
|
||||||
data.video_enabled ?? this.video,
|
data.video_enabled ?? this.video,
|
||||||
this.sound
|
this.sound,
|
||||||
|
this.screenshare,
|
||||||
|
this.spotlight
|
||||||
);
|
);
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
@@ -83,6 +159,20 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onControlMutation() {
|
||||||
|
const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
|
||||||
|
const spotlight: boolean = this.spotlightButton?.checked ?? false;
|
||||||
|
|
||||||
|
this.state = new CallControlState(
|
||||||
|
this.microphone,
|
||||||
|
this.video,
|
||||||
|
this.sound,
|
||||||
|
screenshare,
|
||||||
|
spotlight
|
||||||
|
);
|
||||||
|
this.emitStateUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
public toggleMicrophone() {
|
public toggleMicrophone() {
|
||||||
const payload: ElementMediaStatePayload = {
|
const payload: ElementMediaStatePayload = {
|
||||||
audio_enabled: !this.microphone,
|
audio_enabled: !this.microphone,
|
||||||
@@ -104,7 +194,13 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
|
|
||||||
this.setSound(sound);
|
this.setSound(sound);
|
||||||
|
|
||||||
const state = new CallControlState(this.microphone, this.video, sound);
|
const state = new CallControlState(
|
||||||
|
this.microphone,
|
||||||
|
this.video,
|
||||||
|
sound,
|
||||||
|
this.screenshare,
|
||||||
|
this.spotlight
|
||||||
|
);
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.emitStateUpdate();
|
this.emitStateUpdate();
|
||||||
|
|
||||||
@@ -113,6 +209,30 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toggleScreenshare() {
|
||||||
|
this.screenshareButton?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleSpotlight() {
|
||||||
|
if (this.spotlight) {
|
||||||
|
this.gridButton?.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.spotlightButton?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleReactions() {
|
||||||
|
this.reactionsButton?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleSettings() {
|
||||||
|
this.settingsButton?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this.controlMutationObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
private emitStateUpdate() {
|
private emitStateUpdate() {
|
||||||
this.emit(CallControlEvent.StateUpdate);
|
this.emit(CallControlEvent.StateUpdate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,21 @@ export class CallControlState {
|
|||||||
|
|
||||||
public readonly sound: boolean;
|
public readonly sound: boolean;
|
||||||
|
|
||||||
constructor(microphone: boolean, video: boolean, sound: boolean) {
|
public readonly screenshare: boolean;
|
||||||
|
|
||||||
|
public readonly spotlight: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
microphone: boolean,
|
||||||
|
video: boolean,
|
||||||
|
sound: boolean,
|
||||||
|
screenshare = false,
|
||||||
|
spotlight = false
|
||||||
|
) {
|
||||||
this.microphone = microphone;
|
this.microphone = microphone;
|
||||||
this.video = video;
|
this.video = video;
|
||||||
this.sound = sound;
|
this.sound = sound;
|
||||||
|
this.screenshare = screenshare;
|
||||||
|
this.spotlight = spotlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export class CallEmbed {
|
|||||||
});
|
});
|
||||||
this.call.stop();
|
this.call.stop();
|
||||||
this.container.removeChild(this.iframe);
|
this.container.removeChild(this.iframe);
|
||||||
|
this.control.dispose();
|
||||||
|
|
||||||
this.mx.off(ClientEvent.Event, this.onEvent.bind(this));
|
this.mx.off(ClientEvent.Event, this.onEvent.bind(this));
|
||||||
this.mx.off(MatrixEventEvent.Decrypted, this.onEventDecrypted.bind(this));
|
this.mx.off(MatrixEventEvent.Decrypted, this.onEventDecrypted.bind(this));
|
||||||
@@ -233,6 +234,21 @@ export class CallEmbed {
|
|||||||
|
|
||||||
private onCallJoined(): void {
|
private onCallJoined(): void {
|
||||||
this.joined = true;
|
this.joined = true;
|
||||||
|
this.applyStyles();
|
||||||
|
this.control.startObserving();
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyStyles(): void {
|
||||||
|
const doc = this.document;
|
||||||
|
if (!doc) return;
|
||||||
|
|
||||||
|
doc.body.style.setProperty('background', 'none', 'important');
|
||||||
|
const controls = doc.body.querySelector('[data-testid="incall_leave"]')?.parentElement
|
||||||
|
?.parentElement;
|
||||||
|
if (controls) {
|
||||||
|
controls.style.setProperty('position', 'absolute');
|
||||||
|
controls.style.setProperty('visibility', 'hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEvent(ev: MatrixEvent): void {
|
private onEvent(ev: MatrixEvent): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user