Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
190 lines
6.9 KiB
TypeScript
190 lines
6.9 KiB
TypeScript
import type { ICustomUserStatus, IUser, UserStatus as UserStatusEnum } from '@rocket.chat/core-typings';
|
|
import { Box, Icon, RadioButton } from '@rocket.chat/fuselage';
|
|
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
|
|
import { clientCallbacks } from '@rocket.chat/ui-client';
|
|
import { useEndpoint, useSetting, useStream } from '@rocket.chat/ui-contexts';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useCallback, useEffect, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { useCustomStatusModalHandler } from './useCustomStatusModalHandler';
|
|
import { useStatusVisibilityModalHandler } from './useStatusVisibilityModalHandler';
|
|
import MarkdownText from '../../../../components/MarkdownText';
|
|
import { UserStatus } from '../../../../components/UserStatus';
|
|
import { useExpirationText } from '../../../../hooks/useExpirationText';
|
|
import { useFireGlobalEvent } from '../../../../hooks/useFireGlobalEvent';
|
|
import { userStatuses } from '../../../../lib/userStatuses';
|
|
import type { UserStatusDescriptor } from '../../../../lib/userStatuses';
|
|
import { mapCustomUserStatusFromApi } from '../../../../lib/utils/mapCustomUserStatusFromApi';
|
|
import { useStatusDisabledModal } from '../../../../views/admin/customUserStatus/hooks/useStatusDisabledModal';
|
|
|
|
export const useStatusItems = (user?: IUser): GenericMenuItemProps[] => {
|
|
// We should lift this up to somewhere else if we want to use it in other places
|
|
|
|
userStatuses.invisibleAllowed = useSetting('Accounts_AllowInvisibleStatusOption', true);
|
|
|
|
const queryClient = useQueryClient();
|
|
const stream = useStream('notify-logged');
|
|
const listCustomUserStatusEndpoint = useEndpoint('GET', '/v1/custom-user-status.list');
|
|
const listCustomUserStatus = useCallback(async (): Promise<ICustomUserStatus[]> => {
|
|
const all: ICustomUserStatus[] = [];
|
|
const count = 100;
|
|
let offset = 0;
|
|
// REST endpoint is paginated; loop until total reached.
|
|
while (true) {
|
|
const { statuses, total } = await listCustomUserStatusEndpoint({ count, offset });
|
|
all.push(...statuses.map(mapCustomUserStatusFromApi));
|
|
if (all.length >= total || statuses.length === 0) break;
|
|
offset += statuses.length;
|
|
}
|
|
return all;
|
|
}, [listCustomUserStatusEndpoint]);
|
|
|
|
useEffect(
|
|
() =>
|
|
userStatuses.watch(stream, () => {
|
|
queryClient.setQueryData(['user-statuses'], Array.from(userStatuses));
|
|
}),
|
|
[queryClient, stream],
|
|
);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const presenceDisabled = useSetting('Presence_broadcast_disabled', false);
|
|
const allowUserStatusMessageChange = useSetting('Accounts_AllowUserStatusMessageChange', true);
|
|
|
|
const fireGlobalStatusEvent = useFireGlobalEvent('user-status-manually-set');
|
|
const setStatus = useEndpoint('POST', '/v1/users.setStatus');
|
|
const setStatusMutation = useMutation({
|
|
mutationFn: async (status: UserStatusDescriptor) => {
|
|
void setStatus({
|
|
status: status.statusType,
|
|
...(allowUserStatusMessageChange && { message: userStatuses.isValidType(status.id) ? '' : status.name }),
|
|
});
|
|
void clientCallbacks.run('userStatusManuallySet', status);
|
|
await fireGlobalStatusEvent.mutateAsync(status);
|
|
},
|
|
});
|
|
|
|
const { data: statuses } = useQuery({
|
|
queryKey: ['user-statuses'],
|
|
queryFn: async () => {
|
|
await userStatuses.sync(listCustomUserStatus);
|
|
return Array.from(userStatuses);
|
|
},
|
|
staleTime: Infinity,
|
|
});
|
|
|
|
const handleStatusDisabledModal = useStatusDisabledModal();
|
|
const handleCustomStatus = useCustomStatusModalHandler();
|
|
const handleStatusVisibility = useStatusVisibilityModalHandler();
|
|
const statusVisibilityEnabled = useSetting('Accounts_StatusVisibility_Enabled', false);
|
|
const customStatusExpiration = useExpirationText(user?.statusExpiresAt);
|
|
|
|
return useMemo<GenericMenuItemProps[]>(() => {
|
|
if (presenceDisabled) {
|
|
return [
|
|
{
|
|
id: 'presence-disabled',
|
|
content: (
|
|
<Box fontScale='p2'>
|
|
<Box marginBlockEnd={4} wordBreak='break-word' style={{ whiteSpace: 'normal' }}>
|
|
{t('User_status_disabled')}
|
|
</Box>
|
|
<Box is='a' color='info' onClick={handleStatusDisabledModal}>
|
|
{t('Learn_more')}
|
|
</Box>
|
|
</Box>
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
const items: GenericMenuItemProps[] = [];
|
|
|
|
// Top: user's currently-active custom status (display only — clicking does nothing, already selected).
|
|
if (user?.statusText || customStatusExpiration) {
|
|
const contentValue = user?.statusText || (user?.status ? t(user.status) : undefined);
|
|
items.push({
|
|
id: 'current-custom-status',
|
|
status: <UserStatus status={user?.status} />,
|
|
content: (
|
|
<>
|
|
{contentValue && <MarkdownText withTruncatedText content={contentValue} parseEmoji variant='inline' />}
|
|
{customStatusExpiration && (
|
|
<Box color='secondary-info' display='flex' alignItems='center'>
|
|
<Icon name='clock' size='x16' marginInlineEnd={4} />
|
|
{customStatusExpiration}
|
|
</Box>
|
|
)}
|
|
</>
|
|
),
|
|
addon: <RadioButton checked readOnly />,
|
|
});
|
|
}
|
|
|
|
// "Custom..." action opens the edit modal
|
|
if (allowUserStatusMessageChange) {
|
|
items.push({
|
|
id: 'custom-status-edit',
|
|
icon: 'edit',
|
|
content: t('Custom_Status'),
|
|
onClick: handleCustomStatus,
|
|
});
|
|
}
|
|
|
|
const isPresetSelected = (statusType: UserStatusEnum): boolean =>
|
|
!user?.statusText && !customStatusExpiration && user?.status === statusType;
|
|
const presetItems = (statuses ?? [])
|
|
.filter((s) => userStatuses.isValidType(s.id))
|
|
.map((status): GenericMenuItemProps => ({
|
|
id: status.id,
|
|
status: <UserStatus status={status.statusType} />,
|
|
content: <MarkdownText content={status.localizeName ? t(status.name) : status.name} parseEmoji variant='inline' />,
|
|
addon: <RadioButton checked={isPresetSelected(status.statusType)} readOnly />,
|
|
onClick: () => setStatusMutation.mutate(status),
|
|
}));
|
|
|
|
// Admin-defined custom statuses
|
|
const customItems = allowUserStatusMessageChange
|
|
? (statuses ?? [])
|
|
.filter((s) => !userStatuses.isValidType(s.id))
|
|
.map((status): GenericMenuItemProps => ({
|
|
id: status.id,
|
|
status: <UserStatus status={status.statusType} />,
|
|
content: (
|
|
<MarkdownText withTruncatedText content={status.localizeName ? t(status.name) : status.name} parseEmoji variant='inline' />
|
|
),
|
|
addon: <RadioButton checked={user?.statusText === status.name} readOnly />,
|
|
onClick: () => setStatusMutation.mutate(status),
|
|
}))
|
|
: [];
|
|
|
|
const actionItems: GenericMenuItemProps[] = [];
|
|
|
|
if (statusVisibilityEnabled) {
|
|
actionItems.push({
|
|
id: 'status-visibility-edit',
|
|
icon: 'eye-off',
|
|
content: t('Accounts_StatusVisibility_HideFrom'),
|
|
onClick: handleStatusVisibility,
|
|
});
|
|
}
|
|
|
|
return [...items, ...presetItems, ...customItems, ...actionItems];
|
|
}, [
|
|
presenceDisabled,
|
|
allowUserStatusMessageChange,
|
|
t,
|
|
handleStatusDisabledModal,
|
|
user?.status,
|
|
user?.statusText,
|
|
customStatusExpiration,
|
|
statuses,
|
|
handleCustomStatus,
|
|
handleStatusVisibility,
|
|
statusVisibilityEnabled,
|
|
setStatusMutation,
|
|
]);
|
|
};
|