Skip to content

Commit d29edd2

Browse files
committed
feat: redesign user menu and edit status modal
1 parent 724a27f commit d29edd2

6 files changed

Lines changed: 78 additions & 149 deletions

File tree

apps/meteor/client/components/UserCard/UserCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const UserCard = ({
6666
)}
6767
</Box>
6868
{customStatus && (
69-
<UserCardInfo mbe={16}>
69+
<UserCardInfo mbe={4}>
7070
{typeof customStatus === 'string' ? (
7171
<MarkdownText withTruncatedText variant='inlineWithoutBreaks' content={customStatus} parseEmoji={true} />
7272
) : (

apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx

Lines changed: 46 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { IUser } from '@rocket.chat/core-typings';
21
import { UserStatus as UserStatusType } from '@rocket.chat/core-typings';
2+
import { css } from '@rocket.chat/css-in-js';
33
import type { SelectOption } from '@rocket.chat/fuselage';
44
import {
55
Field,
@@ -10,8 +10,7 @@ import {
1010
TextInput,
1111
InputBox,
1212
Select,
13-
SelectLegacy,
14-
Option,
13+
Callout,
1514
Margins,
1615
Modal,
1716
Button,
@@ -25,20 +24,21 @@ import {
2524
ModalFooterControllers,
2625
} from '@rocket.chat/fuselage';
2726
import { useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks';
28-
import { useToastMessageDispatch, useSetting, useEndpoint } from '@rocket.chat/ui-contexts';
27+
import { useToastMessageDispatch, useSetting, useEndpoint, useUser } from '@rocket.chat/ui-contexts';
2928
import type { TFunction } from 'i18next';
3029
import type { ReactElement, ChangeEvent, ComponentProps, FormEvent } from 'react';
3130
import { useState, useCallback, useId, useMemo } from 'react';
3231
import { useTranslation } from 'react-i18next';
3332

33+
import MarkdownText from '../../../components/MarkdownText';
3434
import { UserStatus } from '../../../components/UserStatus';
35+
import UserStatusMenu from '../../../components/UserStatusMenu';
36+
import { useExpirationText } from '../../../components/UserStatusText';
3537
import { useFormatTime } from '../../../hooks/useFormatTime';
3638
import { USER_STATUS_TEXT_MAX_LENGTH } from '../../../lib/constants';
3739

3840
type EditStatusModalProps = {
3941
onClose: () => void;
40-
userStatus: IUser['status'];
41-
userStatusText: IUser['statusText'];
4242
};
4343

4444
type DurationOption = {
@@ -73,27 +73,17 @@ const DURATION_OPTIONS: DurationOption[] = [
7373
{ value: 'custom', getLabel: (t) => t('Status_choose_date_and_time') },
7474
];
7575

76-
const StatusOption = ({ status, label }: { status: UserStatusType; label: string }) => (
77-
<Box display='flex' alignItems='center'>
78-
<Box marginInlineEnd={8}>
79-
<UserStatus status={status} />
80-
</Box>
81-
{label}
82-
</Box>
83-
);
84-
85-
// eslint-disable-next-line react/no-multi-comp
86-
const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModalProps): ReactElement => {
76+
const EditStatusModal = ({ onClose }: EditStatusModalProps): ReactElement => {
77+
const user = useUser();
8778
const allowUserStatusMessageChange = useSetting('Accounts_AllowUserStatusMessageChange');
88-
const allowInvisibleStatus = useSetting('Accounts_AllowInvisibleStatusOption', true);
8979
const dispatchToastMessage = useToastMessageDispatch();
9080
const [customStatus, setCustomStatus] = useLocalStorage<string>('Local_Custom_Status', '');
91-
const initialStatusText = customStatus || userStatusText || '';
81+
const initialStatusText = customStatus || user?.statusText || '';
9282

9383
const { t } = useTranslation();
9484
const modalId = useId();
9585
const [statusText, setStatusText] = useState(initialStatusText);
96-
const [statusType, setStatusType] = useState(userStatus);
86+
const [statusType, setStatusType] = useState(user?.status ?? UserStatusType.ONLINE);
9787
const [statusTextError, setStatusTextError] = useState<string | undefined>();
9888
const [duration, setDuration] = useState('');
9989
const [customDate, setCustomDate] = useState(() => new Date().toLocaleDateString('en-CA'));
@@ -103,19 +93,9 @@ const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModa
10393
const setUserStatus = useEndpoint('POST', '/v1/users.setStatus');
10494
const formatTime = useFormatTime();
10595

106-
const statusOptions: SelectOption[] = useMemo(() => {
107-
const options: SelectOption[] = [
108-
[UserStatusType.ONLINE, t('Online')],
109-
[UserStatusType.AWAY, t('Away')],
110-
[UserStatusType.BUSY, t('Busy')],
111-
];
112-
113-
if (allowInvisibleStatus) {
114-
options.push([UserStatusType.OFFLINE, t('Offline')]);
115-
}
116-
117-
return options;
118-
}, [t, allowInvisibleStatus]);
96+
const currentStatusText = user?.statusText || t(user?.status ?? 'offline');
97+
const expirationText = useExpirationText(user?.statusExpiresAt);
98+
const defaultStatusLabel = `${t(statusType)} (${t('Default')})`;
11999

120100
const durationOptions: SelectOption[] = useMemo(
121101
() => DURATION_OPTIONS.map(({ value, getLabel }) => [value, getLabel(t, formatTime, new Date())]),
@@ -154,7 +134,7 @@ const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModa
154134
}
155135
await setUserStatus({
156136
message: statusText,
157-
status: statusType as UserStatusType,
137+
status: statusType,
158138
...(expiresAt && { expiresAt: expiresAt.toISOString() }),
159139
});
160140
setCustomStatus(statusText);
@@ -188,40 +168,44 @@ const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModa
188168
<ModalContent fontScale='p2'>
189169
<Box display='flex' flexDirection='column' rowGap={12}>
190170
<Field>
191-
<FieldLabel htmlFor={`${modalId}-status-type`}>{t('Status')}</FieldLabel>
192-
<FieldRow>
193-
<SelectLegacy
194-
id={`${modalId}-status-type`}
195-
aria-label={t('Status')}
196-
value={statusType}
197-
options={statusOptions}
198-
onChange={(value: string) => setStatusType(value as UserStatusType)}
199-
renderSelected={({ value, label }) => (
200-
<Box flexGrow='1'>
201-
<StatusOption status={value as UserStatusType} label={label} />
171+
<FieldLabel>{t('Status_current')}</FieldLabel>
172+
<Box display='flex' alignItems='center' mbs={8}>
173+
<UserStatus status={user?.status} />
174+
<Box mis={8}>
175+
<MarkdownText content={currentStatusText} parseEmoji variant='inlineWithoutBreaks' />
176+
{expirationText && (
177+
<Box color='hint' fontScale='c1'>
178+
{expirationText}
202179
</Box>
203180
)}
204-
renderItem={({ value, label, ...props }) => (
205-
<Option {...props} label={<StatusOption status={value as UserStatusType} label={label} />} />
206-
)}
207-
/>
208-
</FieldRow>
181+
</Box>
182+
</Box>
209183
</Field>
210184
<Field>
211-
<FieldLabel htmlFor={`${modalId}-status-message`}>{t('StatusMessage')}</FieldLabel>
185+
<FieldLabel htmlFor={`${modalId}-status-message`}>{t('Status')}</FieldLabel>
212186
<FieldRow>
213187
<TextInput
214188
id={`${modalId}-status-message`}
215-
aria-label={t('StatusMessage')}
189+
aria-label={t('Status')}
216190
error={statusTextError}
217191
disabled={!allowUserStatusMessageChange}
218192
flexGrow={1}
219193
value={statusText}
220194
onChange={handleStatusText}
221-
placeholder={t('StatusMessage_Placeholder')}
195+
placeholder={defaultStatusLabel}
196+
className={css`
197+
align-items: center;
198+
199+
& > .rcx-input-box__addon {
200+
order: -1;
201+
margin-inline-end: 0.5rem;
202+
}
203+
`}
204+
addon={<UserStatusMenu margin='none' initialStatus={statusType} onChange={setStatusType} placement='bottom-start' />}
222205
/>
223206
</FieldRow>
224207
{!allowUserStatusMessageChange && <FieldHint>{t('StatusMessage_Change_Disabled')}</FieldHint>}
208+
{allowUserStatusMessageChange && <FieldHint>{t('Status_you_can_use_emoji')}</FieldHint>}
225209
<FieldError>{statusTextError}</FieldError>
226210
</Field>
227211
<Field>
@@ -256,22 +240,18 @@ const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModa
256240
</Box>
257241
)}
258242
</Field>
243+
<Callout type='info'>{t('Status_new_status_warning')}</Callout>
259244
</Box>
260245
</ModalContent>
261246
<ModalFooter>
262-
<Box display='flex' justifyContent='space-between' alignItems='center' width='100%'>
263-
<Box fontScale='c1' color='hint'>
264-
{t('Status_calendar_events_wont_override')}
265-
</Box>
266-
<ModalFooterControllers>
267-
<Button secondary onClick={onClose}>
268-
{t('Cancel')}
269-
</Button>
270-
<Button primary type='submit' disabled={!!statusTextError}>
271-
{t('Save')}
272-
</Button>
273-
</ModalFooterControllers>
274-
</Box>
247+
<ModalFooterControllers>
248+
<Button secondary onClick={onClose}>
249+
{t('Cancel')}
250+
</Button>
251+
<Button primary type='submit' disabled={!!statusTextError}>
252+
{t('Save')}
253+
</Button>
254+
</ModalFooterControllers>
275255
</ModalFooter>
276256
</Modal>
277257
);

apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/UserMenuHeader.tsx

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,20 @@
11
import type { IUser } from '@rocket.chat/core-typings';
2-
import { Box, Margins } from '@rocket.chat/fuselage';
2+
import { Box } from '@rocket.chat/fuselage';
33
import { UserAvatar } from '@rocket.chat/ui-avatar';
44
import { useUserDisplayName } from '@rocket.chat/ui-client';
5-
import { useSetting } from '@rocket.chat/ui-contexts';
6-
import { useTranslation } from 'react-i18next';
7-
8-
import { UserStatus } from '../../../components/UserStatus';
9-
import { UserStatusText } from '../../../components/UserStatusText';
105

116
type UserMenuHeaderProps = { user: IUser };
127

138
const UserMenuHeader = ({ user }: UserMenuHeaderProps) => {
14-
const { t } = useTranslation();
15-
const presenceDisabled = useSetting('Presence_broadcast_disabled', false);
169
const displayName = useUserDisplayName(user);
1710

1811
return (
19-
<Box display='flex' flexDirection='row' alignItems='center' minWidth='x208' mbe='neg-x4' mbs='neg-x8'>
12+
<Box display='flex' flexDirection='row' alignItems='center' minWidth='x208'>
2013
<Box mie={4}>
2114
<UserAvatar size='x36' username={user?.username || ''} etag={user?.avatarETag} />
2215
</Box>
23-
<Box mis={4} display='flex' overflow='hidden' flexDirection='column' fontScale='p2' mb='neg-x4' flexGrow={1} flexShrink={1}>
24-
<Box withTruncatedText w='full' display='flex' alignItems='center' flexDirection='row'>
25-
<Margins inline={4}>
26-
<UserStatus status={presenceDisabled ? 'disabled' : user.status} />
27-
<Box is='span' withTruncatedText display='inline-block' fontWeight='700'>
28-
{displayName}
29-
</Box>
30-
</Margins>
31-
</Box>
32-
<Box color='hint'>
33-
<UserStatusText
34-
statusText={user.statusText || t(user.status ?? 'offline')}
35-
statusExpiresAt={user.statusExpiresAt}
36-
showExpiration
37-
/>
38-
</Box>
16+
<Box mis={4} fontScale='p2' fontWeight='700' withTruncatedText flexGrow={1} flexShrink={1}>
17+
{displayName}
3918
</Box>
4019
</Box>
4120
);
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { useSetModal, useUser } from '@rocket.chat/ui-contexts';
1+
import { useSetModal } from '@rocket.chat/ui-contexts';
22

33
import EditStatusModal from '../EditStatusModal';
44

55
export const useCustomStatusModalHandler = () => {
6-
const user = useUser();
76
const setModal = useSetModal();
87

98
return () => {
109
const handleModalClose = () => setModal(null);
11-
setModal(<EditStatusModal userStatus={user?.status} userStatusText={user?.statusText} onClose={handleModalClose} />);
10+
setModal(<EditStatusModal onClose={handleModalClose} />);
1211
};
1312
};
Lines changed: 22 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,25 @@
11
import { Box } from '@rocket.chat/fuselage';
22
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
3-
import { clientCallbacks } from '@rocket.chat/ui-client';
4-
import { useEndpoint, useSetting } from '@rocket.chat/ui-contexts';
5-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6-
import { useEffect } from 'react';
3+
import { useSetting, useUser } from '@rocket.chat/ui-contexts';
74
import { useTranslation } from 'react-i18next';
85

96
import { useCustomStatusModalHandler } from './useCustomStatusModalHandler';
107
import MarkdownText from '../../../../components/MarkdownText';
118
import { UserStatus } from '../../../../components/UserStatus';
12-
import { useFireGlobalEvent } from '../../../../hooks/useFireGlobalEvent';
13-
import { userStatuses } from '../../../../lib/userStatuses';
14-
import type { UserStatusDescriptor } from '../../../../lib/userStatuses';
9+
import { useExpirationText } from '../../../../components/UserStatusText';
1510
import { useStatusDisabledModal } from '../../../../views/admin/customUserStatus/hooks/useStatusDisabledModal';
1611

1712
export const useStatusItems = (): GenericMenuItemProps[] => {
18-
// We should lift this up to somewhere else if we want to use it in other places
19-
20-
userStatuses.invisibleAllowed = useSetting('Accounts_AllowInvisibleStatusOption', true);
21-
22-
const queryClient = useQueryClient();
23-
24-
useEffect(
25-
() =>
26-
userStatuses.watch(() => {
27-
queryClient.setQueryData(['user-statuses'], Array.from(userStatuses));
28-
}),
29-
[queryClient],
30-
);
31-
3213
const { t } = useTranslation();
33-
34-
const fireGlobalStatusEvent = useFireGlobalEvent('user-status-manually-set');
35-
const setStatus = useEndpoint('POST', '/v1/users.setStatus');
36-
const setStatusMutation = useMutation({
37-
mutationFn: async (status: UserStatusDescriptor) => {
38-
void setStatus({ status: status.statusType, message: userStatuses.isValidType(status.id) ? '' : status.name });
39-
void clientCallbacks.run('userStatusManuallySet', status);
40-
await fireGlobalStatusEvent.mutateAsync(status);
41-
},
42-
});
14+
const user = useUser();
4315

4416
const presenceDisabled = useSetting('Presence_broadcast_disabled', false);
45-
46-
const { data: statuses } = useQuery({
47-
queryKey: ['user-statuses'],
48-
queryFn: async () => {
49-
await userStatuses.sync();
50-
return Array.from(userStatuses);
51-
},
52-
staleTime: Infinity,
53-
select: (statuses) =>
54-
statuses.map((status): GenericMenuItemProps => {
55-
const content = status.localizeName ? t(status.name) : status.name;
56-
return {
57-
id: status.id,
58-
status: <UserStatus status={status.statusType} />,
59-
content: <MarkdownText content={content} parseEmoji={true} variant='inline' />,
60-
disabled: presenceDisabled,
61-
onClick: () => setStatusMutation.mutate(status),
62-
};
63-
}),
64-
});
65-
6617
const handleStatusDisabledModal = useStatusDisabledModal();
6718
const handleCustomStatus = useCustomStatusModalHandler();
6819

20+
const statusText = user?.statusText || t(user?.status ?? 'offline');
21+
const expirationText = useExpirationText(user?.statusExpiresAt);
22+
6923
return [
7024
...(presenceDisabled
7125
? [
@@ -84,7 +38,21 @@ export const useStatusItems = (): GenericMenuItemProps[] => {
8438
},
8539
]
8640
: []),
87-
...(statuses ?? []),
88-
{ id: 'custom-status', icon: 'emoji', content: t('Custom_Status'), onClick: handleCustomStatus, disabled: presenceDisabled },
41+
{
42+
id: 'current-status',
43+
status: <UserStatus status={presenceDisabled ? 'disabled' : user?.status} />,
44+
content: (
45+
<>
46+
<MarkdownText content={statusText} parseEmoji variant='inlineWithoutBreaks' withTruncatedText />
47+
{expirationText && (
48+
<Box color='hint' fontScale='c1'>
49+
{expirationText}
50+
</Box>
51+
)}
52+
</>
53+
),
54+
onClick: handleCustomStatus,
55+
disabled: presenceDisabled,
56+
},
8957
];
9058
};

packages/i18n/src/locales/en.i18n.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5155,6 +5155,9 @@
51555155
"Status_4_hours": "4 hours",
51565156
"Status_choose_date_and_time": "Choose date and time",
51575157
"Status_calendar_events_wont_override": "Calendar events won't override this",
5158+
"Status_current": "Current status",
5159+
"Status_you_can_use_emoji": "You can use emoji",
5160+
"Status_new_status_warning": "Setting a manual status will override any current external status. Automated integrations may temporarily change your status and will restore it once they end.",
51585161
"Step": "Step",
51595162
"Stop_Recording": "Stop Recording",
51605163
"Stop_call": "Stop call",

0 commit comments

Comments
 (0)