diff --git a/apps/meteor/client/views/room/Header/ParentRoom/ParentDiscussion/ParentDiscussion.tsx b/apps/meteor/client/views/room/Header/ParentRoom/ParentDiscussion/ParentDiscussion.tsx index 3a0e4a471f2fa..b62e6e02df0b8 100644 --- a/apps/meteor/client/views/room/Header/ParentRoom/ParentDiscussion/ParentDiscussion.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoom/ParentDiscussion/ParentDiscussion.tsx @@ -1,18 +1,33 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { useRoomRoute } from '@rocket.chat/ui-client'; +import { useSetting } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; -import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; import ParentRoomButton from '../ParentRoomButton'; export type ParentDiscussionProps = { loading?: boolean; - room: Pick; + room: Pick; +}; + +const getChannelRoomName = (room: ParentDiscussionProps['room'], allowSpecialChars: boolean): string => { + if (room.prid || isRoomFederated(room)) { + return room.fname || ''; + } + + return (allowSpecialChars ? room.fname || room.name : room.name) || ''; }; const ParentDiscussion = ({ loading = false, room }: ParentDiscussionProps) => { const { t } = useTranslation(); - const roomName = roomCoordinator.getRoomName(room.t, room); - const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); + const goToRoom = useRoomRoute(); + const allowSpecialChars = useSetting('UI_Allow_room_names_with_special_chars', false); + const roomName = room.t === 'c' || room.t === 'p' ? getChannelRoomName(room, allowSpecialChars) : room.fname || room.name || ''; + + const handleRedirect = (): void => { + goToRoom({ rid: room._id, t: room.t, name: room.name }); + }; return ; }; diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx index 4aa389dd54b10..79fe45f200736 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx @@ -1,19 +1,21 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Margins } from '@rocket.chat/fuselage'; +import { useRouter } from '@rocket.chat/ui-contexts'; import RoomForewordUsernameListItem from './RoomForewordUsernameListItem'; -import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; export type RoomForewordUsernameListProps = { usernames: Array> }; const RoomForewordUsernameList = ({ usernames }: RoomForewordUsernameListProps) => { + const router = useRouter(); + return ( {usernames.map((username) => ( ))} diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx index 8f0ac94243756..87578964cd7bf 100644 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx @@ -1,5 +1,5 @@ import { getUserDisplayName, VideoConferenceStatus } from '@rocket.chat/core-typings'; -import { useGoToRoom, useSetting, useTranslation, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; import type * as UiKit from '@rocket.chat/ui-kit'; import { VideoConfMessageSkeleton, @@ -19,6 +19,7 @@ import type { MouseEventHandler } from 'react'; import { useContext, memo, useMemo } from 'react'; import { UiKitContext } from '../..'; +import { useGoToRoom } from './hooks/useGoToRoom'; import { useVideoConfDataStream } from './hooks/useVideoConfDataStream'; import { useSurfaceType } from '../../hooks/useSurfaceType'; import type { BlockProps } from '../../utils/BlockProps'; diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useGoToRoom.spec.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useGoToRoom.spec.tsx new file mode 100644 index 0000000000000..ba63737cc4f3d --- /dev/null +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useGoToRoom.spec.tsx @@ -0,0 +1,66 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { MockedRouterContext, MockedServerContext } from '@rocket.chat/mock-providers'; +import type { MockedRouterContextProps } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useGoToRoom } from './useGoToRoom'; + +const getWrapper = + (room: Partial | undefined, router?: MockedRouterContextProps['router']) => + ({ children }: { children: any }) => { + return ( + ({ room }) as any}> + {children} + + ); + }; + +describe('useGoToRoom', () => { + it('should not navigate if the room is not found', async () => { + const navigate = jest.fn(); + const { result } = renderHook(() => useGoToRoom(), { wrapper: getWrapper(undefined, { navigate }) }); + + await result.current('room-id'); + + expect(navigate).not.toHaveBeenCalled(); + }); + + it('should build the route by name for channels (c) and navigate to it', async () => { + const navigate = jest.fn(); + const getRoomRoute = jest.fn().mockReturnValue({ path: '/channel/some-channel' }); + const { result } = renderHook(() => useGoToRoom(), { + wrapper: getWrapper({ _id: 'room-id', t: 'c', name: 'some-channel' }, { navigate, getRoomRoute }), + }); + + await result.current('room-id'); + + await waitFor(() => expect(getRoomRoute).toHaveBeenCalledWith('c', { name: 'some-channel' })); + expect(navigate).toHaveBeenCalledWith({ pathname: '/channel/some-channel' }); + }); + + it('should build the route by name for private groups (p) and navigate to it', async () => { + const navigate = jest.fn(); + const getRoomRoute = jest.fn().mockReturnValue({ path: '/group/some-group' }); + const { result } = renderHook(() => useGoToRoom(), { + wrapper: getWrapper({ _id: 'room-id', t: 'p', name: 'some-group' }, { navigate, getRoomRoute }), + }); + + await result.current('room-id'); + + await waitFor(() => expect(getRoomRoute).toHaveBeenCalledWith('p', { name: 'some-group' })); + expect(navigate).toHaveBeenCalledWith({ pathname: '/group/some-group' }); + }); + + it('should build the route by rid for direct messages (d) and navigate to it', async () => { + const navigate = jest.fn(); + const getRoomRoute = jest.fn().mockReturnValue({ path: '/direct/room-id' }); + const { result } = renderHook(() => useGoToRoom(), { + wrapper: getWrapper({ _id: 'room-id', t: 'd', name: 'some-user' }, { navigate, getRoomRoute }), + }); + + await result.current('room-id'); + + await waitFor(() => expect(getRoomRoute).toHaveBeenCalledWith('d', { rid: 'room-id' })); + expect(navigate).toHaveBeenCalledWith({ pathname: '/direct/room-id' }); + }); +}); diff --git a/packages/ui-contexts/src/hooks/useGoToRoom.ts b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useGoToRoom.ts similarity index 71% rename from packages/ui-contexts/src/hooks/useGoToRoom.ts rename to packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useGoToRoom.ts index ff218097226bc..d4463829164e2 100644 --- a/packages/ui-contexts/src/hooks/useGoToRoom.ts +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useGoToRoom.ts @@ -1,15 +1,13 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useStableCallback } from '@rocket.chat/fuselage-hooks'; - -import { useEndpoint } from './useEndpoint'; -import { useRouter } from './useRouter'; +import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; export const useGoToRoom = (): ((roomId: IRoom['_id']) => Promise) => { const router = useRouter(); - const getRoomById = useEndpoint('GET', '/v1/rooms.info'); + const getRoomInfo = useEndpoint('GET', '/v1/rooms.info'); return useStableCallback(async (roomId: IRoom['_id']) => { - const { room } = await getRoomById({ roomId }); + const { room } = await getRoomInfo({ roomId }); if (!room) return; diff --git a/packages/ui-client/src/hooks/index.ts b/packages/ui-client/src/hooks/index.ts index 1427f4df332df..6801e83d84d4f 100644 --- a/packages/ui-client/src/hooks/index.ts +++ b/packages/ui-client/src/hooks/index.ts @@ -7,6 +7,7 @@ export * from './useFeaturePreviewList'; export * from './useGoToDirectMessage'; export * from './useLicense'; export * from './usePreferenceFeaturePreviewList'; +export * from './useRoomRoute'; export * from './useThemeMode'; export * from './useUserDisplayName'; export * from './useValidatePassword'; diff --git a/packages/ui-client/src/hooks/useRoomRoute.ts b/packages/ui-client/src/hooks/useRoomRoute.ts new file mode 100644 index 0000000000000..0a3829c6f3b1a --- /dev/null +++ b/packages/ui-client/src/hooks/useRoomRoute.ts @@ -0,0 +1,32 @@ +import type { IRoom, RoomType } from '@rocket.chat/core-typings'; +import { useRouter } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +type RoomRouteData = { + rid: IRoom['_id']; + t: RoomType; + name?: IRoom['name']; +}; + +/** + * Returns a function to navigate to a room using existing room data. + * Unlike `useGoToRoom`, this doesn't make an API call - use it when you already have the room data. + */ +export const useRoomRoute = ({ replace = false }: { replace?: boolean } = {}): ((room: RoomRouteData) => void) => { + const router = useRouter(); + + return useCallback( + (room: RoomRouteData) => { + const { t, name, rid } = room; + const { path } = router.getRoomRoute(t, ['c', 'p'].includes(t) ? { name } : { rid }); + + router.navigate( + { + pathname: path, + }, + { replace }, + ); + }, + [router, replace], + ); +}; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 7b070cbd6f940..7e8893d288791 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -37,7 +37,6 @@ export { useCurrentModal } from './hooks/useCurrentModal'; export { useCurrentRoutePath } from './hooks/useCurrentRoutePath'; export { useCustomSound } from './hooks/useCustomSound'; export { useEndpoint } from './hooks/useEndpoint'; -export { useGoToRoom } from './hooks/useGoToRoom'; export type { EndpointFunction } from './hooks/useEndpoint'; export { useIsLoggingIn } from './hooks/useIsLoggingIn'; export { useIsPrivilegedSettingsContext } from './hooks/useIsPrivilegedSettingsContext';