diff --git a/client/hooks/usePresence.ts b/client/hooks/usePresence.ts index e95a8705994..e02fd9c2cc3 100644 --- a/client/hooks/usePresence.ts +++ b/client/hooks/usePresence.ts @@ -12,7 +12,7 @@ type Presence = 'online' | 'offline' | 'busy' | 'away' | 'loading'; * @returns UserPresence * @public */ -export const usePresence = (uid: string): UserPresence | undefined => { +export const usePresence = (uid: string | undefined): UserPresence | undefined => { const subscription = useMemo( () => ({ getCurrentValue: (): UserPresence | undefined => (uid ? Presence.store.get(uid) : undefined), diff --git a/client/hooks/useRoomIcon.tsx b/client/hooks/useRoomIcon.tsx index 4f07ba0e3dd..2e3510295f9 100644 --- a/client/hooks/useRoomIcon.tsx +++ b/client/hooks/useRoomIcon.tsx @@ -1,6 +1,7 @@ -import React, { ReactNode } from 'react'; +import { Icon } from '@rocket.chat/fuselage'; +import React, { ComponentProps, ReactElement } from 'react'; -import { IRoom, isDirectMessageRoom } from '../../definition/IRoom'; +import { IRoom } from '../../definition/IRoom'; import { ReactiveUserStatus } from '../components/UserStatus'; export const colors = { @@ -10,7 +11,9 @@ export const colors = { offline: 'neutral-600', }; -export const useRoomIcon = (room: IRoom): ReactNode | { name: string; color?: string } | null => { +export const useRoomIcon = ( + room: Pick, +): ReactElement | ComponentProps | null => { if (room.prid) { return { name: 'baloons' }; } @@ -19,13 +22,21 @@ export const useRoomIcon = (room: IRoom): ReactNode | { name: string; color?: st return { name: room.t === 'p' ? 'team-lock' : 'team' }; } - if (isDirectMessageRoom(room)) { + if (room.t === 'd') { if (room.uids && room.uids.length > 2) { return { name: 'balloon' }; } - if (room.u && room.uids && room.uids.length > 0) { - return uid !== room.u._id) || room.u._id} />; + + if (room.uids && room.uids.length > 0) { + const uid = room.uids.find((uid) => uid !== room?.u?._id) || room?.u?._id; + + if (!uid) { + return null; + } + + return ; } + return { name: 'at' }; } diff --git a/client/lib/RoomManager.ts b/client/lib/RoomManager.ts index e0467b98239..bdf1f114c30 100644 --- a/client/lib/RoomManager.ts +++ b/client/lib/RoomManager.ts @@ -149,10 +149,12 @@ export const useHandleRoom = (rid: IRoom['_id']): AsyncState const room = uid ? subscription || _room : _room; useEffect(() => { - if (room) { - update(); - resolve(room); + if (!room) { + return; } + + update(); + resolve(room); }, [resolve, update, room]); return state; diff --git a/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx b/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx index abb0db28278..e79c340f5a9 100644 --- a/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx +++ b/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx @@ -30,6 +30,7 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport, onClickCall */ const phoneNumber = Array.isArray(v?.phone) ? v?.phone[0]?.phoneNumber : v?.phone; const shouldShowWrapup = useMemo(() => lastMessage?.t === 'voip-call-wrapup' && lastMessage?.msg, [lastMessage]); const shouldShowTags = useMemo(() => tags && tags.length > 0, [tags]); + const _name = name || fname; return ( <> @@ -48,12 +49,12 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport, onClickCall */ {servedBy && } - {v && (name || fname) && ( + {v && _name && ( {t('Contact')} - - } /> + + } /> )} diff --git a/client/views/room/Header/DirectRoomHeader.js b/client/views/room/Header/DirectRoomHeader.js deleted file mode 100644 index 46895cfabb3..00000000000 --- a/client/views/room/Header/DirectRoomHeader.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -import { useUserId } from '../../../contexts/UserContext'; -import { usePresence } from '../../../hooks/usePresence'; -import RoomHeader from './RoomHeader'; - -const DirectRoomHeader = ({ room, slots }) => { - const userId = useUserId(); - const directUserId = room.uids.filter((uid) => uid !== userId).shift(); - const directUserData = usePresence(directUserId); - - return ; -}; - -export default DirectRoomHeader; diff --git a/client/views/room/Header/DirectRoomHeader.tsx b/client/views/room/Header/DirectRoomHeader.tsx new file mode 100644 index 00000000000..8052e71cd72 --- /dev/null +++ b/client/views/room/Header/DirectRoomHeader.tsx @@ -0,0 +1,32 @@ +import React, { ReactElement } from 'react'; + +import { IRoom } from '../../../../definition/IRoom'; +import { useUserId } from '../../../contexts/UserContext'; +import { usePresence } from '../../../hooks/usePresence'; +import RoomHeader from './RoomHeader'; + +type DirectRoomHeaderProps = { + room: IRoom; + slots: { + start?: unknown; + preContent?: unknown; + insideContent?: unknown; + posContent?: unknown; + end?: unknown; + toolbox?: { + pre?: unknown; + content?: unknown; + pos?: unknown; + }; + }; +}; + +const DirectRoomHeader = ({ room, slots }: DirectRoomHeaderProps): ReactElement => { + const userId = useUserId(); + const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); + const directUserData = usePresence(directUserId); + + return ; +}; + +export default DirectRoomHeader; diff --git a/client/views/room/Header/Header.js b/client/views/room/Header/Header.tsx similarity index 79% rename from client/views/room/Header/Header.js rename to client/views/room/Header/Header.tsx index ec48a739735..56535ea0fa4 100644 --- a/client/views/room/Header/Header.js +++ b/client/views/room/Header/Header.tsx @@ -1,5 +1,6 @@ -import React, { memo, useMemo } from 'react'; +import React, { memo, ReactElement, useMemo } from 'react'; +import { IRoom } from '../../../../definition/IRoom'; import BurgerMenu from '../../../components/BurgerMenu'; import TemplateHeader from '../../../components/Header'; import { useLayout } from '../../../contexts/LayoutContext'; @@ -8,7 +9,11 @@ import OmnichannelRoomHeader from './Omnichannel/OmnichannelRoomHeader'; import VoipRoomHeader from './Omnichannel/VoipRoomHeader'; import RoomHeader from './RoomHeader'; -const Header = ({ room }) => { +type HeaderProps = { + room: IRoom; +}; + +const Header = ({ room }: HeaderProps): ReactElement | null => { const { isMobile, isEmbedded, showTopNavbarEmbeddedLayout } = useLayout(); const slots = useMemo( @@ -26,7 +31,7 @@ const Header = ({ room }) => { return null; } - if (room.t === 'd' && room.uids?.length < 3) { + if (room.t === 'd' && (room.uids?.length ?? 0) < 3) { return ; } diff --git a/client/views/room/Header/HeaderIconWithRoom.js b/client/views/room/Header/HeaderIconWithRoom.tsx similarity index 62% rename from client/views/room/Header/HeaderIconWithRoom.js rename to client/views/room/Header/HeaderIconWithRoom.tsx index b52034bf121..9fc7696f313 100644 --- a/client/views/room/Header/HeaderIconWithRoom.js +++ b/client/views/room/Header/HeaderIconWithRoom.tsx @@ -1,15 +1,20 @@ -import React from 'react'; +import React, { ReactElement } from 'react'; -import { isOmnichannelRoom } from '../../../../definition/IRoom'; +import { IRoom, isOmnichannelRoom } from '../../../../definition/IRoom'; import Header from '../../../components/Header'; import { OmnichannelRoomIcon } from '../../../components/RoomIcon/OmnichannelRoomIcon'; import { useRoomIcon } from '../../../hooks/useRoomIcon'; -const HeaderIconWithRoom = ({ room }) => { +type HeaderIconWithRoomProps = { + room: IRoom; +}; + +const HeaderIconWithRoom = ({ room }: HeaderIconWithRoomProps): ReactElement => { const icon = useRoomIcon(room); if (isOmnichannelRoom(room)) { return ; } + return ; }; export default HeaderIconWithRoom; diff --git a/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx b/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx index f42a26371d5..6fa36eae4ec 100644 --- a/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx +++ b/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx @@ -7,12 +7,27 @@ import { useCurrentRoute } from '../../../../contexts/RouterContext'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; import { ToolboxActionConfig } from '../../lib/Toolbox'; import { ToolboxContext, useToolboxContext } from '../../lib/Toolbox/ToolboxContext'; -import RoomHeader, { RoomHeaderProps } from '../RoomHeader'; +import RoomHeader from '../RoomHeader'; import { BackButton } from './BackButton'; import QuickActions from './QuickActions'; import { useQuickActions } from './QuickActions/hooks/useQuickActions'; -const OmnichannelRoomHeader: FC = ({ slots: parentSlot }) => { +type OmnichannelRoomHeaderProps = { + slots: { + start?: unknown; + preContent?: unknown; + insideContent?: unknown; + posContent?: unknown; + end?: unknown; + toolbox?: { + pre?: unknown; + content?: unknown; + pos?: unknown; + }; + }; +}; + +const OmnichannelRoomHeader: FC = ({ slots: parentSlot }) => { const [name] = useCurrentRoute(); const { isMobile } = useLayout(); const room = useOmnichannelRoom(); diff --git a/client/views/room/Header/ParentRoom.js b/client/views/room/Header/ParentRoom.tsx similarity index 55% rename from client/views/room/Header/ParentRoom.js rename to client/views/room/Header/ParentRoom.tsx index df790cbbee6..8cdb6d01c7c 100644 --- a/client/views/room/Header/ParentRoom.js +++ b/client/views/room/Header/ParentRoom.tsx @@ -1,11 +1,16 @@ -import React from 'react'; +import React, { ReactElement } from 'react'; +import { IRoom } from '../../../../definition/IRoom'; import Header from '../../../components/Header'; import { useRoomIcon } from '../../../hooks/useRoomIcon'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -const ParentRoom = ({ room }) => { - const href = roomCoordinator.getRouteLink(room.t, room); +type ParentRoomProps = { + room: Pick; +}; + +const ParentRoom = ({ room }: ParentRoomProps): ReactElement => { + const href = roomCoordinator.getRouteLink(room.t, room) || undefined; const icon = useRoomIcon(room); return ( diff --git a/client/views/room/Header/ParentRoomWithData.js b/client/views/room/Header/ParentRoomWithData.js deleted file mode 100644 index 0ea0c26e288..00000000000 --- a/client/views/room/Header/ParentRoomWithData.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -import { useUserSubscription } from '../../../contexts/UserContext'; -import ParentRoom from './ParentRoom'; -import ParentRoomWithEndpointData from './ParentRoomWithEndpointData'; - -const ParentRoomWithData = ({ room }) => { - const subscription = useUserSubscription(room.prid); - - if (subscription) { - return ; - } - - return ; -}; - -export default ParentRoomWithData; diff --git a/client/views/room/Header/ParentRoomWithData.tsx b/client/views/room/Header/ParentRoomWithData.tsx new file mode 100644 index 00000000000..1276d1948aa --- /dev/null +++ b/client/views/room/Header/ParentRoomWithData.tsx @@ -0,0 +1,28 @@ +import React, { ReactElement } from 'react'; + +import { IRoom } from '../../../../definition/IRoom'; +import { useUserSubscription } from '../../../contexts/UserContext'; +import ParentRoom from './ParentRoom'; +import ParentRoomWithEndpointData from './ParentRoomWithEndpointData'; + +type ParentRoomWithDataProps = { + room: IRoom; +}; + +const ParentRoomWithData = ({ room }: ParentRoomWithDataProps): ReactElement => { + const { prid } = room; + + if (!prid) { + throw new Error('Parent room ID is missing'); + } + + const subscription = useUserSubscription(prid); + + if (subscription) { + return ; + } + + return ; +}; + +export default ParentRoomWithData; diff --git a/client/views/room/Header/ParentRoomWithEndpointData.js b/client/views/room/Header/ParentRoomWithEndpointData.js deleted file mode 100644 index 041e6496cee..00000000000 --- a/client/views/room/Header/ParentRoomWithEndpointData.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useEffect } from 'react'; - -import Header from '../../../components/Header'; -import { useEndpoint } from '../../../contexts/ServerContext'; -import { AsyncStatePhase, useAsyncState } from '../../../hooks/useAsyncState'; -import ParentRoom from './ParentRoom'; - -const ParentRoomWithEndpointData = ({ rid }) => { - const { resolve, reject, reset, phase, value } = useAsyncState(); - const getData = useEndpoint('GET', 'rooms.info'); - - useEffect(() => { - (async () => { - reset(); - getData({ roomId: rid }) - .then(resolve) - .catch((error) => { - reject(error); - }); - })(); - }, [reset, getData, rid, resolve, reject]); - - if (AsyncStatePhase.LOADING === phase) { - return ; - } - - if (AsyncStatePhase.ERROR === phase || !value?.room) { - return null; - } - - return ; -}; - -export default ParentRoomWithEndpointData; diff --git a/client/views/room/Header/ParentRoomWithEndpointData.tsx b/client/views/room/Header/ParentRoomWithEndpointData.tsx new file mode 100644 index 00000000000..e38eab8f777 --- /dev/null +++ b/client/views/room/Header/ParentRoomWithEndpointData.tsx @@ -0,0 +1,27 @@ +import React, { ReactElement } from 'react'; + +import { IRoom } from '../../../../definition/IRoom'; +import Header from '../../../components/Header'; +import { AsyncStatePhase } from '../../../hooks/useAsyncState'; +import { useEndpointData } from '../../../hooks/useEndpointData'; +import ParentRoom from './ParentRoom'; + +type ParentRoomWithEndpointDataProps = { + rid: IRoom['_id']; +}; + +const ParentRoomWithEndpointData = ({ rid }: ParentRoomWithEndpointDataProps): ReactElement | null => { + const { phase, value } = useEndpointData('rooms.info', { roomId: rid }); + + if (AsyncStatePhase.LOADING === phase) { + return ; + } + + if (AsyncStatePhase.REJECTED === phase || !value?.room) { + return null; + } + + return ; +}; + +export default ParentRoomWithEndpointData; diff --git a/client/views/room/Header/ParentTeam.js b/client/views/room/Header/ParentTeam.tsx similarity index 65% rename from client/views/room/Header/ParentTeam.js rename to client/views/room/Header/ParentTeam.tsx index 3cda611fa1c..a9a6e49ced5 100644 --- a/client/views/room/Header/ParentTeam.js +++ b/client/views/room/Header/ParentTeam.tsx @@ -1,5 +1,6 @@ -import React, { useMemo } from 'react'; +import React, { ReactElement, useMemo } from 'react'; +import { IRoom } from '../../../../definition/IRoom'; import { TEAM_TYPE } from '../../../../definition/ITeam'; import Header from '../../../components/Header'; import { useUserId } from '../../../contexts/UserContext'; @@ -7,12 +8,25 @@ import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; -const ParentTeam = ({ room }) => { +type ParentTeamProps = { + room: IRoom; +}; + +const ParentTeam = ({ room }: ParentTeamProps): ReactElement | null => { + const { teamId } = room; const userId = useUserId(); + if (!teamId) { + throw new Error('invalid rid'); + } + + if (!userId) { + throw new Error('invalid uid'); + } + const { value, phase } = useEndpointData( 'teams.info', - useMemo(() => ({ teamId: room.teamId }), [room.teamId]), + useMemo(() => ({ teamId }), [teamId]), ); const { value: userTeams, phase: userTeamsPhase } = useEndpointData( @@ -20,15 +34,23 @@ const ParentTeam = ({ room }) => { useMemo(() => ({ userId }), [userId]), ); - const belongsToTeam = userTeams?.teams?.find((team) => team._id === room.teamId) || false; + const belongsToTeam = userTeams?.teams?.find((team) => team._id === teamId) || false; const isTeamPublic = value?.teamInfo.type === TEAM_TYPE.PUBLIC; - const teamMainRoomHref = () => goToRoomById(value?.teamInfo.roomId); + const teamMainRoomHref = (): void => { + const rid = value?.teamInfo.roomId; + + if (!rid) { + return; + } + + goToRoomById(rid); + }; if (phase === AsyncStatePhase.LOADING || userTeamsPhase === AsyncStatePhase.LOADING) { return ; } - if (phase === AsyncStatePhase.REJECTED || !value.teamInfo) { + if (phase === AsyncStatePhase.REJECTED || !value?.teamInfo) { return null; } diff --git a/client/views/room/Header/RoomTitle.tsx b/client/views/room/Header/RoomTitle.tsx index d65d40d26d4..435537b656c 100644 --- a/client/views/room/Header/RoomTitle.tsx +++ b/client/views/room/Header/RoomTitle.tsx @@ -4,7 +4,11 @@ import { IRoom } from '../../../../definition/IRoom'; import Header from '../../../components/Header'; import HeaderIconWithRoom from './HeaderIconWithRoom'; -const RoomTitle = ({ room }: { room: IRoom }): ReactElement => ( +type RoomTitleProps = { + room: IRoom; +}; + +const RoomTitle = ({ room }: RoomTitleProps): ReactElement => ( <> {room.name} diff --git a/client/views/room/providers/ToolboxProvider.tsx b/client/views/room/providers/ToolboxProvider.tsx index d8e1ee41c65..650695b3126 100644 --- a/client/views/room/providers/ToolboxProvider.tsx +++ b/client/views/room/providers/ToolboxProvider.tsx @@ -71,7 +71,7 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom open('room-info', username); break; case 'd': - room.uids?.length > 2 ? open('user-info-group', username) : open('user-info', username); + (room.uids?.length ?? 0) > 2 ? open('user-info-group', username) : open('user-info', username); break; default: open('members-list', username); diff --git a/client/views/room/providers/VirtualAction.tsx b/client/views/room/providers/VirtualAction.tsx index 0d813766af9..0f4fe74b89b 100644 --- a/client/views/room/providers/VirtualAction.tsx +++ b/client/views/room/providers/VirtualAction.tsx @@ -1,10 +1,11 @@ import { useLayoutEffect, memo } from 'react'; import { IRoom } from '../../../../definition/IRoom'; +import { RoomType } from '../../../../definition/RoomType'; import { Store } from '../lib/Toolbox/generator'; import { ToolboxAction } from '../lib/Toolbox/index'; -const groupsDict = { +const groupsDict: Record = { l: 'live', v: 'voip', d: 'direct', @@ -18,7 +19,7 @@ const getGroup = (room: IRoom): string => { return 'team'; } - if (group === groupsDict.d && room.uids?.length > 2) { + if (group === groupsDict.d && (room.uids?.length ?? 0) > 2) { return 'direct_multiple'; } diff --git a/definition/IRoom.ts b/definition/IRoom.ts index aa5bb8bbe18..6d266228a4c 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -1,8 +1,8 @@ import { IRocketChatRecord } from './IRocketChatRecord'; import { IMessage } from './IMessage'; import { IUser, Username } from './IUser'; +import { RoomType } from './RoomType'; -export type RoomType = 'c' | 'd' | 'p' | 'l' | 'v'; type CallStatus = 'ringing' | 'ended' | 'declined' | 'ongoing'; export type RoomID = string; @@ -18,7 +18,7 @@ export interface IRoom extends IRocketChatRecord { _id: RoomID; t: RoomType; name?: string; - fname: string; + fname?: string; msgs: number; default?: true; broadcast?: true; @@ -27,7 +27,7 @@ export interface IRoom extends IRocketChatRecord { topic?: any; u: Pick; - uids: Array; + uids?: Array; lastMessage?: IMessage; lm?: Date; @@ -229,3 +229,6 @@ export const isOmnichannelRoomFromAppSource = (room: IRoom): room is IOmnichanne return room.source?.type === OmnichannelSourceType.APP; }; + +/** @deprecated */ +export { RoomType }; diff --git a/definition/ISubscription.ts b/definition/ISubscription.ts index a528bfb3f51..9ed8b2dc5d9 100644 --- a/definition/ISubscription.ts +++ b/definition/ISubscription.ts @@ -1,7 +1,6 @@ import { IRocketChatRecord } from './IRocketChatRecord'; import { IUser } from './IUser'; - -type RoomType = 'c' | 'd' | 'p' | 'l'; +import { RoomType } from './RoomType'; type RoomID = string; @@ -40,7 +39,7 @@ export interface ISubscription extends IRocketChatRecord { E2EKey?: string; unreadAlert?: 'default' | 'all' | 'mentions' | 'nothing'; - fname?: unknown; + fname?: string; code?: unknown; archived?: unknown; diff --git a/definition/RoomType.ts b/definition/RoomType.ts new file mode 100644 index 00000000000..9b2d3782ff4 --- /dev/null +++ b/definition/RoomType.ts @@ -0,0 +1 @@ +export type RoomType = 'c' | 'd' | 'p' | 'l' | 'v';