diff --git a/apps/meteor/client/views/mediaCallHistory/useMediaCallInternalHistoryActions.ts b/apps/meteor/client/views/mediaCallHistory/useMediaCallInternalHistoryActions.ts index d1a47d83119c7..5ae12a636ebea 100644 --- a/apps/meteor/client/views/mediaCallHistory/useMediaCallInternalHistoryActions.ts +++ b/apps/meteor/client/views/mediaCallHistory/useMediaCallInternalHistoryActions.ts @@ -1,10 +1,9 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useGoToDirectMessage } from '@rocket.chat/ui-client'; import { useRouter, useUserAvatarPath } from '@rocket.chat/ui-contexts'; import { useMediaCallContext } from '@rocket.chat/ui-voip'; import { useMemo } from 'react'; -import { useDirectMessageAction } from '../room/hooks/useUserInfoActions/actions/useDirectMessageAction'; - export type InternalCallHistoryContact = { _id: string; name?: string; @@ -48,21 +47,7 @@ export const useMediaCallInternalHistoryActions = ({ }); }); - const directMessage = useDirectMessageAction(contact, openRoomId ?? ''); - - const goToDirectMessage = useMemo(() => { - if (directMessage?.onClick) { - return directMessage.onClick; - } - if (!messageRoomId || openRoomId) { - return; - } - return () => - router.navigate({ - name: 'direct', - params: { rid: messageRoomId }, - }); - }, [directMessage?.onClick, messageRoomId, openRoomId, router]); + const goToDirectMessage = useGoToDirectMessage({ username: contact.username }, openRoomId ?? ''); const jumpToMessage = useEffectEvent(() => { const rid = messageRoomId || openRoomId; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useDirectMessageAction.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useDirectMessageAction.ts index 16cc8f8cfc2a8..7da7288d59bf1 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useDirectMessageAction.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useDirectMessageAction.ts @@ -1,55 +1,27 @@ -import type { IRoom, IUser, ISubscription } from '@rocket.chat/core-typings'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useTranslation, usePermission, useRoute, useUserSubscription, useUserSubscriptionByName } from '@rocket.chat/ui-contexts'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { useGoToDirectMessage } from '@rocket.chat/ui-client'; import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; -import type { UserInfoAction, UserInfoActionType } from '../useUserInfoActions'; - -const getShouldOpenDirectMessage = ( - currentSubscription?: ISubscription, - usernameSubscription?: ISubscription, - canOpenDirectMessage?: boolean, - username?: IUser['username'], -): boolean => { - const canOpenDm = canOpenDirectMessage || usernameSubscription; - const directMessageIsNotAlreadyOpen = currentSubscription && currentSubscription.name !== username; - return (canOpenDm && directMessageIsNotAlreadyOpen) ?? false; -}; +import type { UserInfoAction } from '../useUserInfoActions'; export const useDirectMessageAction = (user: Pick, rid: IRoom['_id']): UserInfoAction | undefined => { - const t = useTranslation(); - const usernameSubscription = useUserSubscriptionByName(user.username ?? ''); - const currentSubscription = useUserSubscription(rid); - const canOpenDirectMessage = usePermission('create-d'); - const directRoute = useRoute('direct'); + const { t } = useTranslation(); - const shouldOpenDirectMessage = getShouldOpenDirectMessage( - currentSubscription, - usernameSubscription, - canOpenDirectMessage, - user.username, - ); + const openDirectMessage = useGoToDirectMessage(user, rid); - const openDirectMessage = useEffectEvent( - () => - user.username && - directRoute.push({ - rid: user.username, - }), - ); + const openDirectMessageOption = useMemo(() => { + if (!openDirectMessage) { + return undefined; + } - const openDirectMessageOption = useMemo( - () => - shouldOpenDirectMessage - ? { - content: t('Direct_Message'), - icon: 'balloon' as const, - onClick: openDirectMessage, - type: 'communication' as UserInfoActionType, - } - : undefined, - [openDirectMessage, shouldOpenDirectMessage, t], - ); + return { + content: t('Direct_Message'), + icon: 'balloon' as const, + onClick: openDirectMessage, + type: 'communication', + } as const; + }, [openDirectMessage, t]); return openDirectMessageOption; }; diff --git a/packages/media-signaling/src/lib/Session.ts b/packages/media-signaling/src/lib/Session.ts index 51255099d1d95..3e52ba182c0b1 100644 --- a/packages/media-signaling/src/lib/Session.ts +++ b/packages/media-signaling/src/lib/Session.ts @@ -426,7 +426,20 @@ export class MediaSignalingSession extends Emitter { return this.hangupCallsThatNeedInput(); } - return this.setInputTrack(tracks[0]); + const inputTrack = tracks[0]; + + // If we no longer have a call that can use this track, just release it + if (inputTrack && !this.mayNeedInputTrack()) { + try { + // Stop the track so the browser doesn't have to wait for GC to detect that the stream is not in use + inputTrack.stop(); + } catch { + // we don't care if this failed + } + return; + } + + return this.setInputTrack(inputTrack); } private hangupCallsThatNeedInput(): void { @@ -445,14 +458,22 @@ export class MediaSignalingSession extends Emitter { } } - private async maybeStopInputTrack(): Promise { - this.config.logger?.debug('MediaSignalingSession.maybeStopInputTrack'); + private mayNeedInputTrack(): boolean { for (const call of this.knownCalls.values()) { if (call.mayNeedInputTrack()) { - return; + return true; } } + return false; + } + + private async maybeStopInputTrack(): Promise { + this.config.logger?.debug('MediaSignalingSession.maybeStopInputTrack'); + if (this.mayNeedInputTrack()) { + return; + } + await this.setInputTrack(null); } diff --git a/packages/ui-client/src/hooks/index.ts b/packages/ui-client/src/hooks/index.ts index ca95c68e10d85..428a4f8d65ee2 100644 --- a/packages/ui-client/src/hooks/index.ts +++ b/packages/ui-client/src/hooks/index.ts @@ -8,3 +8,4 @@ export * from './useLicense'; export * from './usePreferenceFeaturePreviewList'; export * from './useUserDisplayName'; export * from './useValidatePassword'; +export * from './useGoToDirectMessage'; diff --git a/packages/ui-client/src/hooks/useGoToDirectMessage.spec.ts b/packages/ui-client/src/hooks/useGoToDirectMessage.spec.ts new file mode 100644 index 0000000000000..48df789349be9 --- /dev/null +++ b/packages/ui-client/src/hooks/useGoToDirectMessage.spec.ts @@ -0,0 +1,49 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react'; + +import { useGoToDirectMessage } from './useGoToDirectMessage'; + +it('should return undefined if username is not provided', () => { + const { result } = renderHook(() => useGoToDirectMessage({}), { + wrapper: mockAppRoot().build(), + }); + + expect(result.current).toBe(undefined); +}); + +it("should return undefined if the user doesn't have permission to create direct messages and doesn't have a subscription with target user", () => { + const { result } = renderHook(() => useGoToDirectMessage({ username: 'test' }), { + wrapper: mockAppRoot().build(), + }); + + expect(result.current).toBe(undefined); +}); + +it('should return undefined if the room is already open', () => { + const { result } = renderHook(() => useGoToDirectMessage({ username: 'test' }, 'test-room'), { + wrapper: mockAppRoot() + .withSubscription({ _id: 'test-room', name: 'test', t: 'd', rid: 'test-room' } as SubscriptionWithRoom) + .build(), + }); + + expect(result.current).toBe(undefined); +}); + +it('should return a function to navigate to the direct message room if the user has permission to create direct messages and no subscription with target user', () => { + const { result } = renderHook(() => useGoToDirectMessage({ username: 'test' }), { + wrapper: mockAppRoot().withPermission('create-d').build(), + }); + + expect(typeof result.current).toBe('function'); +}); + +it("should return a function to navigate to the direct message room if the user has a subscription with target user and doesn't have permission to create direct messages", () => { + const { result } = renderHook(() => useGoToDirectMessage({ username: 'test' }), { + wrapper: mockAppRoot() + .withSubscription({ _id: 'test-room', name: 'test', t: 'd', rid: 'test-room' } as SubscriptionWithRoom) + .build(), + }); + + expect(typeof result.current).toBe('function'); +}); diff --git a/packages/ui-client/src/hooks/useGoToDirectMessage.ts b/packages/ui-client/src/hooks/useGoToDirectMessage.ts new file mode 100644 index 0000000000000..2a2e76cb2f4d8 --- /dev/null +++ b/packages/ui-client/src/hooks/useGoToDirectMessage.ts @@ -0,0 +1,39 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { usePermission, useUserSubscriptionByName, useRouter } from '@rocket.chat/ui-contexts'; + +// TODO: Routes type definitions are declared in-file for most places, so this route doesn't exist in this package +declare module '@rocket.chat/ui-contexts' { + export interface IRouterPaths { + direct: { + pathname: `/direct/:rid${`/${string}` | ''}${`/${string}` | ''}`; + pattern: '/direct/:rid/:tab?/:context?'; + }; + } +} + +/** + * Hook to navigate to a direct message room + * @param targetUser - Object containing the username of the user to navigate to + * @param openRoomId - Optional ID of the room that is already open + * @returns A function to navigate to the direct message room, or undefined if the room is already open or the user doesn't have permission to create direct messages and doesn't have a subscription to the target user + */ +export const useGoToDirectMessage = (targetUser: { username?: string }, openRoomId?: string): (() => void) | undefined => { + const usernameSubscription = useUserSubscriptionByName(targetUser.username ?? ''); + const router = useRouter(); + const canOpenDirectMessage = usePermission('create-d'); + + const hasPermissionOrSubscription = usernameSubscription || canOpenDirectMessage; + const alreadyOpen = openRoomId && usernameSubscription?.rid === openRoomId; + const shouldOpen = targetUser.username && hasPermissionOrSubscription && !alreadyOpen; + + const openDirectMessage = useEffectEvent( + () => + targetUser.username && + router.navigate({ + name: 'direct' as const, + params: { rid: targetUser.username }, + } as const), + ); + + return shouldOpen ? openDirectMessage : undefined; +};