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:
Ajay Bura 2026-03-09 14:04:48 +11:00 committed by GitHub
commit bc6caddcc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 521 additions and 100 deletions

View file

@ -1,7 +1,8 @@
import { Box, Chip, Icon, IconButton, Icons, Text, Tooltip, TooltipProvider } from 'folds';
import React, { useState } from 'react';
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
import React, { useCallback } from 'react';
import { StatusDivider } from './components';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
type MicrophoneButtonProps = {
enabled: boolean;
@ -104,9 +105,11 @@ function VideoButton({ enabled, onToggle }: VideoButtonProps) {
);
}
function ScreenShareButton() {
const [enabled, setEnabled] = useState(false);
type ScreenShareButtonProps = {
enabled: boolean;
onToggle: () => void;
};
function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
return (
<TooltipProvider
position="Top"
@ -123,7 +126,7 @@ function ScreenShareButton() {
fill="Soft"
radii="300"
size="300"
onClick={() => setEnabled(!enabled)}
onClick={onToggle}
outlined
>
<Icon size="100" src={Icons.ScreenShare} filled={enabled} />
@ -133,8 +136,14 @@ function ScreenShareButton() {
);
}
export function CallControl({ callEmbed }: { callEmbed: CallEmbed }) {
const { microphone, video, sound } = useCallControlState(callEmbed.control);
export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; compact: boolean }) {
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 (
<Box shrink="No" alignItems="Center" gap="300">
@ -144,21 +153,36 @@ export function CallControl({ callEmbed }: { callEmbed: CallEmbed }) {
onToggle={() => callEmbed.control.toggleMicrophone()}
/>
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
{!compact && <StatusDivider />}
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
{false && <ScreenShareButton />}
{!compact && (
<ScreenShareButton
enabled={screenshare}
onToggle={() => callEmbed.control.toggleScreenshare()}
/>
)}
</Box>
<StatusDivider />
<Chip
variant="Critical"
radii="300"
radii="Pill"
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
onClick={() => callEmbed.hangup()}
onClick={hangup}
>
<Text as="span" size="L400">
End
</Text>
{!compact && (
<Text as="span" size="L400">
End
</Text>
)}
</Chip>
</Box>
);

View file

@ -47,18 +47,17 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
) : (
<Spinner variant="Secondary" size="200" />
)}
<Box
grow="Yes"
alignItems="Center"
gap="Inherit"
justifyContent={compact ? 'Center' : undefined}
>
<CallRoomName room={room} />
{speakers.size > 0 && !compact && (
<Box grow="Yes" alignItems="Center" gap="Inherit">
{!compact && (
<>
<StatusDivider />
<span data-spacing-node />
<MemberSpeaking room={room} speakers={speakers} />
<CallRoomName room={room} />
{speakers.size > 0 && (
<>
<StatusDivider />
<span data-spacing-node />
<MemberSpeaking room={room} speakers={speakers} />
</>
)}
</>
)}
</Box>
@ -69,8 +68,13 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
)}
</Box>
{memberVisible && !compact && <StatusDivider />}
<Box shrink="No" alignItems="Center" justifyContent="Center" gap="Inherit">
<CallControl callEmbed={callEmbed} />
<Box shrink="No" alignItems="Center" gap="Inherit">
{compact && (
<Box grow="Yes">
<CallRoomName room={room} />
</Box>
)}
<CallControl compact={compact} callEmbed={callEmbed} />
</Box>
</Box>
);

View file

@ -128,7 +128,7 @@ export function LiveChip({ count, room, members }: LiveChipProps) {
radii="Pill"
onClick={handleOpenMenu}
>
<Text className={css.LiveChipText} as="span" size="L400">
<Text className={css.LiveChipText} as="span" size="L400" truncate>
{count} Live
</Text>
</Chip>

View file

@ -14,7 +14,7 @@ export function MemberSpeaking({ room, speakers }: MemberSpeakingProps) {
);
return (
<Box alignItems="Center" gap="100">
<Icon size="100" src={Icons.Phone} filled />
<Icon size="100" src={Icons.Mic} filled />
<Text size="T200" truncate>
{speakingNames.length === 1 && (
<>