dm calls mvp: phase 1: outgoing DM voice call via element-call voiceOnly intent
This commit is contained in:
parent
755453ecda
commit
c6d59a3852
9 changed files with 148 additions and 26 deletions
|
|
@ -382,6 +382,12 @@
|
|||
"rate_limited": "Server rate-limited your request for {{minutes}} minutes!",
|
||||
"create": "Create"
|
||||
},
|
||||
"Call": {
|
||||
"start": "Start call",
|
||||
"join": "Join call",
|
||||
"unavailable": "Calls are unavailable",
|
||||
"busy_other_room": "You are already in a call"
|
||||
},
|
||||
"Room": {
|
||||
"new_messages": "New Messages",
|
||||
"jump_to_unread": "Jump to Unread",
|
||||
|
|
|
|||
|
|
@ -382,6 +382,12 @@
|
|||
"rate_limited": "Сервер ограничил частоту запросов на {{minutes}} мин.!",
|
||||
"create": "Создать"
|
||||
},
|
||||
"Call": {
|
||||
"start": "Позвонить",
|
||||
"join": "Присоединиться",
|
||||
"unavailable": "Звонки недоступны",
|
||||
"busy_other_room": "Вы уже в другом звонке"
|
||||
},
|
||||
"Room": {
|
||||
"new_messages": "Новые сообщения",
|
||||
"jump_to_unread": "К непрочитанным",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { ReactNode, useCallback, useRef } from 'react';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { config } from 'folds';
|
||||
import {
|
||||
CallEmbedContextProvider,
|
||||
CallEmbedRefContextProvider,
|
||||
|
|
@ -43,7 +42,12 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||
|
||||
const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
|
||||
|
||||
const callVisible = callEmbed && selectedRoom === callEmbed.roomId && joined && !chatOnlyView;
|
||||
const callVisible =
|
||||
callEmbed &&
|
||||
!callEmbed.voiceOnly &&
|
||||
selectedRoom === callEmbed.roomId &&
|
||||
joined &&
|
||||
!chatOnlyView;
|
||||
|
||||
return (
|
||||
<CallEmbedContextProvider value={callEmbed}>
|
||||
|
|
@ -53,6 +57,7 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||
data-call-embed-container
|
||||
style={{
|
||||
visibility: callVisible ? undefined : 'hidden',
|
||||
pointerEvents: callVisible ? undefined : 'none',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
|
|
|
|||
|
|
@ -185,18 +185,22 @@ export function CallControl({
|
|||
onToggle={() => callEmbed.control.toggleSound()}
|
||||
disabled={!callJoined}
|
||||
/>
|
||||
{!compact && <StatusDivider />}
|
||||
<VideoButton
|
||||
enabled={video}
|
||||
onToggle={() => callEmbed.control.toggleVideo()}
|
||||
disabled={!callJoined}
|
||||
/>
|
||||
{!compact && (
|
||||
<ScreenShareButton
|
||||
enabled={screenshare}
|
||||
onToggle={() => callEmbed.control.toggleScreenshare()}
|
||||
disabled={!callJoined}
|
||||
/>
|
||||
{!callEmbed.voiceOnly && (
|
||||
<>
|
||||
{!compact && <StatusDivider />}
|
||||
<VideoButton
|
||||
enabled={video}
|
||||
onToggle={() => callEmbed.control.toggleVideo()}
|
||||
disabled={!callJoined}
|
||||
/>
|
||||
{!compact && (
|
||||
<ScreenShareButton
|
||||
enabled={screenshare}
|
||||
onToggle={() => callEmbed.control.toggleScreenshare()}
|
||||
disabled={!callJoined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<StatusDivider />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { MouseEventHandler, forwardRef, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -69,6 +70,10 @@ import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
|||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { RoomSettingsPage } from '../../state/roomSettings';
|
||||
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||
import { useDmCallStart } from '../../hooks/useDmCallStart';
|
||||
import { callEmbedAtom } from '../../state/callEmbed';
|
||||
|
||||
type RoomMenuProps = {
|
||||
room: Room;
|
||||
|
|
@ -255,6 +260,59 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||
);
|
||||
});
|
||||
|
||||
function DmCallButton({ room }: { room: Room }) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const startDmCall = useDmCallStart();
|
||||
const livekitSupported = useLivekitSupport();
|
||||
const session = useCallSession(room);
|
||||
const members = useCallMembers(room, session);
|
||||
const currentEmbed = useAtomValue(callEmbedAtom);
|
||||
|
||||
const myUserId = mx.getSafeUserId();
|
||||
const inCallHere = currentEmbed?.roomId === room.roomId;
|
||||
if (inCallHere) return null;
|
||||
|
||||
const inCallElsewhere = !!currentEmbed && currentEmbed.roomId !== room.roomId;
|
||||
const ongoingByOthers = members.length > 0 && !members.some((m) => m.sender === myUserId);
|
||||
|
||||
const disabled = !livekitSupported || inCallElsewhere;
|
||||
let tooltipText: string;
|
||||
if (!livekitSupported) tooltipText = t('Call.unavailable');
|
||||
else if (inCallElsewhere) tooltipText = t('Call.busy_other_room');
|
||||
else if (ongoingByOthers) tooltipText = t('Call.join');
|
||||
else tooltipText = t('Call.start');
|
||||
|
||||
const handleClick = () => {
|
||||
if (disabled) return;
|
||||
startDmCall(room.roomId);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>{tooltipText}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
fill="None"
|
||||
ref={triggerRef}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
aria-label={tooltipText}
|
||||
>
|
||||
<Icon size="400" src={Icons.Phone} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -382,6 +440,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||
</Box>
|
||||
|
||||
<Box shrink="No">
|
||||
{direct && <DmCallButton room={room} />}
|
||||
{!encryptedRoom && (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
|
|
|
|||
|
|
@ -42,22 +42,25 @@ export const createCallEmbed = (
|
|||
dm: boolean,
|
||||
themeKind: ElementCallThemeKind,
|
||||
container: HTMLElement,
|
||||
pref?: CallPreferences
|
||||
pref?: CallPreferences,
|
||||
voiceOnly = false
|
||||
): CallEmbed => {
|
||||
const rtcSession = mx.matrixRTC.getRoomSession(room);
|
||||
const ongoing =
|
||||
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0;
|
||||
|
||||
const intent = CallEmbed.getIntent(dm, ongoing);
|
||||
const intent = CallEmbed.getIntent(dm, ongoing, voiceOnly);
|
||||
const widget = CallEmbed.getWidget(mx, room, intent, themeKind);
|
||||
const controlState = pref && new CallControlState(pref.microphone, pref.video, pref.sound);
|
||||
const controlState =
|
||||
pref &&
|
||||
new CallControlState(pref.microphone, voiceOnly ? false : pref.video, pref.sound);
|
||||
|
||||
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
||||
const embed = new CallEmbed(mx, room, widget, container, controlState, voiceOnly);
|
||||
|
||||
return embed;
|
||||
};
|
||||
|
||||
export const useCallStart = (dm = false) => {
|
||||
export const useCallStart = (dm = false, voiceOnly = false) => {
|
||||
const mx = useMatrixClient();
|
||||
const theme = useTheme();
|
||||
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||
|
|
@ -69,11 +72,11 @@ export const useCallStart = (dm = false) => {
|
|||
if (!container) {
|
||||
throw new Error('Failed to start call, No embed container element found!');
|
||||
}
|
||||
const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref);
|
||||
const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref, voiceOnly);
|
||||
|
||||
setCallEmbed(callEmbed);
|
||||
},
|
||||
[mx, dm, theme, setCallEmbed, callEmbedRef]
|
||||
[mx, dm, voiceOnly, theme, setCallEmbed, callEmbedRef]
|
||||
);
|
||||
|
||||
return startCall;
|
||||
|
|
|
|||
24
src/app/hooks/useDmCallStart.ts
Normal file
24
src/app/hooks/useDmCallStart.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useCallStart } from './useCallEmbed';
|
||||
import { useCallPreferencesAtom } from '../state/hooks/callPreferences';
|
||||
|
||||
export const useDmCallStart = () => {
|
||||
const mx = useMatrixClient();
|
||||
const startCall = useCallStart(true, true);
|
||||
const callPref = useAtomValue(useCallPreferencesAtom());
|
||||
|
||||
return useCallback(
|
||||
(roomId: string) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[dm-call] room not found', roomId);
|
||||
return;
|
||||
}
|
||||
startCall(room, callPref);
|
||||
},
|
||||
[mx, startCall, callPref]
|
||||
);
|
||||
};
|
||||
|
|
@ -12,7 +12,12 @@ export function CallStatusRenderer() {
|
|||
|
||||
if (!callEmbed) return null;
|
||||
|
||||
if (screenSize === ScreenSize.Mobile && callEmbed.roomId === selectedRoom) return null;
|
||||
if (
|
||||
screenSize === ScreenSize.Mobile &&
|
||||
callEmbed.roomId === selectedRoom &&
|
||||
!callEmbed.voiceOnly
|
||||
)
|
||||
return null;
|
||||
|
||||
return <CallStatus callEmbed={callEmbed} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ export class CallEmbed {
|
|||
|
||||
public joined = false;
|
||||
|
||||
public readonly voiceOnly: boolean;
|
||||
|
||||
public readonly control: CallControl;
|
||||
|
||||
private readonly container: HTMLElement;
|
||||
|
|
@ -55,12 +57,18 @@ export class CallEmbed {
|
|||
|
||||
private readonly boundOnToDeviceEvent = this.onToDeviceEvent.bind(this);
|
||||
|
||||
static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent {
|
||||
static getIntent(dm: boolean, ongoing: boolean, voiceOnly = false): ElementCallIntent {
|
||||
if (ongoing) {
|
||||
return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting;
|
||||
if (dm) {
|
||||
return voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM;
|
||||
}
|
||||
return ElementCallIntent.JoinExisting;
|
||||
}
|
||||
|
||||
return dm ? ElementCallIntent.StartCallDM : ElementCallIntent.StartCall;
|
||||
if (dm) {
|
||||
return voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM;
|
||||
}
|
||||
return ElementCallIntent.StartCall;
|
||||
}
|
||||
|
||||
static getWidget(
|
||||
|
|
@ -133,7 +141,8 @@ export class CallEmbed {
|
|||
room: Room,
|
||||
widget: Widget,
|
||||
container: HTMLElement,
|
||||
initialControlState?: CallControlState
|
||||
initialControlState?: CallControlState,
|
||||
voiceOnly = false
|
||||
) {
|
||||
const iframe = CallEmbed.getIframe(
|
||||
widget.getCompleteUrl({ currentUserId: mx.getSafeUserId() })
|
||||
|
|
@ -148,6 +157,7 @@ export class CallEmbed {
|
|||
this.room = room;
|
||||
this.iframe = iframe;
|
||||
this.container = container;
|
||||
this.voiceOnly = voiceOnly;
|
||||
|
||||
const controlState = initialControlState ?? new CallControlState(true, false, true);
|
||||
this.control = new CallControl(controlState, call, iframe);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue