diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index cab77ce07ef90..66a5ff9304009 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -1,4 +1,4 @@ -import { MeteorError, Presence, Team, Calendar } from '@rocket.chat/core-services'; +import { MeteorError, Presence, Team } from '@rocket.chat/core-services'; import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings'; import { Users, Subscriptions, Sessions, OAuthAccessTokens, OAuthRefreshTokens, OAuthAuthCodes } from '@rocket.chat/models'; import { @@ -29,7 +29,7 @@ import { validateForbiddenErrorResponse, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; +import { getLoginExpirationInMs } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -1546,7 +1546,7 @@ API.v1.get( if (ids) { return API.v1.success({ - users: await Users.findNotOfflineByIds(Array.isArray(ids) ? ids : ids.split(','), options).toArray(), + users: await Users.findPresenceUsersByIds(Array.isArray(ids) ? ids : ids.split(','), options).toArray(), full: false, }); } @@ -1982,12 +1982,6 @@ API.v1 ), ); - if (!settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { - method: 'users.setStatus', - }); - } - const user = await (async () => { if (isUserFromParams(this.bodyParams, this.userId, this.user)) { return Users.findOneById(this.userId); @@ -2003,6 +1997,12 @@ API.v1 const { status, message, expiresAt } = this.bodyParams; + if (message && !settings.get('Accounts_AllowUserStatusMessageChange')) { + throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { + method: 'users.setStatus', + }); + } + const statusExpiresAt = expiresAt ? new Date(expiresAt) : undefined; if (statusExpiresAt && Number.isNaN(statusExpiresAt.getTime())) { throw new Meteor.Error('error-invalid-date', 'Invalid expiresAt date string', { @@ -2028,10 +2028,6 @@ API.v1 await Presence.setStatus(user._id, effectiveStatus, message, statusExpiresAt); - if (status) { - void wrapExceptions(() => Calendar.cancelUpcomingStatusChanges(user._id)).suppress(); - } - return API.v1.success(); }, ) diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 1caa744316e7b..4a49f6b613e9d 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -181,7 +181,7 @@ export class AppUserBridge extends UserBridge { protected async setActiveState( userId: IUser['id'], - state: Pick, + state: Pick, appId: string, ): Promise { this.orch.debugLog(`The App ${appId} is setting active state for user ${userId}`); @@ -191,13 +191,14 @@ export class AppUserBridge extends UserBridge { statusText: state.statusText, statusSource: state.statusSource as PresenceSource, ...(state.statusExpiresAt && { statusExpiresAt: state.statusExpiresAt }), + ...(state.statusId && { statusId: state.statusId }), }); } - protected async endActiveState(userId: IUser['id'], appId: string): Promise { + protected async endActiveState(userId: IUser['id'], appId: string, statusId?: string): Promise { this.orch.debugLog(`The App ${appId} is ending active state for user ${userId}`); - await Presence.endActiveState(userId); + await Presence.endActiveState(userId, statusId); } protected async getActiveUserCount(): Promise { diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index 8a042c0733e72..b44fd11201b4d 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -49,7 +49,7 @@ const UserCard = ({ const isLayoutEmbedded = useEmbeddedLayout(); return ( - +
{username && } diff --git a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx index 81487409ddbb3..c6e6b07107bf3 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx @@ -1,9 +1,10 @@ import { ContextualbarDialog } from '@rocket.chat/ui-client'; import type { Meta, StoryFn } from '@storybook/react'; -import * as Status from '../UserStatus'; import UserInfo from './UserInfo'; import { UserCardRole } from '../UserCard'; +import * as Status from '../UserStatus'; +import { UserStatusText } from '../UserStatusText'; export default { component: UserInfo, @@ -24,10 +25,10 @@ const defaultArgs = { name: 'Guilherme Gazzo', username: 'guilherme.gazzo', nickname: 'gazzo', - statusText: '🛴 currently working on User Card', bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus.', email: 'rocketchat@rocket.chat', status: , + customStatus: , roles: [admin, user], }; diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index 1fabbdb131ed6..8ecf4b6b697fc 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -37,7 +37,6 @@ type UserInfoDataProps = Serialized< | 'utcOffset' | 'phone' | 'createdAt' - | 'statusText' | 'canViewAllInfo' | 'customFields' | 'freeSwitchExtension' @@ -47,6 +46,7 @@ type UserInfoDataProps = Serialized< type UserInfoProps = UserInfoDataProps & { status: ReactNode; + customStatus?: ReactNode; email?: string; verified?: boolean; actions: ReactNode; @@ -69,7 +69,7 @@ const UserInfo = ({ verified, createdAt, status, - statusText, + customStatus, customFields, canViewAllInfo, actions, @@ -100,11 +100,7 @@ const UserInfo = ({ {userDisplayName && } - {statusText && ( - - - - )} + {customStatus && {customStatus}} diff --git a/apps/meteor/client/components/UserStatusMenu.tsx b/apps/meteor/client/components/UserStatusMenu.tsx index 26d78e2c6a32d..42cd249276e1a 100644 --- a/apps/meteor/client/components/UserStatusMenu.tsx +++ b/apps/meteor/client/components/UserStatusMenu.tsx @@ -9,7 +9,6 @@ import { useTranslation } from 'react-i18next'; import { UserStatus } from './UserStatus'; type UserStatusMenuProps = { - margin: ComponentProps['margin']; onChange: (type: UserStatusType) => void; initialStatus?: UserStatusType; optionWidth?: ComponentProps['width']; @@ -17,11 +16,10 @@ type UserStatusMenuProps = { }; const UserStatusMenu = ({ - margin, onChange, initialStatus = UserStatusType.OFFLINE, optionWidth = undefined, - placement = 'bottom-end', + placement = 'bottom-start', }: UserStatusMenuProps) => { const { t } = useTranslation(); const [status, setStatus] = useState(initialStatus); @@ -39,16 +37,21 @@ const UserStatusMenu = ({ const statuses: Array<[value: UserStatusType, label: ReactNode]> = [ [UserStatusType.ONLINE, renderOption(UserStatusType.ONLINE, t('Online'))], - [UserStatusType.AWAY, renderOption(UserStatusType.AWAY, t('Away'))], [UserStatusType.BUSY, renderOption(UserStatusType.BUSY, t('Busy'))], ]; + // Away is no longer manually selectable, but surface it if the user is currently on it + // (e.g., set in a previous version or auto-applied by the server) so they can switch off. + if (status === UserStatusType.AWAY) { + statuses.push([UserStatusType.AWAY, renderOption(UserStatusType.AWAY, t('Away'))]); + } + if (allowInvisibleStatus) { statuses.push([UserStatusType.OFFLINE, renderOption(UserStatusType.OFFLINE, t('Offline'))]); } return statuses; - }, [t, allowInvisibleStatus]); + }, [t, allowInvisibleStatus, status]); const [cursor, handleKeyDown, handleKeyUp, reset, [visible, hide, show]] = useCursor(-1, options, ([selected], [, hide]) => { setStatus(selected); @@ -82,14 +85,13 @@ const UserStatusMenu = ({ <> - - - - + + + {t('Status')} + + ( + } + /> + } + /> + )} + /> + + {t(allowUserStatusMessageChange ? 'Status_you_can_use_emoji' : 'StatusMessage_Change_Disabled')} + {errors.statusText && {errors.statusText.message}} + + + {t('Status_clear_after')} + + + validateStatusExpiration(value, { statusCustomDate, statusCustomTime }, t), + }} + render={({ field: { value, onChange } }) => ( + onChange(String(next))} + /> + )} + /> + + {statusDuration === 'custom' && ( + + + ( + ) => onChange(e.currentTarget.value)} + min={new Date().toLocaleDateString('en-CA')} + /> + )} + /> + ( + ) => onChange(e.currentTarget.value)} + /> + )} + /> + + + )} + {errors.statusDuration && {errors.statusDuration.message}} + {t('Status_new_status_warning')} + {t('Nickname')} diff --git a/apps/meteor/client/views/account/profile/getProfileInitialValues.ts b/apps/meteor/client/views/account/profile/getProfileInitialValues.ts index a15e8477d48db..a1132a7b0d618 100644 --- a/apps/meteor/client/views/account/profile/getProfileInitialValues.ts +++ b/apps/meteor/client/views/account/profile/getProfileInitialValues.ts @@ -1,6 +1,8 @@ import type { AvatarObject, IUser } from '@rocket.chat/core-typings'; import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; +import type { UserStatusInitialValues } from '../../../lib/getUserInitialStatus'; +import { getUserStatusInitialValues } from '../../../lib/getUserInitialStatus'; export type AccountProfileFormValues = { email: string; @@ -8,22 +10,21 @@ export type AccountProfileFormValues = { username: string; avatar: AvatarObject; url: string; - statusText: string; - statusType: string; bio: string; customFields: Record; nickname: string; -}; +} & UserStatusInitialValues; -export const getProfileInitialValues = (user: IUser | null): AccountProfileFormValues => ({ - email: user ? getUserEmailAddress(user) || '' : '', - name: user?.name ?? '', - username: user?.username ?? '', - avatar: '' as AvatarObject, - url: '', - statusText: user?.statusText ?? '', - statusType: user?.status ?? '', - bio: user?.bio ?? '', - customFields: user?.customFields ?? {}, - nickname: user?.nickname ?? '', -}); +export const getProfileInitialValues = (user: IUser | null): AccountProfileFormValues => { + return { + email: user ? getUserEmailAddress(user) || '' : '', + name: user?.name ?? '', + username: user?.username ?? '', + avatar: '' as AvatarObject, + url: '', + bio: user?.bio ?? '', + customFields: user?.customFields ?? {}, + nickname: user?.nickname ?? '', + ...getUserStatusInitialValues(user), + }; +}; diff --git a/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx index bd4aafe002f4d..3c86835dd8964 100644 --- a/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx @@ -13,6 +13,7 @@ import { FormSkeleton } from '../../../components/Skeleton'; import { UserCardRole } from '../../../components/UserCard'; import { UserInfo } from '../../../components/UserInfo'; import { UserStatus } from '../../../components/UserStatus'; +import { UserStatusText } from '../../../components/UserStatusText'; import { getUserEmailVerified } from '../../../lib/utils/getUserEmailVerified'; type AdminUserInfoWithDataProps = { @@ -60,6 +61,7 @@ const AdminUserInfoWithData = ({ uid, onReload, tab }: AdminUserInfoWithDataProp roles = [], status, statusText, + statusExpiresAt, bio, utcOffset, lastLogin, @@ -88,7 +90,7 @@ const AdminUserInfoWithData = ({ uid, onReload, tab }: AdminUserInfoWithDataProp email: getUserEmailAddress(data.user), createdAt, status: , - statusText, + customStatus: , nickname, reason, freeSwitchExtension, diff --git a/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItem.tsx b/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItem.tsx index f94a30524625e..4a4c7541ec6f5 100644 --- a/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItem.tsx +++ b/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItem.tsx @@ -25,7 +25,7 @@ const SidebarItem = ({ icon, title, actions, unread, menu, badges, room, ...prop const { mounted: menuVisibility, requestMount, mountNow } = useDeferredMenuMount(); return ( - + diff --git a/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItemWithData.tsx b/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItemWithData.tsx index 79b933e59ca3d..39ff4b678b081 100644 --- a/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItemWithData.tsx +++ b/apps/meteor/client/views/navigation/sidebar/RoomList/SidebarItemWithData.tsx @@ -2,13 +2,16 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { SidebarV2Action, SidebarV2Actions, SidebarV2ItemIcon } from '@rocket.chat/fuselage'; import { useButtonPattern } from '@rocket.chat/fuselage-hooks'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { useUserId } from '@rocket.chat/ui-contexts'; import type { TFunction } from 'i18next'; import type { AllHTMLAttributes } from 'react'; import { memo, useMemo } from 'react'; import SidebarItem from './SidebarItem'; import { RoomIcon } from '../../../../components/RoomIcon'; +import { useUserStatusTooltip } from '../../../../hooks/useUserStatusTooltip'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { getUidDirectMessage } from '../../../../lib/utils/getUidDirectMessage'; import { useRoomsListContext, useIsRoomFilter, useRedirectToFilter } from '../../contexts/RoomsNavigationContext'; import SidebarItemBadges from '../badges/SidebarItemBadges'; import { useUnreadDisplay } from '../hooks/useUnreadDisplay'; @@ -32,6 +35,9 @@ const SidebarItemWithData = ({ room, id, style, t, videoConfActions }: RoomListR const title = roomCoordinator.getRoomName(room.t, room) || ''; const href = roomCoordinator.getRouteLink(room.t, room) || ''; + const dmUserId = getUidDirectMessage(room, useUserId()); + const dmStatusTooltipHandlers = useUserStatusTooltip(dmUserId, title); + const { unreadTitle, showUnread, highlightUnread: highlighted } = useUnreadDisplay(room); const icon = ( @@ -76,6 +82,7 @@ const SidebarItemWithData = ({ room, id, style, t, videoConfActions }: RoomListR room={room} actions={actions} {...buttonProps} + {...dmStatusTooltipHandlers} /> ); }; diff --git a/apps/meteor/client/views/room/Header/RoomHeader.tsx b/apps/meteor/client/views/room/Header/RoomHeader.tsx index 3d0cb83ebbcc6..0f6f1ad3c2f2a 100644 --- a/apps/meteor/client/views/room/Header/RoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/RoomHeader.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'; import FederatedRoomOriginServer from './FederatedRoomOriginServer'; import ParentRoom from './ParentRoom'; +import RoomMemberStatus from './RoomMemberStatus'; import RoomTitle from './RoomTitle'; import RoomToolbox from './RoomToolbox'; import RoomTopic from './RoomTopic'; @@ -47,6 +48,7 @@ const RoomHeader = ({ room, slots = {} }: RoomHeaderProps) => { + {slots?.insideContent} diff --git a/apps/meteor/client/views/room/Header/RoomMemberStatus.spec.tsx b/apps/meteor/client/views/room/Header/RoomMemberStatus.spec.tsx new file mode 100644 index 0000000000000..dd7502b31edc6 --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomMemberStatus.spec.tsx @@ -0,0 +1,122 @@ +import { UserStatus } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { render, screen } from '@testing-library/react'; + +import RoomMemberStatus from './RoomMemberStatus'; +import FakeRoomProvider from '../../../../tests/mocks/client/FakeRoomProvider'; +import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../tests/mocks/data'; + +const user = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + status: UserStatus.ONLINE, +}); + +const user2 = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + statusText: 'Sample Status', + status: UserStatus.ONLINE, +}); + +const user3 = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + status: UserStatus.ONLINE, +}); + +const userWithExpiration = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + statusText: 'In a meeting', + statusExpiresAt: new Date(Date.now() + 60 * 60 * 1000), + status: UserStatus.BUSY, +}); + +describe('RoomMemberStatus', () => { + it('should render the statusText of the DM partner', () => { + const room = createFakeRoom({ t: 'd', uids: [user._id, user2._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withUsers([user2]) + .build(), + }, + ); + + expect(screen.getByText('Sample Status')).toBeInTheDocument(); + }); + + it('should expose the expiration text on the title attribute when the partner has one', () => { + const room = createFakeRoom({ t: 'd', uids: [user._id, userWithExpiration._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withUsers([userWithExpiration]) + .build(), + }, + ); + + const wrapper = screen.getByText('In a meeting').closest('[title]'); + expect(wrapper).toHaveAttribute('title'); + expect(wrapper?.getAttribute('title')).not.toBe(''); + }); + + it('should not render statusText for multi-user DMs (3+ participants)', () => { + const room = createFakeRoom({ t: 'd', uids: [user._id, user2._id, user3._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withUsers([user2, user3]) + .build(), + }, + ); + + expect(screen.queryByText('Sample Status')).not.toBeInTheDocument(); + }); + + it('should not render statusText for non-DM rooms', () => { + const room = createFakeRoom({ t: 'c' }); + const subscription = createFakeSubscription({ t: 'c', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .build(), + }, + ); + + expect(screen.queryByText('Sample Status')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/meteor/client/views/room/Header/RoomMemberStatus.tsx b/apps/meteor/client/views/room/Header/RoomMemberStatus.tsx new file mode 100644 index 0000000000000..60ec21bc7c57f --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomMemberStatus.tsx @@ -0,0 +1,30 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; +import { useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; + +import MarkdownText from '../../../components/MarkdownText'; +import { useExpirationText } from '../../../hooks/useExpirationText'; +import { getUidDirectMessage } from '../../../lib/utils/getUidDirectMessage'; + +type RoomMemberStatusProps = { + room: IRoom; +}; + +const RoomMemberStatus = ({ room }: RoomMemberStatusProps): ReactElement | null => { + const directUserId = getUidDirectMessage(room, useUserId()); + const presence = useUserPresence(directUserId); + const expirationText = useExpirationText(presence?.statusExpiresAt); + + if (!presence?.statusText && !presence?.statusExpiresAt) { + return null; + } + + return ( + + + + ); +}; + +export default RoomMemberStatus; diff --git a/apps/meteor/client/views/room/Header/RoomTopic.spec.tsx b/apps/meteor/client/views/room/Header/RoomTopic.spec.tsx index f360a20f40e3c..021d1565b486f 100644 --- a/apps/meteor/client/views/room/Header/RoomTopic.spec.tsx +++ b/apps/meteor/client/views/room/Header/RoomTopic.spec.tsx @@ -109,7 +109,7 @@ describe('RoomTopic', () => { expect(screen.getByText('Sample Topic')).toBeInTheDocument(); }); - it('should render statusText when statusText is present for direct message user and users length < 3', () => { + it('should not render the DM partner statusText (handled by RoomMemberStatus)', () => { const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user2._id] }); const subscription = createFakeSubscription({ t: 'd', rid: room._id }); @@ -127,28 +127,6 @@ describe('RoomTopic', () => { }, ); - expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); - expect(screen.getByText('Sample Status')).toBeInTheDocument(); - }); - - it('should not render statusText when statusText is present for direct message user and users length >= 3', () => { - const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user2._id, user3._id] }); - const subscription = createFakeSubscription({ t: 'd', rid: room._id }); - - render( - - - , - { - wrapper: mockAppRoot() - .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) - .withPermission('edit-room') - .withUser(user) - .withUsers([user2, user3]) - .build(), - }, - ); - expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); expect(screen.queryByText('Sample Status')).not.toBeInTheDocument(); }); diff --git a/apps/meteor/client/views/room/Header/RoomTopic.tsx b/apps/meteor/client/views/room/Header/RoomTopic.tsx index abe34ab0f3252..da8e54007ebd2 100644 --- a/apps/meteor/client/views/room/Header/RoomTopic.tsx +++ b/apps/meteor/client/views/room/Header/RoomTopic.tsx @@ -1,7 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { isDirectMessageRoom, isPrivateRoom, isPublicRoom, isTeamRoom } from '@rocket.chat/core-typings'; +import { isPrivateRoom, isPublicRoom, isTeamRoom } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; -import { useUserId, useTranslation, useRouter, useUserPresence } from '@rocket.chat/ui-contexts'; +import { useTranslation, useRouter } from '@rocket.chat/ui-contexts'; import MarkdownText from '../../../components/MarkdownText'; import { useCanEditRoom } from '../contextualBar/Info/hooks/useCanEditRoom'; @@ -13,15 +13,12 @@ type RoomTopicProps = { const RoomTopic = ({ room }: RoomTopicProps) => { const t = useTranslation(); const canEdit = useCanEditRoom(room); - const userId = useUserId(); - const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const directUserData = useUserPresence(directUserId); const router = useRouter(); const currentRoute = router.getLocationPathname(); const href = isTeamRoom(room) ? `${currentRoute}/team-info` : `${currentRoute}/channel-settings`; - const topic = isDirectMessageRoom(room) && (room.uids?.length ?? 0) < 3 ? directUserData?.statusText : room.topic; + const { topic } = room; const canEditTopic = canEdit && (isPublicRoom(room) || isPrivateRoom(room)); if (!topic && !canEditTopic) { diff --git a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx index 3b0c80db3351f..9029daaebb402 100644 --- a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx +++ b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'; import LocalTime from '../../../components/LocalTime'; import { UserCard, UserCardAction, UserCardRole, UserCardSkeleton } from '../../../components/UserCard'; import { ReactiveUserStatus } from '../../../components/UserStatus'; +import { ReactiveUserStatusText } from '../../../components/UserStatusText'; import { useUserInfoQuery } from '../../../hooks/useUserInfoQuery'; import { useMemberExists } from '../../hooks/useMemberExists'; import { useUserInfoActions } from '../hooks/useUserInfoActions'; @@ -44,7 +45,6 @@ const UserCardWithData = ({ username, rid, onOpenUserInfo, onClose }: UserCardWi _id, name, roles = defaultValue, - statusText = defaultValue, bio = defaultValue, utcOffset = defaultValue, nickname, @@ -61,7 +61,7 @@ const UserCardWithData = ({ username, rid, onOpenUserInfo, onClose }: UserCardWi etag: avatarETag, localTime: utcOffset && Number.isInteger(utcOffset) && , status: _id && , - customStatus: statusText, + customStatus: _id && , nickname, freeSwitchExtension, }; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx index f1d56343fe191..afd6601d5478c 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx @@ -20,6 +20,7 @@ import { getUserDisplayNames } from '../../../../../lib/getUserDisplayNames'; import InvitationBadge from '../../../../components/InvitationBadge'; import { ReactiveUserStatus } from '../../../../components/UserStatus'; import { usePreventPropagation } from '../../../../hooks/usePreventPropagation'; +import { useUserStatusTooltip } from '../../../../hooks/useUserStatusTooltip'; import type { RoomMember } from '../../../hooks/useMembersList'; type RoomMembersItemProps = Pick & { @@ -41,18 +42,28 @@ const RoomMembersItem = ({ reload, useRealName, }: RoomMembersItemProps) => { - const [showButton, setShowButton] = useState(); + const [showButton, setShowButton] = useState(false); const isReduceMotionEnabled = usePrefersReducedMotion(); const isInvited = subscription?.status === 'INVITED'; const invitationDate = isInvited ? subscription?.ts : undefined; - const handleMenuEvent = { - [isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setShowButton, - }; - const preventPropagation = usePreventPropagation(); - const [nameOrUsername, displayUsername] = getUserDisplayNames(name, username, useRealName); + const statusTooltipHandlers = useUserStatusTooltip(_id); + + const handleMenuEvent = isReduceMotionEnabled + ? { + ...statusTooltipHandlers, + onMouseEnter: (e: MouseEvent) => { + setShowButton(true); + statusTooltipHandlers.onMouseEnter(e); + }, + } + : { + ...statusTooltipHandlers, + onTransitionEnd: () => setShowButton(true), + }; + return (