snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { Contextualbar } from '@rocket.chat/ui-client';
import type { Meta } from '@storybook/react';
import { FormProvider, useForm } from 'react-hook-form';
import PruneMessages from './PruneMessages';
export default {
component: PruneMessages,
parameters: {
layout: 'fullscreen',
actions: { argTypesRegex: '^on.*' },
},
decorators: [
(fn) => {
const methods = useForm({
defaultValues: {
pinned: true,
},
});
return (
<FormProvider {...methods}>
<Contextualbar height='100vh'>{fn()}</Contextualbar>
</FormProvider>
);
},
],
} satisfies Meta<typeof PruneMessages>;
export const Default = {};
export const WithCallout = {
args: {
callOutText: 'This is a callout',
},
};

View File

@@ -0,0 +1,120 @@
import { Field, FieldLabel, FieldRow, ButtonGroup, Button, CheckBox, Callout } from '@rocket.chat/fuselage';
import {
ContextualbarHeader,
ContextualbarIcon,
ContextualbarTitle,
ContextualbarScrollableContent,
ContextualbarFooter,
ContextualbarClose,
ContextualbarDialog,
} from '@rocket.chat/ui-client';
import { useId } from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import PruneMessagesDateTimeRow from './PruneMessagesDateTimeRow';
import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple';
export type PruneMessagesProps = {
callOutText?: string;
validateText?: string;
users: string[];
onClickClose: () => void;
onClickPrune: () => void;
};
const PruneMessages = ({ callOutText, validateText, onClickClose, onClickPrune }: PruneMessagesProps) => {
const { t } = useTranslation();
const { control } = useFormContext();
const inclusiveCheckboxId = useId();
const pinnedCheckboxId = useId();
const discussionCheckboxId = useId();
const threadsCheckboxId = useId();
const attachedCheckboxId = useId();
return (
<ContextualbarDialog>
<ContextualbarHeader>
<ContextualbarIcon name='eraser' />
<ContextualbarTitle>{t('Prune_Messages')}</ContextualbarTitle>
{onClickClose && <ContextualbarClose onClick={onClickClose} />}
</ContextualbarHeader>
<ContextualbarScrollableContent>
<PruneMessagesDateTimeRow label={t('Newer_than')} field='newer' />
<PruneMessagesDateTimeRow label={t('Older_than')} field='older' />
<Field>
<FieldLabel flexGrow={0}>{t('Only_from_users')}</FieldLabel>
<Controller
control={control}
name='users'
render={({ field: { onChange, value } }) => (
<UserAutoCompleteMultiple value={value} onChange={onChange} placeholder={t('Please_enter_usernames')} />
)}
/>
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={inclusiveCheckboxId}>{t('Inclusive')}</FieldLabel>
<Controller
control={control}
name='inclusive'
render={({ field: { value, ...field } }) => <CheckBox id={inclusiveCheckboxId} {...field} checked={value} />}
/>
</FieldRow>
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={pinnedCheckboxId}>{t('RetentionPolicy_DoNotPrunePinned')}</FieldLabel>
<Controller
control={control}
name='pinned'
render={({ field: { value, ...field } }) => <CheckBox id={pinnedCheckboxId} {...field} checked={value} />}
/>
</FieldRow>
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={discussionCheckboxId}>{t('RetentionPolicy_DoNotPruneDiscussion')}</FieldLabel>
<Controller
control={control}
name='discussion'
render={({ field: { value, ...field } }) => <CheckBox id={discussionCheckboxId} {...field} checked={value} />}
/>
</FieldRow>
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={threadsCheckboxId}>{t('RetentionPolicy_DoNotPruneThreads')}</FieldLabel>
<Controller
control={control}
name='threads'
render={({ field: { value, ...field } }) => <CheckBox id={threadsCheckboxId} {...field} checked={value} />}
/>
</FieldRow>
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={attachedCheckboxId}>{t('Files_only')}</FieldLabel>
<Controller
control={control}
name='attached'
render={({ field: { value, ...field } }) => <CheckBox id={attachedCheckboxId} {...field} checked={value} />}
/>
</FieldRow>
</Field>
{callOutText && !validateText && <Callout type='warning'>{callOutText}</Callout>}
{validateText && <Callout type='warning'>{validateText}</Callout>}
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button danger disabled={Boolean(validateText)} onClick={onClickPrune}>
{t('Prune')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</ContextualbarDialog>
);
};
export default PruneMessages;

View File

@@ -0,0 +1,26 @@
import { Box, Margins, Field, FieldLabel, InputBox } from '@rocket.chat/fuselage';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
export type PruneMessagesDateTimeRowProps = {
label: string;
field: 'newer' | 'older';
};
const PruneMessagesDateTimeRow = ({ label, field }: PruneMessagesDateTimeRowProps) => {
const { register } = useFormContext();
const { t } = useTranslation();
return (
<Field>
<FieldLabel flexGrow={0}>{label}</FieldLabel>
<Box display='flex' marginInline='neg-x4'>
<Margins inline={4}>
<InputBox aria-label={`${label} ${t('Date')}`} type='date' flexGrow={1} height='x20' {...register(`${field}.date`)} />
<InputBox aria-label={`${label} ${t('Time')}`} type='time' flexGrow={1} height='x20' {...register(`${field}.time`)} />
</Margins>
</Box>
</Field>
);
};
export default PruneMessagesDateTimeRow;

View File

@@ -0,0 +1,176 @@
import { isDirectMessageRoom } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { GenericModal } from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useEndpoint, useRoomToolbox } from '@rocket.chat/ui-contexts';
import { useCallback, useMemo, useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import PruneMessages from './PruneMessages';
import { formatDate } from '../../../../lib/utils/dateFormat';
import { useRoom } from '../../contexts/RoomContext';
const getTimeZoneOffset = (): string => {
const offset = new Date().getTimezoneOffset();
const absOffset = Math.abs(offset);
return `${offset < 0 ? '+' : '-'}${`00${Math.floor(absOffset / 60)}`.slice(-2)}:${`00${absOffset % 60}`.slice(-2)}`;
};
export const initialValues = {
newer: {
date: '',
time: '',
},
older: {
date: '',
time: '',
},
users: [],
inclusive: false,
pinned: false,
discussion: false,
threads: false,
attached: false,
};
const DEFAULT_PRUNE_LIMIT = 2000;
const PruneMessagesWithData = () => {
const { t } = useTranslation();
const room = useRoom();
const setModal = useSetModal();
const { closeTab: close } = useRoomToolbox();
const closeModal = useCallback(() => setModal(null), [setModal]);
const dispatchToastMessage = useToastMessageDispatch();
const pruneMessagesAction = useEndpoint('POST', '/v1/rooms.cleanHistory');
const [counter, setCounter] = useState(0);
const methods = useForm({ defaultValues: initialValues });
const {
newer: { date: newerDate, time: newerTime },
older: { date: olderDate, time: olderTime },
users,
inclusive,
pinned,
discussion,
threads,
attached,
} = methods.watch();
const fromDate = useMemo(() => {
return new Date(`${newerDate || '0001-01-01'}T${newerTime || '00:00'}:00${getTimeZoneOffset()}`);
}, [newerDate, newerTime]);
const toDate = useMemo(() => {
return new Date(`${olderDate || '9999-12-31'}T${olderTime || '23:59'}:59${getTimeZoneOffset()}`);
}, [olderDate, olderTime]);
const handlePrune = useStableCallback((): void => {
const handlePruneAction = async () => {
const limit = DEFAULT_PRUNE_LIMIT;
try {
if (counter === limit) {
return;
}
const { count } = await pruneMessagesAction({
roomId: room._id,
latest: toDate.toISOString(),
oldest: fromDate.toISOString(),
inclusive,
limit,
excludePinned: pinned,
filesOnly: attached,
ignoreDiscussion: discussion,
ignoreThreads: threads,
users,
});
setCounter(count);
if (count < 1) {
throw new Error(attached ? t('No_files_found_to_prune') : t('No_messages_found_to_prune'));
}
dispatchToastMessage({
type: 'success',
message: attached ? t('__count__file_pruned', { count }) : t('__count__message_pruned', { count }),
});
methods.reset();
} catch (error: unknown) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
closeModal();
}
};
return setModal(
<GenericModal
variant='danger'
onClose={closeModal}
onCancel={closeModal}
onConfirm={handlePruneAction}
confirmText={t('Yes_prune_them')}
>
{t('Prune_Modal')}
</GenericModal>,
);
});
const callOutText = useMemo(() => {
const name = room && ((isDirectMessageRoom(room) && room.usernames?.join(' x ')) || room.fname || room.name);
const exceptPinned = pinned ? ` ${t('except_pinned')}` : '';
const ifFrom = users.length ? ` ${t('if_they_are_from', { users: users.map((element) => element).join(', ') })}` : '';
const filesOrMessages = attached ? t('files') : t('messages');
if (newerDate && olderDate) {
return (
t('Prune_Warning_between', {
items: filesOrMessages,
roomName: name,
fromDate: formatDate(fromDate, 'L LT'),
toDate: formatDate(toDate, 'L LT'),
}) +
exceptPinned +
ifFrom
);
}
if (newerDate) {
return (
t('Prune_Warning_after', { items: filesOrMessages, roomName: name, fromDate: formatDate(fromDate, 'L LT') }) + exceptPinned + ifFrom
);
}
if (olderDate) {
return (
t('Prune_Warning_before', { items: filesOrMessages, roomName: name, toDate: formatDate(toDate, 'L LT') }) + exceptPinned + ifFrom
);
}
return t('Prune_Warning_all', { items: filesOrMessages, roomName: name }) + exceptPinned + ifFrom;
}, [attached, fromDate, newerDate, olderDate, pinned, room, t, toDate, users]);
const validateText = useMemo(() => {
if (fromDate > toDate) {
return t('Newer_than_may_not_exceed_Older_than');
}
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
return t('error-invalid-date');
}
return undefined;
}, [fromDate, t, toDate]);
return (
<FormProvider {...methods}>
<PruneMessages callOutText={callOutText} validateText={validateText} users={users} onClickClose={close} onClickPrune={handlePrune} />
</FormProvider>
);
};
export default PruneMessagesWithData;

View File

@@ -0,0 +1 @@
export { default } from './PruneMessagesWithData';