Merge branch 'dev' into dm-calls

This commit is contained in:
Ajay Bura
2026-03-13 02:03:12 +11:00
committed by GitHub
24 changed files with 318 additions and 132 deletions
+45 -8
View File
@@ -1,14 +1,17 @@
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
import React, { useCallback } from 'react';
import { useSetAtom } from 'jotai';
import { StatusDivider } from './components';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { callEmbedAtom } from '../../state/callEmbed';
type MicrophoneButtonProps = {
enabled: boolean;
onToggle: () => Promise<unknown>;
disabled?: boolean;
};
function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
return (
<TooltipProvider
position="Top"
@@ -27,6 +30,7 @@ function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon size="100" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
</IconButton>
@@ -38,8 +42,9 @@ function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
type SoundButtonProps = {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
};
function SoundButton({ enabled, onToggle }: SoundButtonProps) {
function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
return (
<TooltipProvider
position="Top"
@@ -58,6 +63,7 @@ function SoundButton({ enabled, onToggle }: SoundButtonProps) {
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon
size="100"
@@ -73,8 +79,9 @@ function SoundButton({ enabled, onToggle }: SoundButtonProps) {
type VideoButtonProps = {
enabled: boolean;
onToggle: () => Promise<unknown>;
disabled?: boolean;
};
function VideoButton({ enabled, onToggle }: VideoButtonProps) {
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
return (
<TooltipProvider
position="Top"
@@ -93,6 +100,7 @@ function VideoButton({ enabled, onToggle }: VideoButtonProps) {
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon
size="100"
@@ -108,8 +116,9 @@ function VideoButton({ enabled, onToggle }: VideoButtonProps) {
type ScreenShareButtonProps = {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
};
function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
return (
<TooltipProvider
position="Top"
@@ -128,6 +137,7 @@ function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
size="300"
onClick={onToggle}
outlined
disabled={disabled}
>
<Icon size="100" src={Icons.ScreenShare} filled={enabled} />
</IconButton>
@@ -136,8 +146,17 @@ function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
);
}
export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; compact: boolean }) {
export function CallControl({
callEmbed,
compact,
callJoined,
}: {
callEmbed: CallEmbed;
compact: boolean;
callJoined: boolean;
}) {
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
const setCallEmbed = useSetAtom(callEmbedAtom);
const [hangupState, hangup] = useAsyncCallback(
useCallback(() => callEmbed.hangup(), [callEmbed])
@@ -145,20 +164,38 @@ export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; comp
const exiting =
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
const handleHangup = () => {
if (!callJoined) {
setCallEmbed(undefined);
return;
}
hangup();
};
return (
<Box shrink="No" alignItems="Center" gap="300">
<Box alignItems="Inherit" gap="200">
<MicrophoneButton
enabled={microphone}
onToggle={() => callEmbed.control.toggleMicrophone()}
disabled={!callJoined}
/>
<SoundButton
enabled={sound}
onToggle={() => callEmbed.control.toggleSound()}
disabled={!callJoined}
/>
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
{!compact && <StatusDivider />}
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
<VideoButton
enabled={video}
onToggle={() => callEmbed.control.toggleVideo()}
disabled={!callJoined}
/>
{!compact && (
<ScreenShareButton
enabled={screenshare}
onToggle={() => callEmbed.control.toggleScreenshare()}
disabled={!callJoined}
/>
)}
</Box>
@@ -176,7 +213,7 @@ export function CallControl({ callEmbed, compact }: { callEmbed: CallEmbed; comp
}
disabled={exiting}
outlined
onClick={hangup}
onClick={handleHangup}
>
{!compact && (
<Text as="span" size="L400">
+1 -1
View File
@@ -74,7 +74,7 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
<CallRoomName room={room} />
</Box>
)}
<CallControl compact={compact} callEmbed={callEmbed} />
<CallControl callJoined={callJoined} compact={compact} callEmbed={callEmbed} />
</Box>
</Box>
);
+31 -5
View File
@@ -13,10 +13,29 @@ import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { CallMemberRenderer } from './CallMemberCard';
import * as css from './styles.css';
import { CallControls } from './CallControls';
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
function JoinMessage({ hasParticipant }: { hasParticipant?: boolean }) {
function LivekitServerMissingMessage() {
return (
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
Your homeserver does not support calling. But you can still join call started by others.
</Text>
);
}
function JoinMessage({
hasParticipant,
livekitSupported,
}: {
hasParticipant?: boolean;
livekitSupported?: boolean;
}) {
if (hasParticipant) return null;
if (livekitSupported === false) {
return <LivekitServerMissingMessage />;
}
return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
Voice chats empty Be the first to hop in!
@@ -43,12 +62,13 @@ function AlreadyInCallMessage() {
function CallPrescreen() {
const mx = useMatrixClient();
const room = useRoom();
const livekitSupported = useLivekitSupport();
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const canJoin = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId());
const hasPermission = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId());
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
@@ -57,6 +77,8 @@ function CallPrescreen() {
const callEmbed = useCallEmbed();
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
const canJoin = hasPermission && (livekitSupported || hasParticipant);
return (
<Scroll variant="Surface" hideTrack>
<Box className={css.CallViewContent} alignItems="Center" justifyContent="Center">
@@ -75,11 +97,15 @@ function CallPrescreen() {
)}
<CallMemberRenderer members={callMembers} />
<PrescreenControls canJoin={canJoin} />
<Header size="300">
<Box className={css.PrescreenMessage} alignItems="Center">
{!inOtherCall &&
(canJoin ? <JoinMessage hasParticipant={hasParticipant} /> : <NoPermissionMessage />)}
(hasPermission ? (
<JoinMessage hasParticipant={hasParticipant} livekitSupported={livekitSupported} />
) : (
<NoPermissionMessage />
))}
{inOtherCall && <AlreadyInCallMessage />}
</Header>
</Box>
</Box>
</Box>
</Scroll>
+4
View File
@@ -22,3 +22,7 @@ export const CallMemberCard = style({
export const CallControlContainer = style({
padding: config.space.S400,
});
export const PrescreenMessage = style({
padding: config.space.S200,
});
@@ -57,6 +57,8 @@ import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
import { callChatAtom } from '../../state/callEmbed';
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
import { livekitSupport } from '../../hooks/useLivekitSupport';
type RoomNavItemMenuProps = {
room: Room;
@@ -282,8 +284,14 @@ export function RoomNavItem({
const startCall = useCallStart(direct);
const callEmbed = useCallEmbed();
const callPref = useAtomValue(useCallPreferencesAtom());
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
// Do not join if no livekit support or call is not started by others
if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) {
return;
}
// Do not join if already in call
if (callEmbed) {
return;
+1 -1
View File
@@ -221,7 +221,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const isComposing = useComposingCheck();
useElementSizeObserver(
useCallback(() => document.body, []),
useCallback(() => fileDropContainerRef.current, [fileDropContainerRef]),
useCallback((width) => setHideStickerBtn(width < 500), [])
);
+7 -1
View File
@@ -1475,7 +1475,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const senderId = mEvent.getSender() ?? '';
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const callJoined = mEvent.getContent<SessionMembershipData>().application;
const content = mEvent.getContent<SessionMembershipData>();
const prevContent = mEvent.getPrevContent();
const callJoined = content.application;
if (callJoined && 'application' in prevContent) {
return null;
}
const timeJSX = (
<Time
+5 -1
View File
@@ -492,7 +492,11 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
offset={4}
tooltip={
<Tooltip>
<Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
{callView ? (
<Text>Members</Text>
) : (
<Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
)}
</Tooltip>
}
>
+1 -1
View File
@@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
<Box direction="Column" gap="100">
<Box gap="100" alignItems="End">
<Text size="H3">Cinny</Text>
<Text size="T200">v4.10.5</Text>
<Text size="T200">v4.11.1</Text>
</Box>
<Text>Yet another matrix client.</Text>
</Box>