diff --git a/apps/meteor/app/api/server/v1/videoConference.ts b/apps/meteor/app/api/server/v1/videoConference.ts index 1f76fa4b7366..f471fcaebe40 100644 --- a/apps/meteor/app/api/server/v1/videoConference.ts +++ b/apps/meteor/app/api/server/v1/videoConference.ts @@ -9,6 +9,7 @@ import { import { API } from '../api'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { VideoConf } from '../../../../server/sdk'; import { videoConfProviders } from '../../../../server/lib/videoConfProviders'; import { availabilityErrors } from '../../../../lib/videoConference/constants'; @@ -18,7 +19,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isVideoConfStartProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60000 } }, { async post() { - const { roomId, title, allowRinging } = this.bodyParams; + const { roomId, title, allowRinging: requestRinging } = this.bodyParams; const { userId } = this; if (!userId || !(await canAccessRoomIdAsync(roomId, userId))) { return API.v1.failure('invalid-params'); @@ -31,9 +32,11 @@ API.v1.addRoute( throw new Error(availabilityErrors.NOT_ACTIVE); } + const allowRinging = Boolean(requestRinging) && (await hasPermissionAsync(userId, 'videoconf-ring-users')); + return API.v1.success({ data: { - ...(await VideoConf.start(userId, roomId, { title, allowRinging: Boolean(allowRinging) })), + ...(await VideoConf.start(userId, roomId, { title, allowRinging })), providerName, }, }); diff --git a/apps/meteor/app/apps/server/bridges/videoConferences.ts b/apps/meteor/app/apps/server/bridges/videoConferences.ts index 9d7bf245e73b..93c9e445f91c 100644 --- a/apps/meteor/app/apps/server/bridges/videoConferences.ts +++ b/apps/meteor/app/apps/server/bridges/videoConferences.ts @@ -60,8 +60,9 @@ export class AppVideoConferenceBridge extends VideoConferenceBridge { } } - protected async registerProvider(info: IVideoConfProvider): Promise { - videoConfProviders.registerProvider(info.name, info.capabilities || {}); + protected async registerProvider(info: IVideoConfProvider, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is registering a video conference provider.`); + videoConfProviders.registerProvider(info.name, info.capabilities || {}, appId); } protected async unRegisterProvider(info: IVideoConfProvider): Promise { diff --git a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts index fcddaced90cf..837020591c01 100644 --- a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts +++ b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts @@ -224,6 +224,7 @@ export const upsertPermissions = async (): Promise => { { _id: 'remove-slackbridge-links', roles: ['admin'] }, { _id: 'view-import-operations', roles: ['admin'] }, { _id: 'clear-oembed-cache', roles: ['admin'] }, + { _id: 'videoconf-ring-users', roles: ['admin', 'owner', 'moderator', 'user'] }, ]; for await (const permission of permissions) { diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js index 4126f8b719a7..5ab704946619 100644 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js +++ b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js @@ -10,7 +10,6 @@ const getCustomSoundId = (sound) => `custom-sound-${sound}`; class CustomSoundsClass { constructor() { this.list = new ReactiveVar({}); - this.add({ _id: 'calling', name: 'Calling', extension: 'mp3', src: getURL('sounds/calling.mp3') }); this.add({ _id: 'chime', name: 'Chime', extension: 'mp3', src: getURL('sounds/chime.mp3') }); this.add({ _id: 'door', name: 'Door', extension: 'mp3', src: getURL('sounds/door.mp3') }); this.add({ _id: 'beep', name: 'Beep', extension: 'mp3', src: getURL('sounds/beep.mp3') }); @@ -52,6 +51,8 @@ class CustomSoundsClass { extension: 'mp3', src: getURL('sounds/call-ended.mp3'), }); + this.add({ _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getURL('sounds/dialtone.mp3') }); + this.add({ _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getURL('sounds/ringtone.mp3') }); } add(sound) { diff --git a/apps/meteor/app/models/server/models/Users.js b/apps/meteor/app/models/server/models/Users.js index 2b3651a08d79..725f90a0be2f 100644 --- a/apps/meteor/app/models/server/models/Users.js +++ b/apps/meteor/app/models/server/models/Users.js @@ -642,7 +642,7 @@ export class Users extends Base { return this.find(query, options); } - findOneByAppId(appId, options) { + findOneByAppId(appId, options = {}) { const query = { appId }; return this.findOne(query, options); diff --git a/apps/meteor/client/lib/VideoConfManager.ts b/apps/meteor/client/lib/VideoConfManager.ts index 00c819da2640..8d4507ab8563 100644 --- a/apps/meteor/client/lib/VideoConfManager.ts +++ b/apps/meteor/client/lib/VideoConfManager.ts @@ -475,6 +475,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter this.onDirectCallRejected(params)); this.hookNotification('video-conference.confirmed', (params: DirectCallParams) => this.onDirectCallConfirmed(params)); this.hookNotification('video-conference.join', (params: DirectCallParams) => this.onDirectCallJoined(params)); + this.hookNotification('video-conference.end', (params: DirectCallParams) => this.onDirectCallEnded(params)); } private abortIncomingCall(callId: string): void { @@ -658,6 +659,41 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter void; -}; - -const CallingPopup = ({ room, onClose, id }: CallingPopupProps): ReactElement => { - const t = useTranslation(); - const user = useUser(); - const userId = user?._id; - const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const [directUsername] = room.usernames?.filter((username) => username !== user?.username) || []; - - const videoConfPreferences = useVideoConfPreferences(); - const setPreferences = useVideoConfSetPreferences(); - const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(videoConfPreferences); - const capabilities = useVideoConfCapabilities(); - - const showCam = !!capabilities.cam; - const showMic = !!capabilities.mic; - - const handleToggleMicPref = useMutableCallback(() => { - handleToggleMic(); - setPreferences({ mic: !controllersConfig.mic }); - }); - - const handleToggleCamPref = useMutableCallback(() => { - handleToggleCam(); - setPreferences({ cam: !controllersConfig.cam }); - }); - - return ( - - - - - {directUserId && ( - - - {directUsername && } - - )} - {(showCam || showMic) && ( - - {showMic && ( - - )} - {showCam && ( - - )} - - )} - - - - {onClose && ( - onClose(id)}> - {t('Cancel')} - - )} - - - - ); -}; - -export default CallingPopup; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/ReceivingPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/IncomingPopup.tsx similarity index 62% rename from apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/ReceivingPopup.tsx rename to apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/IncomingPopup.tsx index 3a336b632a04..1139a9cf44fb 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/ReceivingPopup.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/IncomingPopup.tsx @@ -1,7 +1,7 @@ import { IRoom } from '@rocket.chat/core-typings'; -import { Box, Skeleton } from '@rocket.chat/fuselage'; +import { Skeleton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation, useUser } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import { VideoConfPopup, VideoConfPopupContent, @@ -12,36 +12,26 @@ import { VideoConfPopupFooter, VideoConfPopupFooterButtons, VideoConfPopupTitle, - VideoConfPopupIndicators, - VideoConfPopupClose, - VideoConfPopupUsername, + VideoConfPopupHeader, } from '@rocket.chat/ui-video-conf'; import React, { ReactElement, useMemo } from 'react'; -import ReactiveUserStatus from '../../../../../../components/UserStatus/ReactiveUserStatus'; -import RoomAvatar from '../../../../../../components/avatar/RoomAvatar'; import { useVideoConfSetPreferences } from '../../../../../../contexts/VideoConfContext'; import { AsyncStatePhase } from '../../../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../../../hooks/useEndpointData'; +import VideoConfPopupRoomInfo from './VideoConfPopupRoomInfo'; -type ReceivingPopupProps = { +type IncomingPopupProps = { id: string; room: IRoom; position: number; - current: number; - total: number; onClose: (id: string) => void; onMute: (id: string) => void; onConfirm: () => void; }; -const ReceivingPopup = ({ id, room, position, current, total, onClose, onMute, onConfirm }: ReceivingPopupProps): ReactElement => { +const IncomingPopup = ({ id, room, position, onClose, onMute, onConfirm }: IncomingPopupProps): ReactElement => { const t = useTranslation(); - const user = useUser(); - const userId = user?._id; - const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const [directUsername] = room.usernames?.filter((username) => username !== user?.username) || []; - const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(); const setPreferences = useVideoConfSetPreferences(); @@ -57,40 +47,32 @@ const ReceivingPopup = ({ id, room, position, current, total, onClose, onMute, o return ( - - onMute(id)} /> - - {current && total ? : null} - - {directUserId && ( - - - {directUsername && } - - )} + + {phase === AsyncStatePhase.LOADING && } {phase === AsyncStatePhase.RESOLVED && (showMic || showCam) && ( - {showMic && ( - - )} {showCam && ( )} + {showMic && ( + + )} )} + + + @@ -98,14 +80,15 @@ const ReceivingPopup = ({ id, room, position, current, total, onClose, onMute, o {t('Accept')} {onClose && ( - onClose(id)}> + onClose(id)}> {t('Decline')} )} + onMute(id)} /> ); }; -export default ReceivingPopup; +export default IncomingPopup; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/OutgoingPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/OutgoingPopup.tsx new file mode 100644 index 000000000000..faab0e688e02 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/OutgoingPopup.tsx @@ -0,0 +1,72 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import { + VideoConfPopup, + VideoConfPopupContent, + VideoConfPopupControllers, + VideoConfController, + useVideoConfControllers, + VideoConfButton, + VideoConfPopupFooter, + VideoConfPopupFooterButtons, + VideoConfPopupTitle, + VideoConfPopupHeader, +} from '@rocket.chat/ui-video-conf'; +import React, { ReactElement } from 'react'; + +import { useVideoConfCapabilities, useVideoConfPreferences } from '../../../../../../contexts/VideoConfContext'; +import VideoConfPopupRoomInfo from './VideoConfPopupRoomInfo'; + +type OutgoingPopupProps = { + id: string; + room: IRoom; + onClose: (id: string) => void; +}; + +const OutgoingPopup = ({ room, onClose, id }: OutgoingPopupProps): ReactElement => { + const t = useTranslation(); + const videoConfPreferences = useVideoConfPreferences(); + const { controllersConfig } = useVideoConfControllers(videoConfPreferences); + const capabilities = useVideoConfCapabilities(); + + const showCam = !!capabilities.cam; + const showMic = !!capabilities.mic; + + return ( + + + + {(showCam || showMic) && ( + + {showCam && ( + + )} + {showMic && ( + + )} + + )} + + + + + + + {onClose && onClose(id)}>{t('Cancel')}} + + + + ); +}; + +export default OutgoingPopup; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartGroupCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup.tsx similarity index 62% rename from apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartGroupCallPopup.tsx rename to apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup.tsx index 80950074c541..5b372fc17f9e 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartGroupCallPopup.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup.tsx @@ -1,8 +1,9 @@ import { IRoom } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useOutsideClick, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import { VideoConfPopup, + VideoConfPopupHeader, VideoConfPopupContent, VideoConfPopupControllers, VideoConfController, @@ -12,25 +13,23 @@ import { VideoConfPopupTitle, VideoConfPopupFooterButtons, } from '@rocket.chat/ui-video-conf'; -import React, { ReactElement, forwardRef, Ref } from 'react'; +import React, { ReactElement, useRef } from 'react'; -import RoomAvatar from '../../../../../../../components/avatar/RoomAvatar'; -import { - useVideoConfSetPreferences, - useVideoConfCapabilities, - useVideoConfPreferences, -} from '../../../../../../../contexts/VideoConfContext'; +import { useVideoConfSetPreferences, useVideoConfCapabilities, useVideoConfPreferences } from '../../../../../../contexts/VideoConfContext'; +import VideoConfPopupRoomInfo from './VideoConfPopupRoomInfo'; -type StartGroupCallPopup = { +type StartCallPopup = { + id: string; room: IRoom; + onClose: () => void; onConfirm: () => void; - loading?: boolean; + loading: boolean; }; -const StartGroupCallPopup = forwardRef(function StartGroupCallPopup( - { room, onConfirm, loading }: StartGroupCallPopup, - ref: Ref, -): ReactElement { +const StartCallPopup = ({ loading, room, onClose, onConfirm }: StartCallPopup): ReactElement => { + const ref = useRef(null); + useOutsideClick([ref], !loading ? onClose : (): void => undefined); + const t = useTranslation(); const setPreferences = useVideoConfSetPreferences(); const videoConfPreferences = useVideoConfPreferences(); @@ -40,16 +39,6 @@ const StartGroupCallPopup = forwardRef(function StartGroupCallPopup( const showCam = !!capabilities.cam; const showMic = !!capabilities.mic; - const handleToggleMicPref = useMutableCallback(() => { - handleToggleMic(); - setPreferences({ mic: !controllersConfig.mic }); - }); - - const handleToggleCamPref = useMutableCallback(() => { - handleToggleCam(); - setPreferences({ cam: !controllersConfig.cam }); - }); - const handleStartCall = useMutableCallback(() => { setPreferences(controllersConfig); onConfirm(); @@ -57,31 +46,31 @@ const StartGroupCallPopup = forwardRef(function StartGroupCallPopup( return ( - - + {(showCam || showMic) && ( - {showMic && ( - - )} {showCam && ( + )} + {showMic && ( + )} )} + + + @@ -92,6 +81,6 @@ const StartGroupCallPopup = forwardRef(function StartGroupCallPopup( ); -}); +}; -export default StartGroupCallPopup; +export default StartCallPopup; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartCallPopup.tsx deleted file mode 100644 index debef4ba25f4..000000000000 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartCallPopup.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { IRoom, isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { useMutableCallback, useOutsideClick } from '@rocket.chat/fuselage-hooks'; -import { useUserId } from '@rocket.chat/ui-contexts'; -import { useVideoConfControllers } from '@rocket.chat/ui-video-conf'; -import React, { ReactElement, useRef } from 'react'; - -import { useVideoConfSetPreferences, useVideoConfPreferences } from '../../../../../../../contexts/VideoConfContext'; -import StartDirectCallPopup from './StartDirectCallPopup'; -import StartGroupCallPopup from './StartGroupCallPopup'; -import StartOmnichannelCallPopup from './StartOmnichannelCallPopup'; - -type StartCallPopup = { - id: string; - room: IRoom; - onClose: () => void; - onConfirm: () => void; - loading: boolean; -}; - -const StartCallPopup = ({ loading, room, onClose, onConfirm }: StartCallPopup): ReactElement => { - const ref = useRef(null); - const userId = useUserId(); - const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const videoConfPreferences = useVideoConfPreferences(); - const setPreferences = useVideoConfSetPreferences(); - const { controllersConfig } = useVideoConfControllers(videoConfPreferences); - - useOutsideClick([ref], !loading ? onClose : (): void => undefined); - - const handleStartCall = useMutableCallback(() => { - setPreferences(controllersConfig); - onConfirm(); - }); - - if (isDirectMessageRoom(room) && !isMultipleDirectMessageRoom(room) && directUserId) { - return ; - } - - if (isOmnichannelRoom(room)) { - return ; - } - - return ; -}; - -export default StartCallPopup; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartDirectCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartDirectCallPopup.tsx deleted file mode 100644 index 2216b0d8cdb0..000000000000 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartDirectCallPopup.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { IRoom } from '@rocket.chat/core-typings'; -import { Box } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation, useUser } from '@rocket.chat/ui-contexts'; -import { - VideoConfPopup, - VideoConfPopupContent, - VideoConfPopupControllers, - VideoConfController, - useVideoConfControllers, - VideoConfButton, - VideoConfPopupFooter, - VideoConfPopupTitle, - VideoConfPopupFooterButtons, - VideoConfPopupUsername, -} from '@rocket.chat/ui-video-conf'; -import React, { ReactElement, forwardRef, Ref } from 'react'; - -import ReactiveUserStatus from '../../../../../../../components/UserStatus/ReactiveUserStatus'; -import RoomAvatar from '../../../../../../../components/avatar/RoomAvatar'; -import { - useVideoConfSetPreferences, - useVideoConfCapabilities, - useVideoConfPreferences, -} from '../../../../../../../contexts/VideoConfContext'; - -type StartDirectCallPopup = { - room: IRoom; - onConfirm: () => void; - loading: boolean; -}; - -const StartDirectCallPopup = forwardRef(function StartDirectCallPopup( - { room, onConfirm, loading }: StartDirectCallPopup, - ref: Ref, -): ReactElement { - const t = useTranslation(); - const user = useUser(); - const userId = user?._id; - const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const [directUsername] = room.usernames?.filter((username) => username !== user?.username) || []; - - const videoConfPreferences = useVideoConfPreferences(); - const setPreferences = useVideoConfSetPreferences(); - const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(videoConfPreferences); - const capabilities = useVideoConfCapabilities(); - - const showCam = !!capabilities.cam; - const showMic = !!capabilities.mic; - - const handleStartCall = useMutableCallback(() => { - setPreferences(controllersConfig); - onConfirm(); - }); - - return ( - - - - - {directUserId && ( - - - {directUsername && } - - )} - {(showCam || showMic) && ( - - {showMic && ( - - )} - {showCam && ( - - )} - - )} - - - - - {t('Start_call')} - - - - - ); -}); - -export default StartDirectCallPopup; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartOmnichannelCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartOmnichannelCallPopup.tsx deleted file mode 100644 index 2a7527e7dfdf..000000000000 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartOmnichannelCallPopup.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { IRoom } from '@rocket.chat/core-typings'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import { - VideoConfPopup, - VideoConfPopupContent, - VideoConfButton, - VideoConfPopupFooter, - VideoConfPopupTitle, - VideoConfPopupFooterButtons, -} from '@rocket.chat/ui-video-conf'; -import React, { ReactElement, forwardRef, Ref } from 'react'; - -import RoomAvatar from '../../../../../../../components/avatar/RoomAvatar'; - -type StartOmnichannelCallPopup = { - room: IRoom; - onConfirm: () => void; - loading?: boolean; -}; - -const StartOmnichannelCallPopup = forwardRef(function StartOmnichannelCallPopup( - { room, onConfirm, loading }: StartOmnichannelCallPopup, - ref: Ref, -): ReactElement { - const t = useTranslation(); - - return ( - - - - - - - - - {t('Start_call')} - - - - - ); -}); - -export default StartOmnichannelCallPopup; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/index.ts deleted file mode 100644 index a2220d6af5f5..000000000000 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './StartCallPopup'; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/TimedVideoConfPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/TimedVideoConfPopup.tsx index 167cf56ac6e9..41b698b70b09 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/TimedVideoConfPopup.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/TimedVideoConfPopup.tsx @@ -10,9 +10,9 @@ import { useVideoConfStartCall, useVideoConfDismissOutgoing, } from '../../../../../../contexts/VideoConfContext'; -import CallingPopup from './CallingPopup'; -import ReceivingPopup from './ReceivingPopup'; -import StartCallPopup from './StartCallPopup/StartCallPopup'; +import IncomingPopup from './IncomingPopup'; +import OutgoingPopup from './OutgoingPopup'; +import StartCallPopup from './StartCallPopup'; export type TimedVideoConfPopupProps = { id: string; @@ -20,8 +20,6 @@ export type TimedVideoConfPopupProps = { isReceiving?: boolean; isCalling?: boolean; position: number; - current: number; - total: number; onClose?: (id: string) => void; }; @@ -31,8 +29,6 @@ const TimedVideoConfPopup = ({ isReceiving = false, isCalling = false, position, - current, - total, }: TimedVideoConfPopupProps): ReactElement | null => { const [starting, setStarting] = useState(false); const acceptCall = useVideoConfAcceptCall(); @@ -70,22 +66,11 @@ const TimedVideoConfPopup = ({ }; if (isReceiving) { - return ( - - ); + return ; } if (isCalling) { - return ; + return ; } return ; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/VideoConfPopupRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/VideoConfPopupRoomInfo.tsx new file mode 100644 index 000000000000..b463e5a62910 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/VideoConfPopupRoomInfo.tsx @@ -0,0 +1,38 @@ +import { IRoom, isDirectMessageRoom, isMultipleDirectMessageRoom } from '@rocket.chat/core-typings'; +import { useUser, useUserSubscription } from '@rocket.chat/ui-contexts'; +import { VideoConfPopupInfo } from '@rocket.chat/ui-video-conf'; +import React, { ReactElement } from 'react'; + +import { RoomIcon } from '../../../../../../components/RoomIcon'; +import ReactiveUserStatus from '../../../../../../components/UserStatus/ReactiveUserStatus'; +import RoomAvatar from '../../../../../../components/avatar/RoomAvatar'; +import { useUserDisplayName } from '../../../../../../hooks/useUserDisplayName'; + +const VideoConfPopupRoomInfo = ({ room }: { room: IRoom }): ReactElement => { + const ownUser = useUser(); + const [userId] = room?.uids?.filter((uid) => uid !== ownUser?._id) || []; + const subscription = useUserSubscription(room._id); + const username = useUserDisplayName({ name: subscription?.fname, username: subscription?.name }); + const avatar = ; + + if (isDirectMessageRoom(room)) { + return ( + : , + })} + > + {username} + + ); + } + + return ( + }> + {room.name} + + ); +}; + +export default VideoConfPopupRoomInfo; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx index a4120bbe4452..559cc1e42822 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx @@ -12,8 +12,8 @@ import VideoConfPopupPortal from '../../../../../portals/VideoConfPopupPortal'; import VideoConfPopup from './VideoConfPopup'; const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): ReactElement => { - const incomingCalls = useVideoConfIncomingCalls(); const customSound = useCustomSound(); + const incomingCalls = useVideoConfIncomingCalls(); const isRinging = useVideoConfIsRinging(); const isCalling = useVideoConfIsCalling(); @@ -26,16 +26,19 @@ const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): Re ); useEffect(() => { - if (!isRinging) { - return; + if (isRinging) { + customSound.play('ringtone', { loop: true }); } - customSound.play('calling', { loop: true }); + if (isCalling) { + customSound.play('dialtone', { loop: true }); + } return (): void => { - customSound.pause('calling'); + customSound.pause('ringtone'); + customSound.pause('dialtone'); }; - }, [customSound, isRinging]); + }, [customSound, isRinging, isCalling]); return ( <> @@ -43,16 +46,7 @@ const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): Re {(children ? [children, ...popups] : popups).map(({ id, rid, isReceiving }, index = 1) => ( - + ))} diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useAudioCallAction.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useAudioCallAction.ts deleted file mode 100644 index 90987c5e4811..000000000000 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useAudioCallAction.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IRoom, isRoomFederated } from '@rocket.chat/core-typings'; -import { useTranslation, useUserRoom } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -import { Action } from '../../../../hooks/useActionSpread'; -import { useWebRTC } from '../../useWebRTC'; - -export const useAudioCallAction = (rid: IRoom['_id']): Action | undefined => { - const t = useTranslation(); - const { shouldAllowCalls, callInProgress, joinCall, startCall } = useWebRTC(rid); - const room = useUserRoom(rid); - - const audioCallOption = useMemo(() => { - const handleJoinCall = (): void => { - joinCall({ audio: true, video: false }); - }; - - const handleStartCall = (): void => { - startCall({ audio: true, video: false }); - }; - - const action = callInProgress ? handleJoinCall : handleStartCall; - - return room && !isRoomFederated(room) && shouldAllowCalls - ? { - label: t(callInProgress ? 'Join_audio_call' : 'Start_audio_call'), - icon: 'mic', - action, - } - : undefined; - }, [callInProgress, shouldAllowCalls, t, joinCall, startCall, room]); - - return audioCallOption; -}; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useCallAction.tsx b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useCallAction.tsx new file mode 100644 index 000000000000..53dba549f919 --- /dev/null +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useCallAction.tsx @@ -0,0 +1,47 @@ +import { IUser, isRoomFederated } from '@rocket.chat/core-typings'; +import { useTranslation, useUserRoom, useUserId, useUserSubscriptionByName } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { closeUserCard } from '../../../../../../app/ui/client/lib/UserCard'; +import { useVideoConfDispatchOutgoing, useVideoConfIsCalling, useVideoConfIsRinging } from '../../../../../contexts/VideoConfContext'; +import { VideoConfManager } from '../../../../../lib/VideoConfManager'; +import { Action } from '../../../../hooks/useActionSpread'; +import { useVideoConfWarning } from '../../../contextualBar/VideoConference/useVideoConfWarning'; + +export const useCallAction = (user: Pick): Action | undefined => { + const t = useTranslation(); + const usernameSubscription = useUserSubscriptionByName(user.username ?? ''); + const room = useUserRoom(usernameSubscription?.rid || ''); + + const dispatchWarning = useVideoConfWarning(); + const dispatchPopup = useVideoConfDispatchOutgoing(); + const isCalling = useVideoConfIsCalling(); + const isRinging = useVideoConfIsRinging(); + const ownUserId = useUserId(); + + const videoCallOption = useMemo(() => { + const action = async (): Promise => { + if (isCalling || isRinging || !room) { + return; + } + + try { + await VideoConfManager.loadCapabilities(); + closeUserCard(); + dispatchPopup({ rid: room._id }); + } catch (error: any) { + dispatchWarning(error.error); + } + }; + + return room && !isRoomFederated(room) && user._id !== ownUserId + ? { + label: t('Start_call'), + icon: 'phone', + action, + } + : undefined; + }, [t, room, dispatchPopup, dispatchWarning, isCalling, isRinging, ownUserId, user._id]); + + return videoCallOption; +}; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useVideoCallAction.tsx b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useVideoCallAction.tsx deleted file mode 100644 index 836be8d3dd63..000000000000 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useVideoCallAction.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { IRoom, isRoomFederated } from '@rocket.chat/core-typings'; -import { useTranslation, useUserRoom } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -import { Action } from '../../../../hooks/useActionSpread'; -import { useWebRTC } from '../../useWebRTC'; - -export const useVideoCallAction = (rid: IRoom['_id']): Action | undefined => { - const t = useTranslation(); - const { shouldAllowCalls, callInProgress, joinCall, startCall } = useWebRTC(rid); - const room = useUserRoom(rid); - - const videoCallOption = useMemo(() => { - const handleJoinCall = (): void => { - joinCall({ audio: true, video: true }); - }; - - const handleStartCall = (): void => { - startCall({ audio: true, video: true }); - }; - - const action = callInProgress ? handleJoinCall : handleStartCall; - - return room && !isRoomFederated(room) && shouldAllowCalls - ? { - label: t(callInProgress ? 'Join_video_call' : 'Start_video_call'), - icon: 'video', - action, - } - : undefined; - }, [callInProgress, shouldAllowCalls, t, joinCall, startCall, room]); - - return videoCallOption; -}; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts index f54586c0a18e..c907a5361470 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts @@ -2,8 +2,8 @@ import { IRoom, IUser } from '@rocket.chat/core-typings'; import { useMemo } from 'react'; import { Action } from '../../../hooks/useActionSpread'; -import { useAudioCallAction } from './actions/useAudioCallAction'; import { useBlockUserAction } from './actions/useBlockUserAction'; +import { useCallAction } from './actions/useCallAction'; import { useChangeLeaderAction } from './actions/useChangeLeaderAction'; import { useChangeModeratorAction } from './actions/useChangeModeratorAction'; import { useChangeOwnerAction } from './actions/useChangeOwnerAction'; @@ -11,7 +11,6 @@ import { useDirectMessageAction } from './actions/useDirectMessageAction'; import { useIgnoreUserAction } from './actions/useIgnoreUserAction'; import { useMuteUserAction } from './actions/useMuteUserAction'; import { useRemoveUserAction } from './actions/useRemoveUserAction'; -import { useVideoCallAction } from './actions/useVideoCallAction'; export const useUserInfoActions = ( user: Pick, @@ -20,7 +19,6 @@ export const useUserInfoActions = ( ): { [key: string]: Action; } => { - const audioCallOption = useAudioCallAction(rid); const blockUserOption = useBlockUserAction(user, rid); const changeLeaderOption = useChangeLeaderAction(user, rid); const changeModeratorOption = useChangeModeratorAction(user, rid); @@ -29,13 +27,12 @@ export const useUserInfoActions = ( const ignoreUserOption = useIgnoreUserAction(user, rid); const muteUserOption = useMuteUserAction(user, rid); const removeUserOption = useRemoveUserAction(user, rid, reload); - const videoCallOption = useVideoCallAction(rid); + const callOption = useCallAction(user); return useMemo( () => ({ ...(openDirectMessageOption && { openDirectMessage: openDirectMessageOption }), - ...(videoCallOption && { video: videoCallOption }), - ...(audioCallOption && { audio: audioCallOption }), + ...(callOption && { call: callOption }), ...(changeOwnerOption && { changeOwner: changeOwnerOption }), ...(changeLeaderOption && { changeLeader: changeLeaderOption }), ...(changeModeratorOption && { changeModerator: changeModeratorOption }), @@ -45,7 +42,6 @@ export const useUserInfoActions = ( ...(removeUserOption && { removeUser: removeUserOption }), }), [ - audioCallOption, changeLeaderOption, changeModeratorOption, changeOwnerOption, @@ -53,7 +49,7 @@ export const useUserInfoActions = ( muteUserOption, openDirectMessageOption, removeUserOption, - videoCallOption, + callOption, blockUserOption, ], ); diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index d3c83c903ce8..007bf722335f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1019,6 +1019,7 @@ "Commit_details": "Commit Details", "Completed": "Completed", "Computer": "Computer", + "Conference_call_has_ended": "_Call has ended._", "Conference_name": "Conference name", "Configure_Incoming_Mail_IMAP": "Configure Incoming Mail (IMAP)", "Configure_Outgoing_Mail_SMTP": "Configure Outgoing Mail (SMTP)", @@ -2365,6 +2366,7 @@ "Include_Offline_Agents": "Include offline agents", "Inclusive": "Inclusive", "Incoming": "Incoming", + "Incoming_call_from": "Incoming call from", "Incoming_Livechats": "Queued Chats", "Incoming_WebHook": "Incoming WebHook", "Industry": "Industry", @@ -3220,6 +3222,7 @@ "multi": "multi", "multi_line": "multi line", "Mute": "Mute", + "Mute_and_dismiss": "Mute and dismiss", "Mute_all_notifications": "Mute all notifications", "Mute_Focused_Conversations": "Mute Focused Conversations", "Mute_Group_Mentions": "Mute @all and @here mentions", @@ -4856,22 +4859,24 @@ "Video_message": "Video message", "Videocall_declined": "Video Call Declined.", "Video_and_Audio_Call": "Video and Audio Call", - "video_conference_started": "Started a call.", - "video_conference_ended": "Call ended.", - "video_conference_ended_by": "Ended a call.", - "video_livechat_started": "Started a video call.", - "video_livechat_missed": "Started a video call that wasn't answered.", - "video_direct_calling": "Is calling.", - "video_direct_ended": "Call ended.", - "video_direct_ended_by": "Ended a call.", - "video_direct_missed": "Started a call that wasn't answered.", - "video_direct_started": "Started a call.", + "video_conference_started": "_Started a call._", + "video_conference_started_by": "**__username__** _started a call._", + "video_conference_ended": "_Call has ended._", + "video_conference_ended_by": "**__username__** _ended a call._", + "video_livechat_started": "_Started a video call._", + "video_livechat_missed": "_Started a video call that wasn't answered._", + "video_direct_calling": "_Is calling._", + "video_direct_ended": "_Call has ended._", + "video_direct_ended_by": "**__username__** _ended a call._", + "video_direct_missed": "_Started a call that wasn't answered._", + "video_direct_started": "_Started a call._", "VideoConf_Default_Provider": "Default Provider", "VideoConf_Default_Provider_Description": "If you have multiple provider apps installed, select which one should be used for new conference calls.", "VideoConf_Enable_Channels": "Enable in public channels", "VideoConf_Enable_Groups": "Enable in private channels", "VideoConf_Enable_DMs": "Enable in direct messages", "VideoConf_Enable_Teams": "Enable in teams", + "videoconf-ring-users": "Ring other users when calling", "Videos": "Videos", "View_All": "View All Members", "View_channels": "View Channels", diff --git a/apps/meteor/public/sounds/dialtone.mp3 b/apps/meteor/public/sounds/dialtone.mp3 new file mode 100644 index 000000000000..5a6aa99f39b0 Binary files /dev/null and b/apps/meteor/public/sounds/dialtone.mp3 differ diff --git a/apps/meteor/public/sounds/calling.mp3 b/apps/meteor/public/sounds/ringtone.mp3 similarity index 100% rename from apps/meteor/public/sounds/calling.mp3 rename to apps/meteor/public/sounds/ringtone.mp3 diff --git a/apps/meteor/server/lib/videoConfProviders.ts b/apps/meteor/server/lib/videoConfProviders.ts index 353fa36fad81..4c26f8065f6d 100644 --- a/apps/meteor/server/lib/videoConfProviders.ts +++ b/apps/meteor/server/lib/videoConfProviders.ts @@ -2,11 +2,11 @@ import { VideoConferenceCapabilities } from '@rocket.chat/core-typings'; import { settings } from '../../app/settings/server'; -const providers = new Map(); +const providers = new Map(); export const videoConfProviders = { - registerProvider(providerName: string, capabilities: VideoConferenceCapabilities): void { - providers.set(providerName.toLowerCase(), { capabilities, label: providerName }); + registerProvider(providerName: string, capabilities: VideoConferenceCapabilities, appId: string): void { + providers.set(providerName.toLowerCase(), { capabilities, label: providerName, appId }); }, unRegisterProvider(providerName: string): void { @@ -50,10 +50,21 @@ export const videoConfProviders = { }, getProviderCapabilities(name: string): VideoConferenceCapabilities | undefined { - if (!providers.has(name)) { + const key = name.toLowerCase(); + if (!providers.has(key)) { return; } - return providers.get(name)?.capabilities; + return providers.get(key)?.capabilities; + }, + + getProviderAppId(name: string): string | undefined { + const key = name.toLowerCase(); + + if (!providers.has(key)) { + return; + } + + return providers.get(key)?.appId; }, }; diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index f172cc221890..780366acb3be 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -272,6 +272,12 @@ export class UsersRaw extends BaseRaw { return this.findOne(query); } + async findOneByAppId(appId, options) { + const query = { appId }; + + return this.findOne(query, options); + } + findLDAPUsers(options) { const query = { ldap: true }; diff --git a/apps/meteor/server/modules/notifications/notifications.module.ts b/apps/meteor/server/modules/notifications/notifications.module.ts index 48dc20a1358f..11ec6ed94e04 100644 --- a/apps/meteor/server/modules/notifications/notifications.module.ts +++ b/apps/meteor/server/modules/notifications/notifications.module.ts @@ -2,7 +2,7 @@ import type { IStreamer, IStreamerConstructor, IPublication } from 'meteor/rocke import type { ISubscription, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users, Settings } from '@rocket.chat/models'; -import { Authorization } from '../../sdk'; +import { Authorization, VideoConf } from '../../sdk'; import { emit, StreamPresence } from '../../../app/notifications/server/lib/Presence'; import { SystemLogger } from '../../lib/logger/system'; @@ -280,11 +280,28 @@ export class NotificationsModule { return false; }); - this.streamUser.allowWrite(async function (eventName) { + this.streamUser.allowWrite(async function (eventName: string, data: unknown) { const [, e] = eventName.split('/'); if (e === 'webrtc') { return true; } + if (e.startsWith('video-conference.')) { + if (!this.userId || !data || typeof data !== 'object') { + return false; + } + + const callId = 'callId' in data && typeof (data as any).callId === 'string' ? (data as any).callId : ''; + const uid = 'uid' in data && typeof (data as any).uid === 'string' ? (data as any).uid : ''; + const rid = 'rid' in data && typeof (data as any).rid === 'string' ? (data as any).rid : ''; + + const action = e.replace('video-conference.', ''); + + return VideoConf.validateAction(action, this.userId, { + callId, + uid, + rid, + }); + } return Boolean(this.userId); }); diff --git a/apps/meteor/server/sdk/types/IVideoConfService.ts b/apps/meteor/server/sdk/types/IVideoConfService.ts index 031334aa55f0..5c6f9037f918 100644 --- a/apps/meteor/server/sdk/types/IVideoConfService.ts +++ b/apps/meteor/server/sdk/types/IVideoConfService.ts @@ -15,7 +15,7 @@ export type VideoConferenceJoinOptions = { }; export interface IVideoConfService { - create(data: VideoConferenceCreateData): Promise; + create(data: VideoConferenceCreateData, useAppUser?: boolean): Promise; start(caller: IUser['_id'], rid: string, options: { title?: string; allowRinging?: boolean }): Promise; join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise; cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise; @@ -33,4 +33,9 @@ export interface IVideoConfService { declineLivechatCall(callId: VideoConference['_id']): Promise; diagnoseProvider(uid: string, rid: string, providerName?: string): Promise; getStatistics(): Promise; + validateAction( + event: string, + caller: IUser['_id'], + params: { callId: VideoConference['_id']; uid: IUser['_id']; rid: IRoom['_id'] }, + ): Promise; } diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 0b45f465f623..0808e685ba51 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -1,6 +1,7 @@ import { MongoInternals } from 'meteor/mongo'; import type { IDirectVideoConference, + ILivechatVideoConference, IRoom, IUser, VideoConferenceInstructions, @@ -42,6 +43,7 @@ import { readSecondaryPreferred } from '../../database/readSecondaryPreferred'; import { availabilityErrors } from '../../../lib/videoConference/constants'; import { callbacks } from '../../../lib/callbacks'; import { Notifications } from '../../../app/notifications/server'; +import { canAccessRoomIdAsync } from '../../../app/authorization/server/functions/canAccessRoom'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; @@ -49,7 +51,10 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf protected name = 'video-conference'; // VideoConference.create: Start a video conference using the type and provider specified as arguments - public async create({ type, rid, createdBy, providerName, ...data }: VideoConferenceCreateData): Promise { + public async create( + { type, rid, createdBy, providerName, ...data }: VideoConferenceCreateData, + useAppUser = true, + ): Promise { const room = await Rooms.findOneById>(rid, { projection: { t: 1, uids: 1, name: 1, fname: 1 }, }); @@ -76,7 +81,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } const title = (data as Partial).title || room.fname || room.name || ''; - return this.startGroup(providerName, user, room._id, title, data); + return this.startGroup(providerName, user, room._id, title, data, useAppUser); } // VideoConference.start: Detect the desired type and provider then start a video conference using them @@ -99,7 +104,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf data.title = title; } - return this.create(data); + return this.create(data, false); } public async join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise { @@ -142,7 +147,9 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } if (call.messages.started) { - const text = TAPi18n.__('video_direct_missed', { username: call.createdBy.username as string }); + const name = + (settings.get('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || ''; + const text = TAPi18n.__('video_direct_missed', { username: name }); await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text)]); } @@ -270,7 +277,9 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } if (call.messages.started) { - const text = TAPi18n.__('video_livechat_missed', { username: call.createdBy.username as string }); + const name = + (settings.get('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || ''; + const text = TAPi18n.__('video_livechat_missed', { username: name }); await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text)]); } @@ -326,6 +335,43 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }; } + public async validateAction( + action: string, + caller: IUser['_id'], + { callId, uid, rid }: { callId: VideoConference['_id']; uid: IUser['_id']; rid: IRoom['_id'] }, + ): Promise { + if (!callId || !uid || !rid) { + return false; + } + + if (!(await canAccessRoomIdAsync(rid, caller)) || (caller !== uid && !(await canAccessRoomIdAsync(rid, uid)))) { + return false; + } + + const call = await VideoConferenceModel.findOneById>(callId, { + projection: { status: 1, endedAt: 1, createdBy: 1 }, + }); + + if (!call) { + return false; + } + + if (action === 'end') { + return true; + } + + if (call.endedAt || call.status > VideoConferenceStatus.STARTED) { + // If the caller is still calling about a call that has already ended, notify it + if (action === 'call' && caller === call.createdBy._id) { + Notifications.notifyUser(call.createdBy._id, 'video-conference.end', { rid, uid, callId }); + } + + return false; + } + + return true; + } + private async endCall(callId: VideoConference['_id']): Promise { const call = await this.getUnfiltered(callId); if (!call) { @@ -340,8 +386,6 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf switch (call.type) { case 'direct': return this.endDirectCall(call); - case 'videoconference': - return this.endGroupCall(call); } } @@ -359,17 +403,28 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf private async removeJoinButton(messageId: IMessage['_id']): Promise { await Messages.removeVideoConfJoinButton(messageId); + + const text = TAPi18n.__('Conference_call_has_ended'); + await Messages.addBlocksById(messageId, [this.buildMessageBlock(text)]); } private async endDirectCall(call: IDirectVideoConference): Promise { - if (!call.messages.ended) { - this.createDirectCallEndedMessage(call); - } - } + const params = { rid: call.rid, uid: call.createdBy._id, callId: call._id }; + + // Notify the caller that the call was ended by the server + Notifications.notifyUser(call.createdBy._id, 'video-conference.end', params); + + // If the callee hasn't joined the call yet, notify them that it has already ended + const subscriptions = await Subscriptions.findByRoomIdAndNotUserId(call.rid, call.createdBy._id, { + projection: { 'u._id': 1, '_id': 0 }, + }).toArray(); + + for (const subscription of subscriptions) { + if (call.users.find(({ _id }) => _id === subscription.u._id)) { + continue; + } - private async endGroupCall(call: IGroupVideoConference): Promise { - if (!call.messages.ended) { - this.createGroupCallEndedMessage(call); + Notifications.notifyUser(subscription.u._id, 'video-conference.end', params); } } @@ -388,7 +443,12 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return videoConfTypes.getTypeForRoom(room, allowRinging); } - private async createMessage(rid: IRoom['_id'], user: IUser, extraData: Partial = {}): Promise { + private async createMessage( + rid: IRoom['_id'], + providerName: string, + extraData: Partial = {}, + createdBy?: IUser, + ): Promise { const record = { msg: '', groupable: false, @@ -396,37 +456,51 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }; const room = await Rooms.findOneById(rid); + const appId = videoConfProviders.getProviderAppId(providerName); + const user = createdBy || (appId && (await Users.findOneByAppId(appId))) || (await Users.findOneById('rocket.cat')); const message = sendMessage(user, record, room, false); return message._id; } private async createDirectCallMessage(call: IDirectVideoConference, user: IUser): Promise { + const username = (settings.get('UI_Use_Real_Name') ? user.name : user.username) || user.username || ''; const text = TAPi18n.__('video_direct_calling', { - username: user.username || '', + username, }); - return this.createMessage(call.rid, user, { - blocks: [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)], - }); + return this.createMessage( + call.rid, + call.providerName, + { + blocks: [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)], + }, + user, + ); } - private async createGroupCallMessage(rid: IRoom['_id'], user: IUser, callId: string, title: string): Promise { - const text = TAPi18n.__('video_conference_started', { - conference: title, - username: user.username || '', + private async createGroupCallMessage(call: IGroupVideoConference, user: IUser, useAppUser = true): Promise { + const username = (settings.get('UI_Use_Real_Name') ? user.name : user.username) || user.username || ''; + const text = TAPi18n.__(useAppUser ? 'video_conference_started_by' : 'video_conference_started', { + conference: call.title || '', + username, }); - return this.createMessage(rid, user, { - blocks: [ - this.buildMessageBlock(text), - this.buildJoinButtonBlock(callId, title), - { - type: 'context', - elements: [], - }, - ], - } as Partial); + return this.createMessage( + call.rid, + call.providerName, + { + blocks: [ + this.buildMessageBlock(text), + this.buildJoinButtonBlock(call._id, call.title), + { + type: 'context', + elements: [], + }, + ], + } as Partial, + useAppUser ? undefined : user, + ); } private async validateProvider(providerName: string): Promise { @@ -465,74 +539,41 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }); } - private async createDirectCallEndedMessage(call: IDirectVideoConference): Promise { - const user = await Users.findOneById(call.endedBy?._id || call.createdBy._id); - if (!user) { - return; - } - - const text = - user._id === call.endedBy?._id - ? TAPi18n.__('video_direct_ended_by', { - username: user.username || '', - }) - : TAPi18n.__('video_direct_ended'); - - return this.createMessage(call.rid, user, { - blocks: [this.buildMessageBlock(text)], - } as Partial); - } - - private async createGroupCallEndedMessage(call: IGroupVideoConference): Promise { - const user = await Users.findOneById(call.endedBy?._id || call.createdBy._id); - if (!user) { - return; - } - - const text = - user._id === call.endedBy?._id - ? TAPi18n.__('video_conference_ended_by', { - conference: call.title, - username: user.username || '', - }) - : TAPi18n.__('video_conference_ended', { - conference: call.title, - }); - - return this.createMessage(call.rid, user, { - blocks: [this.buildMessageBlock(text)], - } as Partial); - } - - private async createLivechatMessage(rid: IRoom['_id'], user: IUser, callId: string, url: string): Promise { + private async createLivechatMessage(call: ILivechatVideoConference, user: IUser, url: string): Promise { + const username = (settings.get('UI_Use_Real_Name') ? user.name : user.username) || user.username || ''; const text = TAPi18n.__('video_livechat_started', { - username: user.username || '', + username, }); - return this.createMessage(rid, user, { - blocks: [ - this.buildMessageBlock(text), - { - type: 'actions', - appId: 'videoconf-core', - blockId: callId, - elements: [ - { - appId: 'videoconf-core', - blockId: callId, - actionId: 'joinLivechat', - type: 'button', - text: { - type: 'plain_text', - text: TAPi18n.__('Join_call'), - emoji: true, + return this.createMessage( + call.rid, + call.providerName, + { + blocks: [ + this.buildMessageBlock(text), + { + type: 'actions', + appId: 'videoconf-core', + blockId: call._id, + elements: [ + { + appId: 'videoconf-core', + blockId: call._id, + actionId: 'joinLivechat', + type: 'button', + text: { + type: 'plain_text', + text: TAPi18n.__('Join_call'), + emoji: true, + }, + url, }, - url, - }, - ], - }, - ], - }); + ], + }, + ], + }, + user, + ); } private buildMessageBlock(text: string): MessageSurfaceLayout[number] { @@ -540,9 +581,8 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf type: 'section', appId: 'videoconf-core', text: { - type: 'plain_text', - text, - emoji: true, + type: 'mrkdwn', + text: `${text}`, }, }; } @@ -637,6 +677,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf rid: IRoom['_id'], title: string, extraData?: Partial, + useAppUser = true, ): Promise { const callId = await VideoConferenceModel.createGroup({ ...extraData, @@ -649,7 +690,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }, providerName, }); - const call = await this.getUnfiltered(callId); + const call = (await this.getUnfiltered(callId)) as IGroupVideoConference | null; if (!call) { throw new Error('failed-to-create-group-call'); } @@ -659,11 +700,11 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf call.url = url; - const messageId = await this.createGroupCallMessage(rid, user, callId, title); + const messageId = await this.createGroupCallMessage(call, user, useAppUser); VideoConferenceModel.setMessageById(callId, 'started', messageId); if (call.ringing) { - await this.notifyUsersOfRoom(rid, user._id, 'video-conference.ring', { callId, title, createdBy: call.createdBy, providerName }); + await this.notifyUsersOfRoom(rid, user._id, 'video-conference.ring', { callId, rid, title, uid: call.createdBy, providerName }); } return { @@ -684,13 +725,13 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf providerName, }); - const call = await this.getUnfiltered(callId); + const call = (await this.getUnfiltered(callId)) as ILivechatVideoConference | null; if (!call) { throw new Error('failed-to-create-livechat-call'); } const joinUrl = await this.getUrl(call); - const messageId = await this.createLivechatMessage(rid, user, callId, joinUrl); + const messageId = await this.createLivechatMessage(call, user, joinUrl); await VideoConferenceModel.setMessageById(callId, 'started', messageId); return { @@ -914,7 +955,9 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf await VideoConferenceModel.setStatusById(call._id, VideoConferenceStatus.STARTED); if (call.messages.started) { - const text = TAPi18n.__('video_direct_started', { username: call.createdBy.username || '' }); + const username = + (settings.get('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || ''; + const text = TAPi18n.__('video_direct_started', { username }); await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)]); } } diff --git a/apps/meteor/server/startup/migrations/index.ts b/apps/meteor/server/startup/migrations/index.ts index 535f72d8aa29..3091a46c53ac 100644 --- a/apps/meteor/server/startup/migrations/index.ts +++ b/apps/meteor/server/startup/migrations/index.ts @@ -37,4 +37,5 @@ import './v275'; import './v276'; import './v277'; import './v278'; +import './v279'; import './xrun'; diff --git a/apps/meteor/server/startup/migrations/v279.ts b/apps/meteor/server/startup/migrations/v279.ts new file mode 100644 index 000000000000..824751b90d99 --- /dev/null +++ b/apps/meteor/server/startup/migrations/v279.ts @@ -0,0 +1,9 @@ +import { addMigration } from '../../lib/migrations'; +import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; + +addMigration({ + version: 279, + up() { + upsertPermissions(); + }, +}); diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 8e1505fd7101..e2809e30394a 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -1,4 +1,4 @@ -import type { Document, UpdateResult, FindCursor } from 'mongodb'; +import type { Document, UpdateResult, FindCursor, FindOptions } from 'mongodb'; import type { IUser, IRole, IRoom, ILivechatAgent } from '@rocket.chat/core-typings'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -44,6 +44,8 @@ export interface IUsersModel extends IBaseModel { findOneByLDAPId(id: any, attribute?: any): Promise; + findOneByAppId(appId: string, options?: FindOptions): Promise; + findLDAPUsers(options?: any): any; findConnectedLDAPUsers(options?: any): any; diff --git a/packages/ui-video-conf/.eslintrc b/packages/ui-video-conf/.eslintrc index afaf8e438ea9..1859ff825f63 100644 --- a/packages/ui-video-conf/.eslintrc +++ b/packages/ui-video-conf/.eslintrc @@ -1,73 +1,67 @@ { - "extends": ["@rocket.chat/eslint-config"], - "plugins": ["react", "react-hooks"], - "parser": "@babel/eslint-parser", + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "@rocket.chat/eslint-config/original", + "prettier", + "plugin:anti-trojan-source/recommended", + "plugin:react/jsx-runtime" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"], "rules": { + "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/indent": "off", + "@typescript-eslint/no-extra-parens": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/prefer-optional-chain": "warn", + "func-call-spacing": "off", + "import/named": "error", + "import/order": [ + "error", + { + "newlines-between": "always", + "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"]], + "alphabetize": { + "order": "asc" + } + } + ], + "indent": "off", "jsx-quotes": ["error", "prefer-single"], + "new-cap": ["error"], + "no-extra-parens": "off", + "no-spaced-func": "off", + "no-undef": "off", + "no-unused-vars": "off", + "no-useless-constructor": "off", + "no-use-before-define": "off", + "prefer-arrow-callback": ["error", { "allowNamedFunctions": true }], + "prettier/prettier": 2, "react/display-name": "error", - "react/self-closing-comp": "error", - "react/jsx-uses-react": "error", "react/jsx-uses-vars": "error", "react/jsx-no-undef": "error", "react/jsx-fragments": ["error", "syntax"], "react/no-multi-comp": "error", - "react/react-in-jsx-scope": "error", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" }, "settings": { "import/resolver": { "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] + "extensions": [".js", ".ts", ".tsx"] } }, "react": { "version": "detect" } }, - "env": { - "browser": true, - "es6": true - }, - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "extends": "../typescript", - "parser": "@typescript-eslint/parser", - "plugins": ["react", "react-hooks"], - "rules": { - "jsx-quotes": ["error", "prefer-single"], - "react/display-name": "error", - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error", - "react/jsx-no-undef": "error", - "react/jsx-fragments": ["error", "syntax"], - "react/no-multi-comp": "error", - "react/react-in-jsx-scope": "error", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn" - }, - "env": { - "browser": true, - "es6": true - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] - } - }, - "react": { - "version": "detect" - } - } - }, - { - "files": ["**/*.stories.js", "**/*.stories.jsx", "**/*.stories.ts", "**/*.stories.tsx"], - "rules": { - "react/display-name": "off", - "react/no-multi-comp": "off" - } - } - ] + "ignorePatterns": ["**/dist"] } diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 7b5c6770997e..1faf534385a7 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -10,6 +10,8 @@ "@rocket.chat/styled": "next", "@types/jest": "^27.4.1", "eslint": "^8.12.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" diff --git a/packages/ui-video-conf/src/VideoConfButton.tsx b/packages/ui-video-conf/src/VideoConfButton.tsx index 3a0b4f1afe5f..8ca73c154146 100644 --- a/packages/ui-video-conf/src/VideoConfButton.tsx +++ b/packages/ui-video-conf/src/VideoConfButton.tsx @@ -1,21 +1,20 @@ -import type { ReactNode, ReactElement, ButtonHTMLAttributes } from 'react'; import { Button, Icon, IconProps } from '@rocket.chat/fuselage'; +import type { ReactNode, ReactElement, ButtonHTMLAttributes } from 'react'; type VideoConfButtonProps = { icon?: IconProps['name']; primary?: boolean; + secondary?: boolean; danger?: boolean; disabled?: boolean; children: ReactNode; } & Omit, 'ref' | 'is' | 'className' | 'size' | 'elevation'>; -const VideoConfButton = ({ primary, danger, disabled, icon, children, ...props }: VideoConfButtonProps): ReactElement => { - return ( - - ); -}; +const VideoConfButton = ({ primary, secondary, danger, disabled, icon, children, ...props }: VideoConfButtonProps): ReactElement => ( + +); export default VideoConfButton; diff --git a/packages/ui-video-conf/src/VideoConfController.tsx b/packages/ui-video-conf/src/VideoConfController.tsx index 3ebc9a464404..3dcb991818eb 100644 --- a/packages/ui-video-conf/src/VideoConfController.tsx +++ b/packages/ui-video-conf/src/VideoConfController.tsx @@ -1,20 +1,30 @@ -import React from 'react'; -import type { ReactElement, ButtonHTMLAttributes } from 'react'; - import { IconButton } from '@rocket.chat/fuselage'; import type { IconProps } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import type { ReactElement, ButtonHTMLAttributes } from 'react'; type VideoConfControllerProps = { icon: IconProps['name']; active?: boolean; - text: string; + secondary?: boolean; + disabled?: boolean; + small?: boolean; } & Omit, 'ref' | 'is' | 'className' | 'size' | 'elevation'>; -const VideoConfController = ({ active, text, icon, ...props }: VideoConfControllerProps): ReactElement => { - const id = useUniqueId(); +const VideoConfController = ({ icon, active, secondary, disabled, small = true, ...props }: VideoConfControllerProps): ReactElement => { + const id = useUniqueId(); - return -} + return ( + + ); +}; export default VideoConfController; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx index c9e49384ad5a..3f93b1a278b1 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx @@ -1,29 +1,30 @@ -import React, { forwardRef } from 'react'; -import type { ReactNode, ReactElement, HTMLAttributes, Ref } from 'react'; -import styled from '@rocket.chat/styled'; import { Box } from '@rocket.chat/fuselage'; +import styled from '@rocket.chat/styled'; +import { forwardRef } from 'react'; +import type { ReactNode, ReactElement, HTMLAttributes, Ref } from 'react'; -export const VideoConfPopupContainer = styled( - 'div', - ({ position: _position, ...props }: { position?: number }) => - props -)` +export const VideoConfPopupContainer = styled('div', ({ position: _position, ...props }: { position?: number }) => props)` width: 100%; position: absolute; box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.15); - top: ${(p) => p.position ? `${p.position}px` : '0'}; - left: -${(p) => p.position ? `${p.position}px` : '0'}; + top: ${(p): string => (p.position ? `${p.position}px` : '0')}; + left: -${(p): string => (p.position ? `${p.position}px` : '0')}; `; type VideoConfPopupProps = { - children: ReactNode; + children: ReactNode; position?: number; } & HTMLAttributes; -const VideoConfPopup = forwardRef(function VideoConfPopup({ children, position }: VideoConfPopupProps, ref: Ref): ReactElement { +const VideoConfPopup = forwardRef(function VideoConfPopup( + { children, position }: VideoConfPopupProps, + ref: Ref, +): ReactElement { return ( - {children} + + {children} + ); }); diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupBackdrop.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupBackdrop.tsx index 64a891406e7e..5b1720c78dbc 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupBackdrop.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupBackdrop.tsx @@ -1,15 +1,23 @@ -import type { ReactNode } from 'react'; -import React from 'react'; -import { Box } from '@rocket.chat/fuselage'; import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import type { ReactNode, ReactElement } from 'react'; const backdropStyle = css` - position: fixed; - top: 0; - right: 0; - min-width: 276px; + position: fixed; + top: 0; + min-width: 276px; + [dir='ltr'] & { + right: 0; + } + [dir='rtl'] & { + left: 0; + } `; -const VideoConfPopupBackdrop = ({ children }: { children: ReactNode }) => {children} +const VideoConfPopupBackdrop = ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + +); export default VideoConfPopupBackdrop; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupClose.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupClose.tsx deleted file mode 100644 index 46920741856e..000000000000 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupClose.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Box, IconButton } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import React from 'react'; - -const VideoConfPopupClose = ({ onClick, title }: { onClick: () => void; title?: string }): ReactElement => ( - - - -); - -export default VideoConfPopupClose; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupContent.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupContent.tsx index 0a9e577c34d3..9912d42d805a 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupContent.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupContent.tsx @@ -1,11 +1,10 @@ import { Box } from '@rocket.chat/fuselage'; import type { ReactNode, ReactElement } from 'react'; -import React from 'react'; const VideoConfPopupContent = ({ children }: { children: ReactNode }): ReactElement => ( - - {children} - + + {children} + ); export default VideoConfPopupContent; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupControllers.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupControllers.tsx index 21a0107fc203..b94f7e17ad31 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupControllers.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupControllers.tsx @@ -1,7 +1,6 @@ -import React from 'react'; -import type { ReactNode, ReactElement } from 'react'; import { ButtonGroup } from '@rocket.chat/fuselage'; +import type { ReactNode, ReactElement } from 'react'; -const VideoConfPopupControllers = ({ children }: { children: ReactNode }): ReactElement => {children}; +const VideoConfPopupControllers = ({ children }: { children: ReactNode }): ReactElement => {children}; export default VideoConfPopupControllers; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooter.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooter.tsx index ed7c1aa44400..40f1745c63b8 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooter.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooter.tsx @@ -1,7 +1,6 @@ -import type { ReactNode } from 'react'; -import React from 'react'; import { Margins } from '@rocket.chat/fuselage'; +import type { ReactNode, ReactElement } from 'react'; -const VideoConfPopupFooter = ({ children }: { children: ReactNode }) => {children} +const VideoConfPopupFooter = ({ children }: { children: ReactNode }): ReactElement => {children}; export default VideoConfPopupFooter; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooterButtons.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooterButtons.tsx index 7bf0cf6537b4..cf9a4c5f4f54 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooterButtons.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooterButtons.tsx @@ -1,7 +1,10 @@ -import type { ReactNode } from 'react'; -import React from 'react'; import { ButtonGroup } from '@rocket.chat/fuselage'; +import type { ReactNode, ReactElement } from 'react'; -const VideoConfPopupFooterButtons = ({ children }: { children: ReactNode }) => {children} +const VideoConfPopupFooterButtons = ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + +); export default VideoConfPopupFooterButtons; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupHeader.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupHeader.tsx new file mode 100644 index 000000000000..721bd20da515 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupHeader.tsx @@ -0,0 +1,10 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactNode, ReactElement } from 'react'; + +const VideoConfPopupHeader = ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + +); + +export default VideoConfPopupHeader; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIcon.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIcon.tsx new file mode 100644 index 000000000000..d0ad146f7c54 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIcon.tsx @@ -0,0 +1,10 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactNode, ReactElement } from 'react'; + +const VideoConfPopupIcon = ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + +); + +export default VideoConfPopupIcon; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIndicators.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIndicators.tsx deleted file mode 100644 index 9c30f2cc2779..000000000000 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIndicators.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Box } from '@rocket.chat/fuselage'; - -const VideoConfPopupIndicators = ({ current, total }: { current: number; total: number }) => - {current} of {total} - -export default VideoConfPopupIndicators; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupInfo.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupInfo.tsx new file mode 100644 index 000000000000..8be2062ca998 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupInfo.tsx @@ -0,0 +1,24 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement, ReactNode } from 'react'; + +type VideoConfPopupInfoProps = { + avatar?: ReactElement; + icon?: ReactNode; + children: ReactNode; +}; + +const VideoConfPopupInfo = ({ avatar, icon, children }: VideoConfPopupInfoProps): ReactElement => ( + + {avatar} + {(icon || children) && ( + + {icon} + + {children} + + + )} + +); + +export default VideoConfPopupInfo; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupTitle.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupTitle.tsx index e565cfe45d47..5c724ae56ce3 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupTitle.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupTitle.tsx @@ -1,23 +1,16 @@ -import React from 'react'; -import type { ComponentProps } from 'react'; -import { Box, Icon, Throbber } from '@rocket.chat/fuselage'; +import { Box, Throbber } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; type VideoConfPopupTitleProps = { - text: string; - counter?: boolean; - icon?: ComponentProps['name']; + text: string; + counter?: boolean; }; -const VideoConfPopupTitle = ({ text, counter = false, icon }: VideoConfPopupTitleProps) => { - return ( - - {icon && } - - {text} - - {counter && } - - ); -} +const VideoConfPopupTitle = ({ text, counter = false }: VideoConfPopupTitleProps): ReactElement => ( + + {text} + {counter && } + +); export default VideoConfPopupTitle; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupUsername.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupUsername.tsx deleted file mode 100644 index 809688803e00..000000000000 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupUsername.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import React from 'react'; - -const VideoConfPopupUsername = ({ name, username }: { name?: string; username: string }): ReactElement => ( - - {name || username} - {name && ( - - {`(${username})`} - - )} - -); - -export default VideoConfPopupUsername; - - diff --git a/packages/ui-video-conf/src/VideoConfPopup/index.ts b/packages/ui-video-conf/src/VideoConfPopup/index.ts index ba9488dd26d4..319f72eabda9 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/index.ts +++ b/packages/ui-video-conf/src/VideoConfPopup/index.ts @@ -1,23 +1,23 @@ import VideoConfPopup from './VideoConfPopup'; -import VideoConfPopupContent from './VideoConfPopupContent'; import VideoConfPopupBackdrop from './VideoConfPopupBackdrop'; +import VideoConfPopupContent from './VideoConfPopupContent'; import VideoConfPopupControllers from './VideoConfPopupControllers'; import VideoConfPopupFooter from './VideoConfPopupFooter'; import VideoConfPopupFooterButtons from './VideoConfPopupFooterButtons'; +import VideoConfPopupHeader from './VideoConfPopupHeader'; +import VideoConfPopupIcon from './VideoConfPopupIcon'; +import VideoConfPopupInfo from './VideoConfPopupInfo'; import VideoConfPopupTitle from './VideoConfPopupTitle'; -import VideoConfPopupIndicators from './VideoConfPopupIndicators'; -import VideoConfPopupClose from './VideoConfPopupClose'; -import VideoConfPopupUsername from './VideoConfPopupUsername'; export { - VideoConfPopup, - VideoConfPopupContent, - VideoConfPopupTitle, - VideoConfPopupBackdrop, - VideoConfPopupControllers, - VideoConfPopupIndicators, - VideoConfPopupClose, - VideoConfPopupUsername, - VideoConfPopupFooter, - VideoConfPopupFooterButtons, -} + VideoConfPopup, + VideoConfPopupHeader, + VideoConfPopupIcon, + VideoConfPopupInfo, + VideoConfPopupContent, + VideoConfPopupTitle, + VideoConfPopupBackdrop, + VideoConfPopupControllers, + VideoConfPopupFooter, + VideoConfPopupFooterButtons, +}; diff --git a/packages/ui-video-conf/src/hooks/useVideoConfControllers.ts b/packages/ui-video-conf/src/hooks/useVideoConfControllers.ts index 1e44a35938f1..8e55ffa24d79 100644 --- a/packages/ui-video-conf/src/hooks/useVideoConfControllers.ts +++ b/packages/ui-video-conf/src/hooks/useVideoConfControllers.ts @@ -1,17 +1,26 @@ import { useCallback, useState } from 'react'; -export const useVideoConfControllers = (initialPreferences: { mic?: boolean; cam?: boolean } = { mic: true, cam: false }) => { - const [controllersConfig, setControllersConfig] = useState(initialPreferences); +type controllersConfigProps = { + mic?: boolean; + cam?: boolean; +}; - const handleToggleMic = useCallback((): void => { - setControllersConfig((prevState) => ({ ...prevState, mic: !prevState.mic })); - }, []); +export const useVideoConfControllers = ( + initialPreferences: controllersConfigProps = { mic: true, cam: false }, +): { controllersConfig: controllersConfigProps; handleToggleMic: () => void; handleToggleCam: () => void } => { + const [controllersConfig, setControllersConfig] = useState(initialPreferences); - const handleToggleCam = useCallback((): void => { - setControllersConfig((prevState) => ({ ...prevState, cam: !prevState.cam })); - }, []); + const handleToggleMic = useCallback((): void => { + setControllersConfig((prevState) => ({ ...prevState, mic: !prevState.mic })); + }, []); - return { - controllersConfig, handleToggleMic, handleToggleCam - } -} + const handleToggleCam = useCallback((): void => { + setControllersConfig((prevState) => ({ ...prevState, cam: !prevState.cam })); + }, []); + + return { + controllersConfig, + handleToggleMic, + handleToggleCam, + }; +}; diff --git a/packages/ui-video-conf/src/index.ts b/packages/ui-video-conf/src/index.ts index 02d4cb2b5155..e7244b55b214 100644 --- a/packages/ui-video-conf/src/index.ts +++ b/packages/ui-video-conf/src/index.ts @@ -3,4 +3,4 @@ import VideoConfController from './VideoConfController'; export * from './VideoConfPopup'; export * from './hooks'; -export { VideoConfButton, VideoConfController } +export { VideoConfButton, VideoConfController }; diff --git a/yarn.lock b/yarn.lock index 929d5a58c642..b3482d196900 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4301,6 +4301,8 @@ __metadata: "@rocket.chat/styled": next "@types/jest": ^27.4.1 eslint: ^8.12.0 + eslint-plugin-react: ^7.30.1 + eslint-plugin-react-hooks: ^4.6.0 jest: ^27.5.1 ts-jest: ^27.1.4 typescript: ~4.5.5 @@ -13704,6 +13706,30 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-react@npm:^7.30.1": + version: 7.30.1 + resolution: "eslint-plugin-react@npm:7.30.1" + dependencies: + array-includes: ^3.1.5 + array.prototype.flatmap: ^1.3.0 + doctrine: ^2.1.0 + estraverse: ^5.3.0 + jsx-ast-utils: ^2.4.1 || ^3.0.0 + minimatch: ^3.1.2 + object.entries: ^1.1.5 + object.fromentries: ^2.0.5 + object.hasown: ^1.1.1 + object.values: ^1.1.5 + prop-types: ^15.8.1 + resolve: ^2.0.0-next.3 + semver: ^6.3.0 + string.prototype.matchall: ^4.0.7 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + checksum: 553fb9ece6beb7c14cf6f84670c786c8ac978c2918421994dcc4edd2385302022e5d5ac4a39fafdb35954e29cecddefed61758040c3c530cafcf651f674a9d51 + languageName: node + linkType: hard + "eslint-plugin-testing-library@npm:^5.3.1": version: 5.5.1 resolution: "eslint-plugin-testing-library@npm:5.5.1"