feat: Add option to start video call in DM (#2745)
* add option to start video all in DM * show speaker icon for dm's in call status name * show call view if call is active in room * add Atria call ringtone * update element call and widget api * add option to start voice/video call in dms * only show call button if user have permission * allow call widget to send call notification event * show incoming call dialog and play sound * fix call permission checks * allow option to start call in all rooms * send notification when starting call in non-voice rooms * hide header call button from voice rooms * prevent call join if call not supported and started by other party * update call menu style * show call not supported message on incoming call notification * improve the incoming call layout * video call with right click without opening menu * allow call widget to fetch media url * add webRTC missing error * improve call permission label --------- Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { Chip, Text } from 'folds';
|
||||
import { Chip, Icon, Icons, Text } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useRoomName } from '../../hooks/useRoomMeta';
|
||||
import { RoomIcon } from '../../components/room-avatar';
|
||||
@@ -38,7 +38,11 @@ export function CallRoomName({ room }: CallRoomNameProps) {
|
||||
variant="Background"
|
||||
radii="Pill"
|
||||
before={
|
||||
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled />
|
||||
dm ? (
|
||||
<Icon size="200" src={Icons.VolumeHigh} filled />
|
||||
) : (
|
||||
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled />
|
||||
)
|
||||
}
|
||||
onClick={() => navigateRoom(room.roomId)}
|
||||
>
|
||||
|
||||
@@ -14,11 +14,20 @@ import { CallMemberRenderer } from './CallMemberCard';
|
||||
import * as css from './styles.css';
|
||||
import { CallControls } from './CallControls';
|
||||
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
|
||||
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.
|
||||
Your homeserver does not support calling.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function WebRTCMissingError() {
|
||||
return (
|
||||
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
|
||||
Your browser does not support WebRTC, which is required for calling.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -26,16 +35,22 @@ function LivekitServerMissingMessage() {
|
||||
function JoinMessage({
|
||||
hasParticipant,
|
||||
livekitSupported,
|
||||
rtcSupported,
|
||||
}: {
|
||||
hasParticipant?: boolean;
|
||||
livekitSupported?: boolean;
|
||||
rtcSupported?: boolean;
|
||||
}) {
|
||||
if (hasParticipant) return null;
|
||||
if (rtcSupported === false) {
|
||||
return <WebRTCMissingError />;
|
||||
}
|
||||
|
||||
if (livekitSupported === false) {
|
||||
return <LivekitServerMissingMessage />;
|
||||
}
|
||||
|
||||
if (hasParticipant) return null;
|
||||
|
||||
return (
|
||||
<Text style={{ margin: 'auto' }} size="L400" align="Center">
|
||||
Voice chat’s empty — Be the first to hop in!
|
||||
@@ -63,12 +78,16 @@ function CallPrescreen() {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const livekitSupported = useLivekitSupport();
|
||||
const rtcSupported = webRTCSupported();
|
||||
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
const hasPermission = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId());
|
||||
const hasPermission = permissions.stateEvent(
|
||||
StateEvent.GroupCallMemberPrefix,
|
||||
mx.getSafeUserId()
|
||||
);
|
||||
|
||||
const callSession = useCallSession(room);
|
||||
const callMembers = useCallMembers(room, callSession);
|
||||
@@ -77,7 +96,7 @@ function CallPrescreen() {
|
||||
const callEmbed = useCallEmbed();
|
||||
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
|
||||
|
||||
const canJoin = hasPermission && (livekitSupported || hasParticipant);
|
||||
const canJoin = hasPermission && livekitSupported && rtcSupported;
|
||||
|
||||
return (
|
||||
<Scroll variant="Surface" hideTrack>
|
||||
@@ -100,7 +119,11 @@ function CallPrescreen() {
|
||||
<Box className={css.PrescreenMessage} alignItems="Center">
|
||||
{!inOtherCall &&
|
||||
(hasPermission ? (
|
||||
<JoinMessage hasParticipant={hasParticipant} livekitSupported={livekitSupported} />
|
||||
<JoinMessage
|
||||
hasParticipant={hasParticipant}
|
||||
livekitSupported={livekitSupported}
|
||||
rtcSupported={rtcSupported}
|
||||
/>
|
||||
) : (
|
||||
<NoPermissionMessage />
|
||||
))}
|
||||
|
||||
@@ -60,6 +60,7 @@ import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
|
||||
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
|
||||
import { livekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
|
||||
type RoomNavItemMenuProps = {
|
||||
room: Room;
|
||||
@@ -293,13 +294,13 @@ export function RoomNavItem({
|
||||
const creators = getRoomCreatorsForRoomId(mx, room.roomId);
|
||||
const permissions = getRoomPermissionsAPI(creators, powerLevels);
|
||||
|
||||
const hasCallPermission = permissions.event(
|
||||
const hasCallPermission = permissions.stateEvent(
|
||||
StateEvent.GroupCallMemberPrefix,
|
||||
mx.getSafeUserId()
|
||||
);
|
||||
|
||||
// Do not join if missing permissions or no livekit support and call is not started by others
|
||||
if (!hasCallPermission || (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0)) {
|
||||
// Do not join if missing permissions or no livekit support or no webRTC support
|
||||
if (!hasCallPermission || !livekitSupport(autoDiscoveryInfo) || !webRTCSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -378,7 +379,7 @@ export function RoomNavItem({
|
||||
aria-label={notificationMode}
|
||||
/>
|
||||
)}
|
||||
{room.isCallRoom() && callMembers.length > 0 && (
|
||||
{callMembers.length > 0 && (
|
||||
<Badge variant="Critical" fill="Solid" size="400">
|
||||
<Text as="span" size="L400" truncate>
|
||||
{callMembers.length} Live
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
|
||||
|
||||
const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
|
||||
const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
|
||||
const permissionGroups = usePermissionGroups(room.isCallRoom());
|
||||
const permissionGroups = usePermissionGroups();
|
||||
|
||||
const [powerEditor, setPowerEditor] = useState(false);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
import { PermissionGroup } from '../../common-settings/permissions';
|
||||
|
||||
export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
|
||||
export const usePermissionGroups = (): PermissionGroup[] => {
|
||||
const groups: PermissionGroup[] = useMemo(() => {
|
||||
const messagesGroup: PermissionGroup = {
|
||||
name: 'Messages',
|
||||
@@ -54,7 +54,7 @@ export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
|
||||
state: true,
|
||||
key: StateEvent.GroupCallMemberPrefix,
|
||||
},
|
||||
name: 'Join Call',
|
||||
name: 'Start or Join Call',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -216,13 +216,13 @@ export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
|
||||
|
||||
return [
|
||||
messagesGroup,
|
||||
...(isCallRoom ? [callSettingsGroup] : []),
|
||||
callSettingsGroup,
|
||||
moderationGroup,
|
||||
roomOverviewGroup,
|
||||
roomSettingsGroup,
|
||||
otherSettingsGroup,
|
||||
];
|
||||
}, [isCallRoom]);
|
||||
}, []);
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
@@ -18,12 +18,18 @@ import { CallView } from '../call/CallView';
|
||||
import { RoomViewHeader } from './RoomViewHeader';
|
||||
import { callChatAtom } from '../../state/callEmbed';
|
||||
import { CallChatView } from './CallChatView';
|
||||
import { useCallEmbed } from '../../hooks/useCallEmbed';
|
||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||
|
||||
export function Room() {
|
||||
const { eventId } = useParams();
|
||||
const room = useRoom();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const callSession = useCallSession(room);
|
||||
const callMembers = useCallMembers(room, callSession);
|
||||
const callEmbed = useCallEmbed();
|
||||
|
||||
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const screenSize = useScreenSizeContext();
|
||||
@@ -43,7 +49,7 @@ export function Room() {
|
||||
)
|
||||
);
|
||||
|
||||
const callView = room.isCallRoom();
|
||||
const callView = callEmbed?.roomId === room.roomId || room.isCallRoom() || callMembers.length > 0;
|
||||
|
||||
return (
|
||||
<PowerLevelsContextProvider value={powerLevels}>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
RectCords,
|
||||
Badge,
|
||||
Spinner,
|
||||
Button,
|
||||
} from 'folds';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
@@ -68,6 +69,9 @@ import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { RoomSettingsPage } from '../../state/roomSettings';
|
||||
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
||||
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
|
||||
type RoomMenuProps = {
|
||||
room: Room;
|
||||
@@ -253,6 +257,132 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
||||
);
|
||||
});
|
||||
|
||||
type CallMenuProps = {
|
||||
onVoiceCall: () => void;
|
||||
onVideoCall: () => void;
|
||||
requestClose: () => void;
|
||||
};
|
||||
const CallMenu = forwardRef<HTMLDivElement, CallMenuProps>(
|
||||
({ requestClose, onVoiceCall, onVideoCall }, ref) => {
|
||||
const handleVoice = () => {
|
||||
onVoiceCall();
|
||||
requestClose();
|
||||
};
|
||||
const handleVideo = () => {
|
||||
onVideoCall();
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ padding: config.space.S200, minWidth: toRem(150) }}>
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="L400">Start Call</Text>
|
||||
<Box direction="Column" gap="200">
|
||||
<Button
|
||||
size="300"
|
||||
variant="Success"
|
||||
fill="Soft"
|
||||
outlined
|
||||
radii="300"
|
||||
before={<Icon size="100" src={Icons.Phone} filled />}
|
||||
onClick={handleVoice}
|
||||
>
|
||||
<Text size="B300">Voice</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Success"
|
||||
radii="300"
|
||||
before={<Icon size="100" src={Icons.VideoCamera} filled />}
|
||||
onClick={handleVideo}
|
||||
>
|
||||
<Text size="B300">Video</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function CallButton() {
|
||||
const room = useRoom();
|
||||
const direct = useIsDirectRoom();
|
||||
|
||||
const callEmbed = useCallEmbed();
|
||||
const startCall = useCallStart(direct);
|
||||
const callStarted = callEmbed && callEmbed.roomId === room.roomId;
|
||||
const inAnotherCall = callEmbed && !callStarted;
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
{inAnotherCall ? (
|
||||
<Text size="L400">Already in another call — End the current call to join!</Text>
|
||||
) : (
|
||||
<Text>Call</Text>
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
ref={triggerRef}
|
||||
onClick={handleOpenMenu}
|
||||
onContextMenu={(evt) => {
|
||||
evt.preventDefault();
|
||||
startCall(room, {
|
||||
microphone: true,
|
||||
video: true,
|
||||
sound: true,
|
||||
});
|
||||
}}
|
||||
disabled={inAnotherCall || callStarted}
|
||||
aria-pressed={!!menuAnchor}
|
||||
>
|
||||
<Icon size="400" src={Icons.VideoCamera} filled={!!menuAnchor} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
position="Bottom"
|
||||
align="Center"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => setMenuAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<CallMenu
|
||||
onVideoCall={() => startCall(room, { microphone: true, video: true, sound: true })}
|
||||
onVoiceCall={() => startCall(room, { microphone: true, video: false, sound: true })}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const mx = useMatrixClient();
|
||||
@@ -260,6 +390,17 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
const screenSize = useScreenSizeContext();
|
||||
const room = useRoom();
|
||||
const space = useSpaceOptionally();
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const creators = useRoomCreators(room);
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
|
||||
const hasCallPermission = permissions.stateEvent(
|
||||
StateEvent.GroupCallMemberPrefix,
|
||||
mx.getSafeUserId()
|
||||
);
|
||||
const livekitSupported = useLivekitSupport();
|
||||
const rtcSupported = webRTCSupported();
|
||||
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
|
||||
const direct = useIsDirectRoom();
|
||||
@@ -453,7 +594,9 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
|
||||
{!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission && (
|
||||
<CallButton />
|
||||
)}
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
|
||||
Reference in New Issue
Block a user