snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { Box, Icon } from '@rocket.chat/fuselage';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation, useSetting } from '@rocket.chat/ui-contexts';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const VideoConfBlockModal = ({ onClose, onConfirm }: { onClose: () => void; onConfirm: () => void }) => {
|
||||
const t = useTranslation();
|
||||
const workspaceUrl = useSetting('Site_Url');
|
||||
|
||||
const confirmButtonContent = (
|
||||
<Box>
|
||||
<Icon marginInlineEnd={8} size='x20' name='new-window' />
|
||||
{t('Open_call')}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onConfirm();
|
||||
onClose();
|
||||
}, [onClose, onConfirm]);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
open
|
||||
icon={null}
|
||||
variant='warning'
|
||||
title={t('Open_call_in_new_tab')}
|
||||
confirmText={confirmButtonContent}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={onClose}
|
||||
onClose={onClose}
|
||||
>
|
||||
<>
|
||||
<Box marginBlockEnd={24}>{t('Your_web_browser_blocked_Rocket_Chat_from_opening_tab')}</Box>
|
||||
<Box>
|
||||
{t('To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL')}
|
||||
<Box is='span' fontWeight={700}>
|
||||
{workspaceUrl as string}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoConfBlockModal;
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Box,
|
||||
Callout,
|
||||
Margins,
|
||||
ModalHeader,
|
||||
ModalHeaderText,
|
||||
ModalTagline,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalHeroImage,
|
||||
ModalFooter,
|
||||
ModalFooterAnnotation,
|
||||
ModalFooterControllers,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type VideoConfConfigModalProps = {
|
||||
onClose: () => void;
|
||||
onConfirm?: () => void;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
|
||||
const VideoConfConfigModal = ({ onClose, onConfirm, isAdmin }: VideoConfConfigModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<ModalHeader>
|
||||
<ModalHeaderText>
|
||||
<ModalTagline>{isAdmin ? t('Missing_configuration') : t('App_not_enabled')}</ModalTagline>
|
||||
<ModalTitle>{isAdmin ? t('Configure_video_conference') : t('Video_Conference')}</ModalTitle>
|
||||
</ModalHeaderText>
|
||||
<ModalClose title={t('Close')} onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<ModalHeroImage maxHeight='initial' src='/images/conf-call-config.svg' />
|
||||
<Box fontScale='h3'>{t('Premium_capabilities')}</Box>
|
||||
<Box withRichContent>
|
||||
<Box is='ul' paddingInlineStart={24}>
|
||||
<li>{t('Ringtones_and_visual_indicators_notify_people_of_incoming_calls')}</li>
|
||||
<li>{t('Call_history_provides_a_record_of_when_calls_took_place_and_who_joined')}</li>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box fontScale='h3'>{t('Conference_call_apps')}</Box>
|
||||
<Margins blockStart={12}>
|
||||
<Callout icon='team' title={isAdmin ? t('Jitsi_included_with_Community') : 'Jitsi'}>
|
||||
{t('Open-source_conference_call_solution')}
|
||||
</Callout>
|
||||
<Callout icon='lightning' title={t('Pexip_Premium_only')}>
|
||||
{t('A_secure_and_highly_private_self-managed_solution_for_conference_calls')}
|
||||
</Callout>
|
||||
<Callout icon='lightning' title={t('Google_Meet_Premium_only')}>
|
||||
{t('Secure_SaaS_solution')} {t('A_cloud-based_platform_for_those_needing_a_plug-and-play_app')}
|
||||
</Callout>
|
||||
</Margins>
|
||||
<Box fontScale='h3' marginBlockStart={24}>
|
||||
{t('Required_action')}
|
||||
</Box>
|
||||
<Callout marginBlockStart={12} marginBlockEnd={24} title={t('Missing_configuration')} type='warning'>
|
||||
{isAdmin
|
||||
? t('An_app_needs_to_be_installed_and_configured')
|
||||
: t('A_workspace_admin_needs_to_install_and_configure_a_conference_call_app')}
|
||||
</Callout>
|
||||
</ModalContent>
|
||||
<ModalFooter justifyContent='space-between'>
|
||||
<ModalFooterAnnotation>
|
||||
{isAdmin
|
||||
? t('Configure_video_conference_to_make_it_available_on_this_workspace')
|
||||
: t('Talk_to_your_workspace_administrator_about_enabling_video_conferencing')}
|
||||
</ModalFooterAnnotation>
|
||||
<ModalFooterControllers>
|
||||
<Button onClick={onClose}>{t('Close')}</Button>
|
||||
{onConfirm && isAdmin && (
|
||||
<Button primary onClick={onConfirm}>
|
||||
{t('Open_settings')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooterControllers>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoConfConfigModal;
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { VideoConference } from '@rocket.chat/core-typings';
|
||||
import { Box, States, StatesIcon, StatesTitle, StatesSubtitle, Throbber } from '@rocket.chat/fuselage';
|
||||
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
|
||||
import {
|
||||
VirtualizedScrollbars,
|
||||
ContextualbarHeader,
|
||||
ContextualbarIcon,
|
||||
ContextualbarTitle,
|
||||
ContextualbarClose,
|
||||
ContextualbarContent,
|
||||
ContextualbarEmptyContent,
|
||||
ContextualbarDialog,
|
||||
} from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import VideoConfListItem from './VideoConfListItem';
|
||||
import { getErrorMessage } from '../../../../../lib/errorHandling';
|
||||
|
||||
export type VideoConfListProps = {
|
||||
onClose: () => void;
|
||||
total: number;
|
||||
videoConfs: VideoConference[];
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
reload: () => void;
|
||||
loadMoreItems: () => void;
|
||||
};
|
||||
|
||||
const VideoConfList = ({ onClose, total, videoConfs, loading, error, reload, loadMoreItems }: VideoConfListProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { ref, contentBoxSize: { inlineSize = 378, blockSize = 1 } = {} } = useResizeObserver<HTMLElement>({
|
||||
debounceDelay: 200,
|
||||
});
|
||||
|
||||
return (
|
||||
<ContextualbarDialog>
|
||||
<ContextualbarHeader>
|
||||
<ContextualbarIcon name='phone' />
|
||||
<ContextualbarTitle>{t('Calls')}</ContextualbarTitle>
|
||||
<ContextualbarClose onClick={onClose} />
|
||||
</ContextualbarHeader>
|
||||
<ContextualbarContent paddingInline={0} ref={ref}>
|
||||
{loading && (
|
||||
<Box paddingInline={24} paddingBlock={12}>
|
||||
<Throbber size='x12' />
|
||||
</Box>
|
||||
)}
|
||||
{(total === 0 || error) && (
|
||||
<Box display='flex' flexDirection='column' justifyContent='center' height='100%'>
|
||||
{error && (
|
||||
<States>
|
||||
<StatesIcon name='circle-exclamation' variation='danger' />
|
||||
<StatesTitle>{t('Something_went_wrong')}</StatesTitle>
|
||||
<StatesSubtitle>{getErrorMessage(error)}</StatesSubtitle>
|
||||
</States>
|
||||
)}
|
||||
{!loading && total === 0 && (
|
||||
<ContextualbarEmptyContent
|
||||
icon='phone'
|
||||
title={t('No_history')}
|
||||
subtitle={t('There_is_no_video_conference_history_in_this_room')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<Box flexGrow={1} flexShrink={1} overflow='hidden' display='flex'>
|
||||
{videoConfs.length > 0 && (
|
||||
<VirtualizedScrollbars>
|
||||
<Virtuoso
|
||||
style={{
|
||||
height: blockSize,
|
||||
width: inlineSize,
|
||||
}}
|
||||
totalCount={total}
|
||||
endReached={loadMoreItems}
|
||||
overscan={25}
|
||||
data={videoConfs}
|
||||
itemContent={(_index, data) => <VideoConfListItem videoConfData={data} reload={reload} />}
|
||||
/>
|
||||
</VirtualizedScrollbars>
|
||||
)}
|
||||
</Box>
|
||||
</ContextualbarContent>
|
||||
</ContextualbarDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoConfList;
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { VideoConference } from '@rocket.chat/core-typings';
|
||||
import { css } from '@rocket.chat/css-in-js';
|
||||
import {
|
||||
Button,
|
||||
Message,
|
||||
MessageLeftContainer,
|
||||
MessageContainer,
|
||||
MessageHeader,
|
||||
MessageName,
|
||||
MessageTimestamp,
|
||||
MessageBody,
|
||||
MessageBlock,
|
||||
Box,
|
||||
Palette,
|
||||
IconButton,
|
||||
ButtonGroup,
|
||||
AvatarStack,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { UserAvatar } from '@rocket.chat/ui-avatar';
|
||||
import { useUserDisplayName } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from '@rocket.chat/ui-contexts';
|
||||
import { useVideoConfJoinCall } from '@rocket.chat/ui-video-conf';
|
||||
|
||||
import { useTimeAgo } from '../../../../../hooks/useTimeAgo';
|
||||
import { VIDEOCONF_STACK_MAX_USERS } from '../../../../../lib/constants';
|
||||
import { useGoToRoom } from '../../../hooks/useGoToRoom';
|
||||
|
||||
const VideoConfListItem = ({
|
||||
videoConfData,
|
||||
className = [],
|
||||
reload,
|
||||
...props
|
||||
}: {
|
||||
videoConfData: VideoConference;
|
||||
className?: string[];
|
||||
reload: () => void;
|
||||
}) => {
|
||||
const t = useTranslation();
|
||||
const formatDate = useTimeAgo();
|
||||
const joinCall = useVideoConfJoinCall();
|
||||
|
||||
const {
|
||||
_id: callId,
|
||||
createdBy: { name, username, _id },
|
||||
users,
|
||||
createdAt,
|
||||
endedAt,
|
||||
discussionRid,
|
||||
} = videoConfData;
|
||||
|
||||
const displayName = useUserDisplayName({ name, username });
|
||||
const joinedUsers = users.filter((user) => user._id !== _id);
|
||||
|
||||
const hovered = css`
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: ${Palette.surface['surface-tint']};
|
||||
.rcx-message {
|
||||
background: ${Palette.surface['surface-tint']};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const handleJoinConference = useStableCallback((): void => {
|
||||
joinCall(callId);
|
||||
return reload();
|
||||
});
|
||||
|
||||
const goToRoom = useGoToRoom();
|
||||
|
||||
return (
|
||||
<Box
|
||||
color='default'
|
||||
borderBlockEndWidth={1}
|
||||
borderBlockEndColor='stroke-extra-light'
|
||||
borderBlockEndStyle='solid'
|
||||
className={[...className, hovered].filter(Boolean)}
|
||||
paddingBlock={8}
|
||||
>
|
||||
<Message {...props}>
|
||||
<MessageLeftContainer>{username && <UserAvatar username={username} size='x36' />}</MessageLeftContainer>
|
||||
<MessageContainer>
|
||||
<MessageHeader>
|
||||
<MessageName title={username}>{displayName}</MessageName>
|
||||
<MessageTimestamp>{formatDate(createdAt)}</MessageTimestamp>
|
||||
</MessageHeader>
|
||||
<MessageBody clamp={2} />
|
||||
<Box display='flex'></Box>
|
||||
<MessageBlock flexDirection='row' alignItems='center'>
|
||||
<ButtonGroup>
|
||||
<Button disabled={Boolean(endedAt)} small alignItems='center' display='flex' onClick={handleJoinConference}>
|
||||
{endedAt ? t('Call_ended') : t('Join_call')}
|
||||
</Button>
|
||||
{discussionRid && (
|
||||
<IconButton
|
||||
small
|
||||
icon='discussion'
|
||||
data-drid={discussionRid}
|
||||
title={t('Join_discussion')}
|
||||
onClick={() => goToRoom(discussionRid)}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
{joinedUsers.length > 0 && (
|
||||
<Box marginInlineStart={8} fontScale='c1' display='flex' alignItems='center'>
|
||||
<AvatarStack>
|
||||
{joinedUsers.map(
|
||||
(user, index) =>
|
||||
user.username &&
|
||||
index + 1 <= VIDEOCONF_STACK_MAX_USERS && (
|
||||
<UserAvatar
|
||||
data-tooltip={user.username}
|
||||
key={user.username}
|
||||
username={user.username}
|
||||
etag={user.avatarETag ?? undefined}
|
||||
size='x28'
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</AvatarStack>
|
||||
<Box marginInlineStart={4}>
|
||||
{joinedUsers.length > VIDEOCONF_STACK_MAX_USERS
|
||||
? t('__usersCount__joined', { count: joinedUsers.length - VIDEOCONF_STACK_MAX_USERS })
|
||||
: t('joined')}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{joinedUsers.length === 0 && !endedAt && (
|
||||
<Box marginInlineStart={8} fontScale='c1'>
|
||||
{t('Be_the_first_to_join')}
|
||||
</Box>
|
||||
)}
|
||||
</MessageBlock>
|
||||
</MessageContainer>
|
||||
</Message>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoConfListItem;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useRoomToolbox } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import VideoConfList from './VideoConfList';
|
||||
import { useVideoConfList } from './useVideoConfList';
|
||||
import { useRoom } from '../../../contexts/RoomContext';
|
||||
|
||||
const VideoConfListWithData = () => {
|
||||
const room = useRoom();
|
||||
const { closeTab } = useRoomToolbox();
|
||||
const { isPending, data, error, refetch, fetchNextPage } = useVideoConfList({ roomId: room._id });
|
||||
|
||||
return (
|
||||
<VideoConfList
|
||||
loading={isPending}
|
||||
videoConfs={data?.videoConfs ?? []}
|
||||
total={data?.total ?? 0}
|
||||
error={error ?? undefined}
|
||||
reload={refetch}
|
||||
loadMoreItems={fetchNextPage}
|
||||
onClose={closeTab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoConfListWithData;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './VideoConfListWithData';
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { IRoom, VideoConference } from '@rocket.chat/core-typings';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import { videoConferenceQueryKeys } from '../../../../../lib/queryKeys';
|
||||
|
||||
export const useVideoConfList = ({ roomId }: { roomId: IRoom['_id'] }) => {
|
||||
const getVideoConfs = useEndpoint('GET', '/v1/video-conference.list');
|
||||
|
||||
const count = 25;
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: videoConferenceQueryKeys.fromRoom(roomId),
|
||||
queryFn: async ({ pageParam: offset }) => {
|
||||
const { data, total } = await getVideoConfs({
|
||||
roomId,
|
||||
offset,
|
||||
count,
|
||||
});
|
||||
|
||||
return {
|
||||
items: data.map((videoConf): VideoConference => ({
|
||||
...videoConf,
|
||||
_updatedAt: new Date(videoConf._updatedAt),
|
||||
createdAt: new Date(videoConf.createdAt),
|
||||
endedAt: videoConf.endedAt ? new Date(videoConf.endedAt) : undefined,
|
||||
users: videoConf.users.map((user) => ({
|
||||
...user,
|
||||
ts: new Date(user.ts),
|
||||
})),
|
||||
})),
|
||||
itemCount: total,
|
||||
};
|
||||
},
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage, _, lastOffset) => {
|
||||
const nextOffset = lastOffset + count;
|
||||
if (nextOffset >= lastPage.itemCount) return undefined;
|
||||
return nextOffset;
|
||||
},
|
||||
select: ({ pages }) => ({
|
||||
videoConfs: pages.flatMap((page) => page.items),
|
||||
total: pages.at(-1)?.itemCount,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { Skeleton } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import {
|
||||
useVideoConfSetPreferences,
|
||||
VideoConfPopup,
|
||||
VideoConfPopupContent,
|
||||
VideoConfPopupControllers,
|
||||
VideoConfController,
|
||||
useVideoConfControllers,
|
||||
VideoConfButton,
|
||||
VideoConfPopupFooter,
|
||||
VideoConfPopupFooterButtons,
|
||||
VideoConfPopupTitle,
|
||||
VideoConfPopupHeader,
|
||||
} from '@rocket.chat/ui-video-conf';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import VideoConfPopupRoomInfo from './VideoConfPopupRoomInfo';
|
||||
import { useVideoConfRoomName } from '../../hooks/useVideoConfRoomName';
|
||||
|
||||
export type IncomingPopupProps = {
|
||||
id: string;
|
||||
room: IRoom;
|
||||
position: number;
|
||||
onClose: (id: string) => void;
|
||||
onMute: (id: string) => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const IncomingPopup = ({ id, room, position, onClose, onMute, onConfirm }: IncomingPopupProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers();
|
||||
const setPreferences = useVideoConfSetPreferences();
|
||||
const roomName = useVideoConfRoomName(room);
|
||||
|
||||
const videoConfInfo = useEndpoint('GET', '/v1/video-conference.info');
|
||||
const { data, isPending, isSuccess } = useQuery({
|
||||
queryKey: ['getVideoConferenceInfo', id],
|
||||
queryFn: async () => videoConfInfo({ callId: id }),
|
||||
});
|
||||
|
||||
const showMic = Boolean(data?.capabilities?.mic);
|
||||
const showCam = Boolean(data?.capabilities?.cam);
|
||||
|
||||
const handleJoinCall = useStableCallback(() => {
|
||||
setPreferences(controllersConfig);
|
||||
onConfirm();
|
||||
});
|
||||
|
||||
return (
|
||||
<VideoConfPopup position={position} id={id} aria-label={t('Incoming_call_from__roomName__', { roomName })}>
|
||||
<VideoConfPopupHeader>
|
||||
<VideoConfPopupTitle text={t('Incoming_call_from')} />
|
||||
{isPending && <Skeleton />}
|
||||
{isSuccess && (showMic || showCam) && (
|
||||
<VideoConfPopupControllers>
|
||||
{showCam && (
|
||||
<VideoConfController
|
||||
active={controllersConfig.cam}
|
||||
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
|
||||
icon={controllersConfig.cam ? 'video' : 'video-off'}
|
||||
onClick={handleToggleCam}
|
||||
/>
|
||||
)}
|
||||
{showMic && (
|
||||
<VideoConfController
|
||||
active={controllersConfig.mic}
|
||||
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
|
||||
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
|
||||
onClick={handleToggleMic}
|
||||
/>
|
||||
)}
|
||||
</VideoConfPopupControllers>
|
||||
)}
|
||||
</VideoConfPopupHeader>
|
||||
<VideoConfPopupContent>
|
||||
<VideoConfPopupRoomInfo room={room} />
|
||||
</VideoConfPopupContent>
|
||||
<VideoConfPopupFooter>
|
||||
<VideoConfPopupFooterButtons>
|
||||
<VideoConfButton primary onClick={handleJoinCall}>
|
||||
{t('Accept')}
|
||||
</VideoConfButton>
|
||||
{onClose && (
|
||||
<VideoConfButton danger secondary onClick={(): void => onClose(id)}>
|
||||
{t('Decline')}
|
||||
</VideoConfButton>
|
||||
)}
|
||||
<VideoConfController small={false} secondary title={t('Mute_and_dismiss')} icon='cross' onClick={(): void => onMute(id)} />
|
||||
</VideoConfPopupFooterButtons>
|
||||
</VideoConfPopupFooter>
|
||||
</VideoConfPopup>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncomingPopup;
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import {
|
||||
VideoConfPopup,
|
||||
VideoConfPopupContent,
|
||||
VideoConfPopupControllers,
|
||||
VideoConfController,
|
||||
useVideoConfControllers,
|
||||
VideoConfButton,
|
||||
VideoConfPopupFooter,
|
||||
VideoConfPopupFooterButtons,
|
||||
VideoConfPopupTitle,
|
||||
VideoConfPopupHeader,
|
||||
useVideoConfCapabilities,
|
||||
useVideoConfPreferences,
|
||||
} from '@rocket.chat/ui-video-conf';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import VideoConfPopupRoomInfo from './VideoConfPopupRoomInfo';
|
||||
import { useVideoConfRoomName } from '../../hooks/useVideoConfRoomName';
|
||||
|
||||
export type OutgoingPopupProps = {
|
||||
id: string;
|
||||
room: IRoom;
|
||||
onClose: (id: string) => void;
|
||||
};
|
||||
|
||||
const OutgoingPopup = ({ room, onClose, id }: OutgoingPopupProps) => {
|
||||
const { t } = useTranslation();
|
||||
const videoConfPreferences = useVideoConfPreferences();
|
||||
const { controllersConfig } = useVideoConfControllers(videoConfPreferences);
|
||||
const capabilities = useVideoConfCapabilities();
|
||||
const roomName = useVideoConfRoomName(room);
|
||||
|
||||
const showCam = !!capabilities.cam;
|
||||
const showMic = !!capabilities.mic;
|
||||
|
||||
return (
|
||||
<VideoConfPopup aria-label={t('Calling__roomName__', { roomName })}>
|
||||
<VideoConfPopupHeader>
|
||||
<VideoConfPopupTitle text={t('Calling')} counter />
|
||||
{(showCam || showMic) && (
|
||||
<VideoConfPopupControllers>
|
||||
{showCam && (
|
||||
<VideoConfController
|
||||
active={controllersConfig.cam}
|
||||
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
|
||||
icon={controllersConfig.cam ? 'video' : 'video-off'}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
{showMic && (
|
||||
<VideoConfController
|
||||
active={controllersConfig.mic}
|
||||
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
|
||||
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
</VideoConfPopupControllers>
|
||||
)}
|
||||
</VideoConfPopupHeader>
|
||||
<VideoConfPopupContent>
|
||||
<VideoConfPopupRoomInfo room={room} />
|
||||
</VideoConfPopupContent>
|
||||
<VideoConfPopupFooter>
|
||||
<VideoConfPopupFooterButtons>
|
||||
{onClose && <VideoConfButton onClick={(): void => onClose(id)}>{t('Cancel')}</VideoConfButton>}
|
||||
</VideoConfPopupFooterButtons>
|
||||
</VideoConfPopupFooter>
|
||||
</VideoConfPopup>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutgoingPopup;
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { useOutsideClick, useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import {
|
||||
VideoConfPopup,
|
||||
VideoConfPopupHeader,
|
||||
VideoConfPopupContent,
|
||||
VideoConfPopupControllers,
|
||||
VideoConfController,
|
||||
useVideoConfControllers,
|
||||
VideoConfButton,
|
||||
VideoConfPopupFooter,
|
||||
VideoConfPopupTitle,
|
||||
VideoConfPopupFooterButtons,
|
||||
useVideoConfSetPreferences,
|
||||
useVideoConfCapabilities,
|
||||
useVideoConfPreferences,
|
||||
} from '@rocket.chat/ui-video-conf';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import VideoConfPopupRoomInfo from './VideoConfPopupRoomInfo';
|
||||
import { useVideoConfRoomName } from '../../hooks/useVideoConfRoomName';
|
||||
|
||||
export type StartCallPopupProps = {
|
||||
id: string;
|
||||
loading: boolean;
|
||||
room: IRoom;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const StartCallPopup = ({ id, loading, room, onClose, onConfirm }: StartCallPopupProps) => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLElement | null>(null);
|
||||
useOutsideClick([ref], !loading ? onClose : () => undefined);
|
||||
|
||||
const setPreferences = useVideoConfSetPreferences();
|
||||
const videoConfPreferences = useVideoConfPreferences();
|
||||
const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(videoConfPreferences);
|
||||
const capabilities = useVideoConfCapabilities();
|
||||
const roomName = useVideoConfRoomName(room);
|
||||
|
||||
const dialogLabel =
|
||||
room.t === 'd' ? `${t('Start_a_call_with__roomName__', { roomName })}` : `${t('Start_a_call_in__roomName__', { roomName })}`;
|
||||
|
||||
const showCam = !!capabilities.cam;
|
||||
const showMic = !!capabilities.mic;
|
||||
|
||||
const handleStartCall = useStableCallback(() => {
|
||||
setPreferences(controllersConfig);
|
||||
onConfirm();
|
||||
});
|
||||
|
||||
const callbackRef = useCallback(
|
||||
(node: HTMLElement | null) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current = node;
|
||||
node.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<VideoConfPopup ref={callbackRef} id={id} aria-label={dialogLabel}>
|
||||
<VideoConfPopupHeader>
|
||||
<VideoConfPopupTitle text={t('Start_a_call')} />
|
||||
{(showCam || showMic) && (
|
||||
<VideoConfPopupControllers>
|
||||
{showCam && (
|
||||
<VideoConfController
|
||||
active={controllersConfig.cam}
|
||||
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
|
||||
icon={controllersConfig.cam ? 'video' : 'video-off'}
|
||||
onClick={handleToggleCam}
|
||||
/>
|
||||
)}
|
||||
{showMic && (
|
||||
<VideoConfController
|
||||
active={controllersConfig.mic}
|
||||
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
|
||||
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
|
||||
onClick={handleToggleMic}
|
||||
/>
|
||||
)}
|
||||
</VideoConfPopupControllers>
|
||||
)}
|
||||
</VideoConfPopupHeader>
|
||||
<VideoConfPopupContent>
|
||||
<VideoConfPopupRoomInfo room={room} />
|
||||
</VideoConfPopupContent>
|
||||
<VideoConfPopupFooter>
|
||||
<VideoConfPopupFooterButtons>
|
||||
<VideoConfButton disabled={loading} primary onClick={handleStartCall}>
|
||||
{t('Start_call')}
|
||||
</VideoConfButton>
|
||||
</VideoConfPopupFooterButtons>
|
||||
</VideoConfPopupFooter>
|
||||
</VideoConfPopup>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartCallPopup;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useFocusManager } from '@react-aria/focus';
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { useUserRoom } from '@rocket.chat/ui-contexts';
|
||||
import {
|
||||
useVideoConfAcceptCall,
|
||||
useVideoConfAbortCall,
|
||||
useVideoConfRejectIncomingCall,
|
||||
useVideoConfDismissCall,
|
||||
useVideoConfStartCall,
|
||||
useVideoConfDismissOutgoing,
|
||||
} from '@rocket.chat/ui-video-conf';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import IncomingPopup from './IncomingPopup';
|
||||
import OutgoingPopup from './OutgoingPopup';
|
||||
import StartCallPopup from './StartCallPopup';
|
||||
|
||||
export type TimedVideoConfPopupProps = {
|
||||
id: string;
|
||||
rid: IRoom['_id'];
|
||||
isReceiving?: boolean;
|
||||
isCalling?: boolean;
|
||||
position: number;
|
||||
onClose?: (id: string) => void;
|
||||
};
|
||||
|
||||
const TimedVideoConfPopup = ({ id, rid, isReceiving = false, isCalling = false, position }: TimedVideoConfPopupProps) => {
|
||||
const [starting, setStarting] = useState(false);
|
||||
const acceptCall = useVideoConfAcceptCall();
|
||||
const abortCall = useVideoConfAbortCall();
|
||||
const rejectCall = useVideoConfRejectIncomingCall();
|
||||
const dismissCall = useVideoConfDismissCall();
|
||||
const startCall = useVideoConfStartCall();
|
||||
const dismissOutgoing = useVideoConfDismissOutgoing();
|
||||
const focusManager = useFocusManager();
|
||||
const room = useUserRoom(rid);
|
||||
|
||||
useEffect(() => {
|
||||
focusManager?.focusFirst();
|
||||
}, [focusManager]);
|
||||
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleConfirm = (): void => {
|
||||
acceptCall(id);
|
||||
};
|
||||
|
||||
const handleClose = (id: string): void => {
|
||||
if (isReceiving) {
|
||||
rejectCall(id);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCall();
|
||||
};
|
||||
|
||||
const handleMute = (): void => {
|
||||
dismissCall(id);
|
||||
};
|
||||
|
||||
const handleStartCall = async (): Promise<void> => {
|
||||
setStarting(true);
|
||||
startCall(rid);
|
||||
};
|
||||
|
||||
if (isReceiving) {
|
||||
return <IncomingPopup room={room} id={id} position={position} onClose={handleClose} onMute={handleMute} onConfirm={handleConfirm} />;
|
||||
}
|
||||
|
||||
if (isCalling) {
|
||||
return <OutgoingPopup room={room} id={id} onClose={handleClose} />;
|
||||
}
|
||||
|
||||
return <StartCallPopup loading={starting} room={room} id={id} onClose={dismissOutgoing} onConfirm={handleStartCall} />;
|
||||
};
|
||||
|
||||
export default TimedVideoConfPopup;
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { isDirectMessageRoom, isMultipleDirectMessageRoom } from '@rocket.chat/core-typings';
|
||||
import { RoomAvatar } from '@rocket.chat/ui-avatar';
|
||||
import { useUser } from '@rocket.chat/ui-contexts';
|
||||
import { VideoConfPopupInfo } from '@rocket.chat/ui-video-conf';
|
||||
|
||||
import { RoomIcon } from '../../../../../../components/RoomIcon';
|
||||
import ReactiveUserStatus from '../../../../../../components/UserStatus/ReactiveUserStatus';
|
||||
import { useVideoConfRoomName } from '../../hooks/useVideoConfRoomName';
|
||||
|
||||
export type VideoConfPopupRoomInfoProps = { room: IRoom };
|
||||
|
||||
const VideoConfPopupRoomInfo = ({ room }: VideoConfPopupRoomInfoProps) => {
|
||||
const ownUser = useUser();
|
||||
const [userId] = room?.uids?.filter((uid) => uid !== ownUser?._id) || [];
|
||||
const roomName = useVideoConfRoomName(room);
|
||||
const avatar = <RoomAvatar room={room} size='x40' />;
|
||||
|
||||
if (isDirectMessageRoom(room)) {
|
||||
return (
|
||||
<VideoConfPopupInfo
|
||||
avatar={avatar}
|
||||
{...(userId && {
|
||||
icon: isMultipleDirectMessageRoom(room) ? <RoomIcon placement='default' room={room} /> : <ReactiveUserStatus uid={userId} />,
|
||||
})}
|
||||
>
|
||||
{roomName}
|
||||
</VideoConfPopupInfo>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VideoConfPopupInfo avatar={avatar} icon={<RoomIcon placement='default' room={room} />}>
|
||||
{roomName}
|
||||
</VideoConfPopupInfo>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoConfPopupRoomInfo;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './TimedVideoConfPopup';
|
||||
@@ -0,0 +1,22 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import VideoConfPopups from './VideoConfPopups';
|
||||
import { createFakeRoom } from '../../../../../../tests/mocks/data';
|
||||
import { createFakeVideoConfCall, createFakeIncomingCall } from '../../../../../../tests/mocks/utils/video-conference';
|
||||
|
||||
const fakeRoom = createFakeRoom({ t: 'd' });
|
||||
const fakeDirectVideoConfCall = createFakeVideoConfCall({ type: 'direct', rid: fakeRoom._id });
|
||||
const fakeIncomingCall = createFakeIncomingCall({ rid: fakeRoom._id });
|
||||
|
||||
test('should render video conference incoming popup', async () => {
|
||||
render(<VideoConfPopups />, {
|
||||
wrapper: mockAppRoot()
|
||||
.withRoom(fakeRoom)
|
||||
.withEndpoint('GET', '/v1/video-conference.info', () => fakeDirectVideoConfCall as any)
|
||||
.withIncomingCalls([fakeIncomingCall])
|
||||
.build(),
|
||||
});
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useCustomSound } from '@rocket.chat/ui-contexts';
|
||||
import type { VideoConfPopupPayload } from '@rocket.chat/ui-video-conf';
|
||||
import {
|
||||
VideoConfPopupBackdrop,
|
||||
useVideoConfIsCalling,
|
||||
useVideoConfIsRinging,
|
||||
useVideoConfIncomingCalls,
|
||||
VideoConfPopupSkeleton,
|
||||
} from '@rocket.chat/ui-video-conf';
|
||||
import { lazy, Suspense, useEffect, useMemo } from 'react';
|
||||
|
||||
import VideoConfPopupPortal from '../../../../../portals/VideoConfPopupPortal';
|
||||
|
||||
const VideoConfPopup = lazy(() => import('./VideoConfPopup'));
|
||||
|
||||
export type VideoConfPopupsProps = { children?: VideoConfPopupPayload };
|
||||
|
||||
const VideoConfPopups = ({ children }: VideoConfPopupsProps) => {
|
||||
const { callSounds } = useCustomSound();
|
||||
const incomingCalls = useVideoConfIncomingCalls();
|
||||
const isRinging = useVideoConfIsRinging();
|
||||
const isCalling = useVideoConfIsCalling();
|
||||
|
||||
const popups = useMemo(
|
||||
() =>
|
||||
incomingCalls
|
||||
.filter((incomingCall) => !incomingCall.dismissed)
|
||||
.map((incomingCall) => ({ id: incomingCall.callId, rid: incomingCall.rid, isReceiving: true })),
|
||||
[incomingCalls],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRinging) {
|
||||
callSounds.playRinger();
|
||||
}
|
||||
|
||||
if (isCalling) {
|
||||
callSounds.playDialer();
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
callSounds.stopRinger();
|
||||
callSounds.stopDialer();
|
||||
};
|
||||
}, [isRinging, isCalling, callSounds]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(children || popups?.length > 0) && (
|
||||
<VideoConfPopupPortal>
|
||||
{(children ? [children, ...popups] : popups).map(({ id, rid, isReceiving }, index = 1) => (
|
||||
<VideoConfPopupBackdrop key={id}>
|
||||
<Suspense fallback={<VideoConfPopupSkeleton />}>
|
||||
<FocusScope restoreFocus>
|
||||
<VideoConfPopup id={id} rid={rid} isReceiving={isReceiving} isCalling={isCalling} position={index * 10} />
|
||||
</FocusScope>
|
||||
</Suspense>
|
||||
</VideoConfPopupBackdrop>
|
||||
))}
|
||||
</VideoConfPopupPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoConfPopups;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './VideoConfPopups';
|
||||
@@ -0,0 +1,85 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { screen, act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { useVideoConfOpenCall } from './useVideoConfOpenCall';
|
||||
|
||||
describe('with window.RocketChatDesktop set', () => {
|
||||
beforeEach(() => {
|
||||
window.RocketChatDesktop = {
|
||||
openInternalVideoChatWindow: jest.fn(),
|
||||
} as any;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete window.RocketChatDesktop;
|
||||
});
|
||||
|
||||
it('should pass to videoConfOpenCall the url', async () => {
|
||||
const { result } = renderHook(() => useVideoConfOpenCall(), { wrapper: mockAppRoot().build() });
|
||||
|
||||
const url = faker.internet.url();
|
||||
|
||||
act(() => {
|
||||
result.current(url);
|
||||
});
|
||||
|
||||
expect(window.RocketChatDesktop?.openInternalVideoChatWindow).toHaveBeenCalledWith(url, { providerName: undefined });
|
||||
});
|
||||
|
||||
it('should pass to videoConfOpenCall the url and the providerName', async () => {
|
||||
const { result } = renderHook(() => useVideoConfOpenCall(), { wrapper: mockAppRoot().build() });
|
||||
|
||||
const url = faker.internet.url();
|
||||
const providerName = faker.lorem.word();
|
||||
|
||||
act(() => {
|
||||
result.current(url, providerName);
|
||||
});
|
||||
|
||||
expect(window.RocketChatDesktop?.openInternalVideoChatWindow).toHaveBeenCalledWith(url, {
|
||||
providerName,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without window.RocketChatDesktop set', () => {
|
||||
const previousWindowOpen = window.open;
|
||||
|
||||
afterAll(() => {
|
||||
window.open = previousWindowOpen;
|
||||
});
|
||||
|
||||
it('should open window', async () => {
|
||||
window.open = jest.fn(() => ({}) as Window);
|
||||
|
||||
const { result } = renderHook(() => useVideoConfOpenCall(), {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
const url = faker.internet.url();
|
||||
act(() => {
|
||||
result.current(url);
|
||||
});
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith(url);
|
||||
expect(screen.queryByRole('dialog', { name: 'Open_call_in_new_tab' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should NOT open window, AND open modal instead', async () => {
|
||||
window.open = jest.fn(() => null);
|
||||
|
||||
const { result } = renderHook(() => useVideoConfOpenCall(), {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
const url = faker.internet.url();
|
||||
act(() => {
|
||||
result.current(url);
|
||||
});
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith(url);
|
||||
expect(window.open).toHaveReturnedWith(null);
|
||||
expect(await screen.findByRole('dialog', { name: 'Open_call_in_new_tab' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import VideoConfBlockModal from '../VideoConfBlockModal';
|
||||
|
||||
export const useVideoConfOpenCall = () => {
|
||||
const setModal = useSetModal();
|
||||
|
||||
const handleOpenCall = useCallback(
|
||||
(callUrl: string, providerName?: string | undefined) => {
|
||||
const desktopApp = window.RocketChatDesktop;
|
||||
|
||||
if (!desktopApp?.openInternalVideoChatWindow) {
|
||||
const open = () => window.open(callUrl);
|
||||
const popup = open();
|
||||
|
||||
if (popup !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setModal(<VideoConfBlockModal onClose={(): void => setModal(null)} onConfirm={open} />);
|
||||
return;
|
||||
}
|
||||
desktopApp.openInternalVideoChatWindow(callUrl, { providerName });
|
||||
},
|
||||
[setModal],
|
||||
);
|
||||
|
||||
return handleOpenCall;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { isDirectMessageRoom } from '@rocket.chat/core-typings';
|
||||
import { useUserDisplayName } from '@rocket.chat/ui-client';
|
||||
import { useUserSubscription } from '@rocket.chat/ui-contexts';
|
||||
|
||||
export const useVideoConfRoomName = (room: IRoom): string | undefined => {
|
||||
const subscription = useUserSubscription(room._id);
|
||||
const username = useUserDisplayName({ name: subscription?.fname, username: subscription?.name });
|
||||
|
||||
return isDirectMessageRoom(room) ? username : room.fname || room.name;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useSetModal, useRoute, useRole } from '@rocket.chat/ui-contexts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import VideoConfConfigModal from '../VideoConfConfigModal';
|
||||
|
||||
export const useVideoConfWarning = (): ((error: unknown) => void) => {
|
||||
const setModal = useSetModal();
|
||||
const isAdmin = useRole('admin');
|
||||
const videoConfSettingsRoute = useRoute('admin-settings');
|
||||
const handleClose = useStableCallback(() => setModal(null));
|
||||
|
||||
const handleRedirectToConfiguration = useStableCallback(() => {
|
||||
handleClose();
|
||||
videoConfSettingsRoute.push({
|
||||
group: 'Video_Conference',
|
||||
});
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => (): void => setModal(<VideoConfConfigModal onClose={handleClose} onConfirm={handleRedirectToConfiguration} isAdmin={isAdmin} />),
|
||||
[handleClose, handleRedirectToConfiguration, isAdmin, setModal],
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user