Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/a_dm_and_room_calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Added ability to start calls in DMs and rooms. DM calls will trigger a notification popup & ringtone (for other sable users/compatible clients, probably).
Binary file added public/sound/ringtone.webm
Binary file not shown.
31 changes: 21 additions & 10 deletions src/app/components/CallEmbedProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { ReactNode, useCallback, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { useAutoJoinCall } from '$hooks/useAutoJoinCall';
import {
CallEmbedContextProvider,
CallEmbedRefContextProvider,
useCallHangupEvent,
useCallJoined,
useCallThemeSync,
useCallMemberSoundSync,
} from '../hooks/useCallEmbed';
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
} from '$hooks/useCallEmbed';
import { callChatAtom, callEmbedAtom } from '$state/callEmbed';
import { CallEmbed } from '$plugins/call';
import { useSelectedRoom } from '$hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize';
import { IncomingCallModal } from './IncomingCallModal';

function CallUtils({ embed }: { embed: CallEmbed }) {
const setCallEmbed = useSetAtom(callEmbedAtom);
Expand All @@ -31,6 +33,12 @@ function CallUtils({ embed }: { embed: CallEmbed }) {
type CallEmbedProviderProps = {
children?: ReactNode;
};

function AutoJoinManager() {
useAutoJoinCall();
return null;
}

export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
const callEmbed = useAtomValue(callEmbedAtom);
const callEmbedRef = useRef<HTMLDivElement>(null);
Expand All @@ -46,17 +54,20 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {

return (
<CallEmbedContextProvider value={callEmbed}>
<IncomingCallModal />
{callEmbed && <CallUtils embed={callEmbed} />}
<CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider>
<CallEmbedRefContextProvider value={callEmbedRef}>
<AutoJoinManager />
{children}
</CallEmbedRefContextProvider>

<div
data-call-embed-container
style={{
visibility: callVisible ? undefined : 'hidden',
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '50%',
zIndex: callVisible ? 10 : -1,
pointerEvents: callVisible ? 'all' : 'none',
}}
ref={callEmbedRef}
/>
Expand Down
143 changes: 143 additions & 0 deletions src/app/components/IncomingCallModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
Box,
Dialog,
Header,
IconButton,
Icon,
Icons,
Text,
Button,
Avatar,
config,
Overlay,
OverlayCenter,
OverlayBackdrop,
} from 'folds';
import { useMatrixClient } from '$hooks/useMatrixClient';
import { useRoomName } from '$hooks/useRoomMeta';
import { getRoomAvatarUrl } from '$utils/room';
import { useRoomNavigate } from '$hooks/useRoomNavigate';
import FocusTrap from 'focus-trap-react';
import { stopPropagation } from '$utils/keyboard';
import { useAtom, useSetAtom } from 'jotai';
import {
autoJoinCallIntentAtom,
incomingCallRoomIdAtom,
mutedCallRoomIdAtom,
} from '$state/callEmbed';
import { RoomAvatar } from './room-avatar';

type IncomingCallInternalProps = {
room: any;
onClose: () => void;
};

export function IncomingCallInternal({ room, onClose }: IncomingCallInternalProps) {
const mx = useMatrixClient();
const roomName = useRoomName(room);
const { navigateRoom } = useRoomNavigate();
const avatarUrl = getRoomAvatarUrl(mx, room, 96);
const setAutoJoinIntent = useSetAtom(autoJoinCallIntentAtom);
const setMutedRoomId = useSetAtom(mutedCallRoomIdAtom);

const handleAnswer = () => {
setMutedRoomId(room.roomId);
setAutoJoinIntent(room.roomId);
onClose();
navigateRoom(room.roomId);
};

const handleDecline = async () => {
setMutedRoomId(room.roomId);
onClose();
};

return (
<Dialog variant="Surface" style={{ width: '340px' }}>
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Incoming Call</Text>
</Box>
<IconButton size="300" onClick={onClose} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>

<Box style={{ padding: config.space.S600 }} direction="Column" alignItems="Center" gap="500">
<Avatar size="500">
<RoomAvatar
roomId={room.roomId}
src={avatarUrl ?? undefined}
alt={roomName}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>

<Box direction="Column" alignItems="Center" gap="100">
<Text size="L400" align="Center" truncate>
{roomName}
</Text>
<Text priority="400" size="T300" align="Center">
Incoming voice chat request
</Text>
</Box>

<Box gap="300" style={{ width: '100%' }} justifyContent="Center">
<Button
variant="Critical"
fill="Soft"
style={{ minWidth: '110px' }}
onClick={handleDecline}
>
<Text size="B400">Decline</Text>
</Button>
<Button
fill="Solid"
variant="Primary"
style={{ minWidth: '110px' }}
onClick={handleAnswer}
before={<Icon size="100" src={Icons.Phone} />}
>
<Text size="B400">Answer</Text>
</Button>
</Box>
</Box>
</Dialog>
);
}

export function IncomingCallModal() {
const [ringingRoomId, setRingingRoomId] = useAtom(incomingCallRoomIdAtom);
const mx = useMatrixClient();
const room = ringingRoomId ? mx.getRoom(ringingRoomId) : null;

if (!ringingRoomId || !room) return null;

const close = () => setRingingRoomId(null);

return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: close,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<div>
<IncomingCallInternal room={room} onClose={close} />
</div>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}
Loading
Loading