[IMPROVE] Rewrite Prune Messages as React component (#19900)

pull/20124/head^2
Douglas Fabris 5 years ago committed by GitHub
parent 9e18df6baa
commit 1ae396fd04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      app/discussion/client/tabBar.ts
  2. 6
      app/otr/client/tabBar.ts
  3. 3
      app/ui-clean-history/client/index.js
  4. 8
      app/ui-clean-history/client/lib/startup.ts
  5. 158
      app/ui-clean-history/client/views/cleanHistory.html
  6. 351
      app/ui-clean-history/client/views/cleanHistory.js
  7. 59
      app/ui-clean-history/client/views/stylesheets/cleanHistory.css
  8. 4
      client/views/room/adapters.js
  9. 319
      client/views/room/contextualBar/PruneMessages/PruneMessages.js
  10. 23
      client/views/room/contextualBar/PruneMessages/PruneMessages.stories.js
  11. 3
      client/views/room/contextualBar/PruneMessages/index.js
  12. 10
      client/views/room/lib/Toolbox/defaultActions.ts
  13. 2
      client/views/room/lib/Toolbox/index.tsx
  14. 2
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -1,8 +1,10 @@
import { useMemo, lazy, LazyExoticComponent, FC } from 'react';
import { useMemo, lazy } from 'react';
import { addAction } from '../../../client/views/room/lib/Toolbox';
import { useSetting } from '../../../client/contexts/SettingsContext';
const template = lazy(() => import('../../../client/views/room/contextualBar/Discussions'));
addAction('discussions', () => {
const discussionEnabled = useSetting('Discussion_enabled');
@ -11,7 +13,7 @@ addAction('discussions', () => {
id: 'discussions',
title: 'Discussions',
icon: 'discussion',
template: lazy(() => import('../../../client/views/room/contextualBar/Discussions')) as LazyExoticComponent<FC>,
template,
full: true,
order: 1,
} : null), [discussionEnabled]);

@ -1,9 +1,11 @@
import { useMemo, lazy, LazyExoticComponent, FC, useEffect } from 'react';
import { useMemo, lazy, useEffect } from 'react';
import { OTR } from './rocketchat.otr';
import { useSetting } from '../../../client/contexts/SettingsContext';
import { addAction } from '../../../client/views/room/lib/Toolbox';
const template = lazy(() => import('../../../client/views/room/contextualBar/OTR'));
addAction('otr', () => {
const enabled = useSetting('OTR_Enable');
@ -24,7 +26,7 @@ addAction('otr', () => {
id: 'otr',
title: 'OTR',
icon: 'key',
template: lazy(() => import('../../../client/views/room/contextualBar/OTR')) as LazyExoticComponent<FC>,
template,
order: 13,
full: true,
} : null), [shouldAddAction]);

@ -1,4 +1 @@
import './lib/startup';
import './views/cleanHistory.html';
import './views/cleanHistory';
import './views/stylesheets/cleanHistory.css';

@ -1,18 +1,20 @@
import { useMemo } from 'react';
import { useMemo, lazy } from 'react';
import { addAction } from '../../../../client/views/room/lib/Toolbox';
import { usePermission } from '../../../../client/contexts/AuthorizationContext';
const template = lazy(() => import('../../../../client/views/room/contextualBar/PruneMessages'));
addAction('clean-history', ({ room }) => {
const hasPermission = usePermission('clean-channel-history', room._id);
return useMemo(() => (hasPermission ? {
groups: ['channel', 'group', 'direct'],
id: 'clean-history',
anonymous: true,
full: true,
title: 'Prune_Messages',
icon: 'eraser',
template: 'cleanHistory',
template,
order: 250,
} : null), [hasPermission]);
});

@ -1,158 +0,0 @@
<template name="cleanHistory">
{{#unless busy}}
<main class="rc-user-info__scroll">
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Newer_than"}}</div>
<div class="rc-input__wrapper rc-datetime__left">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="calendar"}}
</div>
<input type="date" class="rc-input__element" value="" name="from__date" autocomplete="off" placeholder="YYYY-MM-DD"/>
</div>
<div class="rc-input__wrapper rc-datetime__right">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="clock"}}
</div>
<input type="time" class="rc-input__element" value="" name="from__time" autocomplete="off" placeholder="HH:MM"/>
</div>
</label>
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Older_than"}}</div>
<div class="rc-input__wrapper rc-datetime__left">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="calendar"}}
</div>
<input type="date" class="rc-input__element" value="" name="to__date" autocomplete="off" placeholder="YYYY-MM-DD"/>
</div>
<div class="rc-input__wrapper rc-datetime__right">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="clock"}}
</div>
<input type="time" class="rc-input__element" value="" name="to__time" autocomplete="off" placeholder="HH:MM"/>
</div>
</label>
</div>
<div class="rc-input rc-input--usernames">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Only_from_users"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="at"}}
</div>
<div class="rc-tags">
{{#each user in selectedUsers}}
{{> tag user}}
{{/each}}
<input type="text" class="rc-tags__input" placeholder="{{_ "Username_Placeholder"}}" name="users" autocomplete="off"/>
</div>
</div>
{{#with config}}
{{#if autocomplete 'isShowing'}}
{{> popupList data=config items=items ready=(autocomplete 'isLoaded')}}
{{/if}}
{{/with}}
</label>
</div>
<label class="rc-checkbox">
<input type="checkbox" name="inclusive" class="rc-checkbox__input">
{{> icon icon="check" block="rc-checkbox__icon"}}
<span class="rc-checkbox__text rc-text__small">{{_ "Inclusive"}}</span>
</label>
<label class="rc-checkbox">
<input type="checkbox" name="excludePinned" class="rc-checkbox__input">
{{> icon icon="check" block="rc-checkbox__icon"}}
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotPrunePinned"}}</span>
</label>
<label class="rc-checkbox">
<input type="checkbox" name="ignoreDiscussion" class="rc-checkbox__input">
{{> icon icon="check" block="rc-checkbox__icon"}}
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotPruneDiscussion"}}</span>
</label>
<label class="rc-checkbox">
<input type="checkbox" name="ignoreThreads" class="rc-checkbox__input">
{{> icon icon="check" block="rc-checkbox__icon"}}
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotPruneThreads"}}</span>
</label>
<label class="rc-checkbox">
<input type="checkbox" name="filesOnly" class="rc-checkbox__input">
{{> icon icon="check" block="rc-checkbox__icon"}}
<span class="rc-checkbox__text rc-text__small">{{_ "Files_only"}}</span>
</label>
{{#unless validate}}
<div class="mail-messages__instructions mail-messages__instructions--warning">
<div class="mail-messages__instructions-wrapper">
{{> icon block="mail-messages__instructions-icon" icon="warning"}}
<div class="mail-messages__instructions-text">
{{warningBox}}
</div>
</div>
</div>
{{else}}
<div class="mail-messages__instructions mail-messages__instructions--warning">
<div class="mail-messages__instructions-wrapper">
{{> icon block="mail-messages__instructions-icon" icon="modal-error"}}
<div class="mail-messages__instructions-text">
{{validate}}
</div>
</div>
</div>
{{/unless}}
</main>
<div class="rc-user-info__flex rc-user-info__row">
<button class="rc-button rc-button--cancel js-prune" title="{{_ 'Prune'}}" disabled="{{#with validate}}{{.}}{{/with}}">{{_ 'Prune'}}</button>
</div>
{{else}}
{{#unless finished}}
<main class="rc-user-info__pruning">
<p class="pruning__header">
{{#unless filesOnly}}
{{_ "Pruning_messages"}}
{{else}}
{{_ "Pruning_files"}}
{{/unless}}
</p>
<div class="pruning-wrapper">
{{> icon icon="loading" block="rc-icon"}}
<div class="pruning__text">
{{prunedCount}}
</div>
<div class="pruning__text-sub">
{{#unless filesOnly}}
{{_ "messages_pruned"}}
{{else}}
{{_ "files_pruned"}}
{{/unless}}
</div>
</div>
</main>
{{else}}
<main class="rc-user-info__pruning">
<p class="pruning__header">{{_ "Prune_finished"}}</p>
<div class="pruning-wrapper prune__finished">
{{> icon icon="check" block="rc-icon"}}
<div class="pruning__text">
{{prunedCount}}
</div>
<div class="pruning__text-sub">
{{#unless filesOnly}}
{{#if (isSingular prunedCount)}}
{{_ "message_pruned"}}
{{else}}
{{_ "messages_pruned"}}
{{/if}}
{{else}}
{{#if (isSingular prunedCount)}}
{{_ "file_pruned"}}
{{else}}
{{_ "files_pruned"}}
{{/if}}
{{/unless}}
</div>
</div>
</main>
{{/unless}}
{{/unless}}
</template>

@ -1,351 +0,0 @@
import { Tracker } from 'meteor/tracker';
import { Blaze } from 'meteor/blaze';
import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import moment from 'moment';
import { ChatRoom } from '../../../models';
import { t, roomTypes } from '../../../utils';
import { settings } from '../../../settings';
import { modal, call } from '../../../ui-utils';
import { AutoComplete } from '../../../meteor-autocomplete/client';
const getRoomName = function() {
const room = ChatRoom.findOne(Session.get('openedRoom'));
if (!room) {
return;
}
if (room.name) {
return `#${ room.name }`;
}
return t('conversation_with_s', roomTypes.getRoomName(room.t, room));
};
const purgeWorker = function(roomId, oldest, latest, inclusive, limit, excludePinned, ignoreDiscussion, filesOnly, fromUsers, ignoreThreads) {
return call('cleanRoomHistory', {
roomId,
latest,
oldest,
inclusive,
limit,
excludePinned,
ignoreDiscussion,
filesOnly,
fromUsers,
ignoreThreads,
});
};
const getTimeZoneOffset = function() {
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) }`;
};
const filterNames = (old) => {
const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
return [...old.replace(' ', '').toLocaleLowerCase()].filter((f) => reg.test(f)).join('');
};
Template.cleanHistory.helpers({
roomId() {
const room = ChatRoom.findOne(Session.get('openedRoom'));
return room && room._id;
},
roomName() {
return getRoomName();
},
warningBox() {
return Template.instance().warningBox.get();
},
validate() {
return Template.instance().validate.get();
},
filesOnly() {
return Template.instance().cleanHistoryFilesOnly.get();
},
busy() {
return Template.instance().cleanHistoryBusy.get();
},
finished() {
return Template.instance().cleanHistoryFinished.get();
},
prunedCount() {
return Template.instance().cleanHistoryPrunedCount.get();
},
config() {
const filter = Template.instance().userFilter;
return {
filter: filter.get(),
noMatchTemplate: 'userSearchEmpty',
modifier(text) {
const f = filter.get();
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), function(part) {
return `<strong>${ part }</strong>`;
}) }`;
},
};
},
selectedUsers() {
return Template.instance().selectedUsers.get();
},
autocomplete(key) {
const instance = Template.instance();
const param = instance.ac[key];
return typeof param === 'function' ? param.apply(instance.ac) : param;
},
items() {
return Template.instance().ac.filteredList();
},
isSingular(prunedCount) {
return prunedCount === 1;
},
});
Template.cleanHistory.onCreated(function() {
this.warningBox = new ReactiveVar('');
this.validate = new ReactiveVar('');
this.selectedUsers = new ReactiveVar([]);
this.userFilter = new ReactiveVar('');
this.cleanHistoryFromDate = new ReactiveVar('');
this.cleanHistoryFromTime = new ReactiveVar('');
this.cleanHistoryToDate = new ReactiveVar('');
this.cleanHistoryToTime = new ReactiveVar('');
this.cleanHistorySelectedUsers = new ReactiveVar([]);
this.cleanHistoryInclusive = new ReactiveVar(false);
this.cleanHistoryExcludePinned = new ReactiveVar(false);
this.cleanHistoryFilesOnly = new ReactiveVar(false);
this.ignoreDiscussion = new ReactiveVar(false);
this.ignoreThreads = new ReactiveVar(false);
this.cleanHistoryBusy = new ReactiveVar(false);
this.cleanHistoryFinished = new ReactiveVar(false);
this.cleanHistoryPrunedCount = new ReactiveVar(0);
this.ac = new AutoComplete(
{
selector: {
item: '.rc-popup-list__item',
container: '.rc-popup-list__list',
},
limit: 10,
inputDelay: 300,
rules: [
{
collection: 'UserAndRoom',
endpoint: 'users.autocomplete',
field: 'username',
matchAll: true,
doNotChangeWidth: false,
selector(match) {
return { term: match };
},
sort: 'username',
},
],
});
this.ac.tmplInst = this;
});
Template.cleanHistory.onRendered(function() {
const users = this.selectedUsers;
const selUsers = this.cleanHistorySelectedUsers;
this.ac.element = this.firstNode.parentElement.querySelector('[name="users"]');
this.ac.$element = $(this.ac.element);
this.ac.$element.on('autocompleteselect', function(e, { item }) {
const usersArr = users.get();
usersArr.push(item);
users.set(usersArr);
selUsers.set(usersArr);
});
Tracker.autorun(() => {
const metaFromDate = this.cleanHistoryFromDate.get();
const metaFromTime = this.cleanHistoryFromTime.get();
const metaToDate = this.cleanHistoryToDate.get();
const metaToTime = this.cleanHistoryToTime.get();
const metaSelectedUsers = this.cleanHistorySelectedUsers.get();
const metaCleanHistoryExcludePinned = this.cleanHistoryExcludePinned.get();
const metaCleanHistoryFilesOnly = this.cleanHistoryFilesOnly.get();
let fromDate = new Date('0001-01-01T00:00:00Z');
let toDate = new Date('9999-12-31T23:59:59Z');
if (metaFromDate) {
fromDate = new Date(`${ metaFromDate }T${ metaFromTime || '00:00' }:00${ getTimeZoneOffset() }`);
}
if (metaToDate) {
toDate = new Date(`${ metaToDate }T${ metaToTime || '00:00' }:00${ getTimeZoneOffset() }`);
}
const exceptPinned = metaCleanHistoryExcludePinned ? ` ${ t('except_pinned', {}) }` : '';
const ifFrom = metaSelectedUsers.length ? ` ${ t('if_they_are_from', {
postProcess: 'sprintf',
sprintf: [metaSelectedUsers.map((element) => element.username).join(', ')],
}) }` : '';
const filesOrMessages = t(metaCleanHistoryFilesOnly ? 'files' : 'messages', {});
if (metaFromDate && metaToDate) {
this.warningBox.set(t('Prune_Warning_between', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, getRoomName(), moment(fromDate).format('L LT'), moment(toDate).format('L LT')],
}) + exceptPinned + ifFrom);
} else if (metaFromDate) {
this.warningBox.set(t('Prune_Warning_after', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, getRoomName(), moment(fromDate).format('L LT')],
}) + exceptPinned + ifFrom);
} else if (metaToDate) {
this.warningBox.set(t('Prune_Warning_before', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, getRoomName(), moment(toDate).format('L LT')],
}) + exceptPinned + ifFrom);
} else {
this.warningBox.set(t('Prune_Warning_all', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, getRoomName()],
}) + exceptPinned + ifFrom);
}
if (fromDate > toDate) {
return this.validate.set(t('Newer_than_may_not_exceed_Older_than', {
postProcess: 'sprintf',
sprintf: [],
}));
}
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
return this.validate.set(t('error-invalid-date', {
postProcess: 'sprintf',
sprintf: [],
}));
}
this.validate.set('');
});
});
Template.cleanHistory.events({
'change [name=from__date]'(e, instance) {
instance.cleanHistoryFromDate.set(e.target.value);
},
'change [name=from__time]'(e, instance) {
instance.cleanHistoryFromTime.set(e.target.value);
},
'change [name=to__date]'(e, instance) {
instance.cleanHistoryToDate.set(e.target.value);
},
'change [name=to__time]'(e, instance) {
instance.cleanHistoryToTime.set(e.target.value);
},
'change [name=inclusive]'(e, instance) {
instance.cleanHistoryInclusive.set(e.target.checked);
},
'change [name=excludePinned]'(e, instance) {
instance.cleanHistoryExcludePinned.set(e.target.checked);
},
'change [name=filesOnly]'(e, instance) {
instance.cleanHistoryFilesOnly.set(e.target.checked);
},
'change [name=ignoreDiscussion]'(e, instance) {
instance.ignoreDiscussion.set(e.target.checked);
},
'change [name=ignoreThreads]'(e, instance) {
instance.ignoreThreads.set(e.target.checked);
},
'click .js-prune'(e, instance) {
modal.open({
title: t('Are_you_sure'),
text: t('Prune_Modal'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_prune_them'),
cancelButtonText: t('Cancel'),
closeOnConfirm: true,
html: false,
}, async function() {
instance.cleanHistoryBusy.set(true);
const metaFromDate = instance.cleanHistoryFromDate.get();
const metaFromTime = instance.cleanHistoryFromTime.get();
const metaToDate = instance.cleanHistoryToDate.get();
const metaToTime = instance.cleanHistoryToTime.get();
const metaSelectedUsers = instance.cleanHistorySelectedUsers.get();
const metaCleanHistoryInclusive = instance.cleanHistoryInclusive.get();
const metaCleanHistoryExcludePinned = instance.cleanHistoryExcludePinned.get();
const metaCleanHistoryFilesOnly = instance.cleanHistoryFilesOnly.get();
const ignoreDiscussion = instance.ignoreDiscussion.get();
const ignoreThreads = instance.ignoreThreads.get();
let fromDate = new Date('0001-01-01T00:00:00Z');
let toDate = new Date('9999-12-31T23:59:59Z');
if (metaFromDate) {
fromDate = new Date(`${ metaFromDate }T${ metaFromTime || '00:00' }:00${ getTimeZoneOffset() }`);
}
if (metaToDate) {
toDate = new Date(`${ metaToDate }T${ metaToTime || '00:00' }:00${ getTimeZoneOffset() }`);
}
const roomId = Session.get('openedRoom');
const users = metaSelectedUsers.map((element) => element.username);
const limit = 2000;
let count = 0;
let result;
do {
result = await purgeWorker(roomId, fromDate, toDate, metaCleanHistoryInclusive, limit, metaCleanHistoryExcludePinned, ignoreDiscussion, metaCleanHistoryFilesOnly, users, ignoreThreads); // eslint-disable-line no-await-in-loop
count += result;
} while (result === limit);
instance.cleanHistoryPrunedCount.set(count);
instance.cleanHistoryFinished.set(true);
});
},
'click .rc-input--usernames .rc-tags__tag'({ target }, t) {
const { username } = Blaze.getData(target);
t.selectedUsers.set(t.selectedUsers.get().filter((user) => user.username !== username));
t.cleanHistorySelectedUsers.set(t.selectedUsers.get());
},
'click .rc-popup-list__item'(e, t) {
t.ac.onItemClick(this, e);
},
'input [name="users"]'(e, t) {
const input = e.target;
const position = input.selectionEnd || input.selectionStart;
const { length } = input.value;
const modified = filterNames(input.value);
input.value = modified;
document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length);
t.userFilter.set(modified);
},
'keydown [name="users"]'(e, t) {
if ([8, 46].includes(e.keyCode) && e.target.value === '') {
const users = t.selectedUsers;
const usersArr = users.get();
usersArr.pop();
t.cleanHistorySelectedUsers.set(usersArr);
return users.set(usersArr);
}
t.ac.onKeyDown(e);
},
'keyup [name="users"]'(e, t) {
t.ac.onKeyUp(e);
},
'focus [name="users"]'(e, t) {
t.ac.onFocus(e);
},
'blur [name="users"]'(e, t) {
t.ac.onBlur(e);
},
});

@ -1,59 +0,0 @@
.rc-datetime__left {
display: inline-block;
width: 52%;
}
.rc-datetime__right {
display: inline-block;
width: calc(48% - 0.3rem);
}
.rc-user-info__pruning {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, calc(-50% - 2rem));
}
.pruning__header {
text-align: center;
font-weight: 900;
}
.pruning-wrapper {
text-align: center;
color: var(--rc-color-link-active);
&.prune__finished {
color: #12c212;
}
& .rc-icon--loading {
width: 16rem;
height: 16rem;
margin: 1rem 0;
animation: spin 2s linear infinite;
}
& .rc-icon--check {
font-size: 1rem;
}
& .pruning__text {
margin-top: -17rem;
font-size: 3.5em;
line-height: 16rem;
}
& .pruning__text-sub {
margin-top: calc(-8rem + 1.5em);
}
}

@ -89,3 +89,7 @@ createTemplateForComponent('UserInfoWithData', () => import('./contextualBar/Use
createTemplateForComponent('channelFilesList', () => import('./contextualBar/RoomFiles/RoomFiles'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});
createTemplateForComponent('PruneMessages', () => import('./contextualBar/PruneMessages'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});

@ -0,0 +1,319 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Field, ButtonGroup, Button, CheckBox, InputBox, Box, Margins, Callout } from '@rocket.chat/fuselage';
import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks';
import moment from 'moment';
import UserAutoCompleteMultiple from '../../../../../ee/client/audit/UserAutoCompleteMultiple';
import { useTranslation } from '../../../../contexts/TranslationContext';
import VerticalBar from '../../../../components/VerticalBar';
import { useUserRoom } from '../../../../contexts/UserContext';
import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext';
import { useSetModal } from '../../../../contexts/ModalContext';
import { useForm } from '../../../../hooks/useForm';
import { useMethod } from '../../../../contexts/ServerContext';
import DeleteWarningModal from '../../../../components/DeleteWarningModal';
const getTimeZoneOffset = function() {
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 DialogPruneMessages = ({ children, ...props }) =>
<DeleteWarningModal {...props}><Box textAlign='center' fontScale='s1'>{children}</Box></DeleteWarningModal>;
export const DateTimeRow = ({
label,
dateTime,
handleDateTime,
}) => (
<Field >
<Field.Label flexGrow={0}>{label}</Field.Label>
<Box display='flex' mi='neg-x4'>
<Margins inline='x4'>
<InputBox type='date' value={dateTime?.date} onChange={handleDateTime?.date} flexGrow={1} h='x20'/>
<InputBox type='time' value={dateTime?.time} onChange={handleDateTime?.time} flexGrow={1} h='x20'/>
</Margins>
</Box>
</Field>
);
export const PruneMessages = ({
callOutText,
validateText,
newerDateTime,
handleNewerDateTime,
olderDateTime,
handleOlderDateTime,
users,
inclusive,
pinned,
discussion,
threads,
attached,
handleInclusive,
handlePinned,
handleDiscussion,
handleThreads,
handleAttached,
onClickClose,
onClickPrune,
onChangeUsers,
}) => {
const t = useTranslation();
const inclusiveCheckboxId = useUniqueId();
const pinnedCheckboxId = useUniqueId();
const discussionCheckboxId = useUniqueId();
const threadsCheckboxId = useUniqueId();
const attachedCheckboxId = useUniqueId();
return (
<>
<VerticalBar.Header>
<VerticalBar.Icon name='eraser' />
<VerticalBar.Text>{t('Prune_Messages')}</VerticalBar.Text>
{onClickClose && <VerticalBar.Close onClick={onClickClose} />}
</VerticalBar.Header>
<VerticalBar.ScrollableContent>
<DateTimeRow label={t('Newer_than')} dateTime={newerDateTime} handleDateTime={handleNewerDateTime} />
<DateTimeRow label={t('Older_than')} dateTime={olderDateTime} handleDateTime={handleOlderDateTime} />
<Field >
<Field.Label flexGrow={0}>{t('Only_from_users')}</Field.Label>
<UserAutoCompleteMultiple value={users} onChange={onChangeUsers} placeholder={t('Please_enter_usernames')} />
</Field>
<Field>
<Field.Row>
<CheckBox id={inclusiveCheckboxId} checked={inclusive} onChange={handleInclusive} />
<Field.Label htmlFor={inclusiveCheckboxId}>{t('Inclusive')}</Field.Label>
</Field.Row>
</Field>
<Field>
<Field.Row>
<CheckBox id={pinnedCheckboxId} checked={pinned} onChange={handlePinned} />
<Field.Label htmlFor={pinnedCheckboxId}>{t('RetentionPolicy_DoNotPrunePinned')}</Field.Label>
</Field.Row>
</Field>
<Field>
<Field.Row>
<CheckBox id={discussionCheckboxId} checked={discussion} onChange={handleDiscussion} />
<Field.Label htmlFor={discussionCheckboxId}>{t('RetentionPolicy_DoNotPruneDiscussion')}</Field.Label>
</Field.Row>
</Field>
<Field>
<Field.Row>
<CheckBox id={threadsCheckboxId} checked={threads} onChange={handleThreads} />
<Field.Label htmlFor={threadsCheckboxId}>{t('RetentionPolicy_DoNotPruneThreads')}</Field.Label>
</Field.Row>
</Field>
<Field>
<Field.Row>
<CheckBox id={attachedCheckboxId} checked={attached} onChange={handleAttached} />
<Field.Label htmlFor={attachedCheckboxId}>{t('Files_only')}</Field.Label>
</Field.Row>
</Field>
{callOutText && !validateText && <Callout type='warning'>{callOutText}</Callout>}
{validateText && <Callout type='warning'>{validateText}</Callout>}
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<ButtonGroup stretch>
<Button primary danger disabled={validateText && true} onClick={onClickPrune}>{t('Prune')}</Button>
</ButtonGroup>
</VerticalBar.Footer>
</>
);
};
const initialValues = {
newerDate: '',
newerTime: '',
olderDate: '',
olderTime: '',
users: [],
inclusive: false,
pinned: false,
discussion: false,
threads: false,
attached: false,
};
export default ({
rid,
tabBar,
}) => {
const t = useTranslation();
const room = useUserRoom(rid);
room.type = room.t;
room.rid = rid;
const { name } = room;
const setModal = useSetModal();
const onClickClose = useMutableCallback(() => tabBar && tabBar.close());
const closeModal = useCallback(() => setModal(null), [setModal]);
const dispatchToastMessage = useToastMessageDispatch();
const pruneMessages = useMethod('cleanRoomHistory');
const [fromDate, setFromDate] = useState(new Date('0001-01-01T00:00:00Z'));
const [toDate, setToDate] = useState(new Date('9999-12-31T23:59:59Z'));
const [callOutText, setCallOutText] = useState();
const [validateText, setValidateText] = useState();
const [count, setCount] = useState(0);
const { values, handlers, reset } = useForm(initialValues);
const {
newerDate,
newerTime,
olderDate,
olderTime,
users,
inclusive,
pinned,
discussion,
threads,
attached,
} = values;
const {
handleNewerDate,
handleNewerTime,
handleOlderDate,
handleOlderTime,
handleUsers,
handleInclusive,
handlePinned,
handleDiscussion,
handleThreads,
handleAttached,
} = handlers;
const onChangeUsers = useMutableCallback((value, action) => {
if (!action) {
if (users.includes(value)) {
return;
}
return handleUsers([...users, value]);
}
handleUsers(users.filter((current) => current !== value));
});
const handlePrune = useMutableCallback(async () => {
const limit = 2000;
let result;
try {
if (count === limit) {
return;
}
result = await pruneMessages({ roomId: rid, latest: toDate, oldest: fromDate, inclusive, limit, excludePinned: pinned, ignoreDiscussion: discussion, filesOnly: attached, fromUsers: users, ignoreThreads: threads });
setCount(result);
if (result < 1) {
throw new Error(t('No_messages_found_to_prune'));
}
dispatchToastMessage({ type: 'success', message: `${ result } ${ t('messages_pruned') }` });
closeModal();
reset();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
closeModal();
}
});
const handleModal = () => {
setModal(<DialogPruneMessages
onCancel={closeModal}
onDelete={handlePrune}
deleteText={t('Yes_prune_them')}
>{t('Prune_Modal')}</DialogPruneMessages>);
};
useEffect(() => {
if (newerDate) {
setFromDate(new Date(`${ newerDate }T${ newerTime || '00:00' }:00${ getTimeZoneOffset() }`));
}
if (olderDate) {
setToDate(new Date(`${ olderDate }T${ olderTime || '24:00' }:00${ getTimeZoneOffset() }`));
}
}, [newerDate, newerTime, olderDate, olderTime]);
useEffect(() => {
const exceptPinned = pinned ? ` ${ t('except_pinned', {}) }` : '';
const ifFrom = users.length ? ` ${ t('if_they_are_from', {
postProcess: 'sprintf',
sprintf: [users.map((element) => element).join(', ')],
}) }` : '';
const filesOrMessages = t(attached ? 'files' : 'messages', {});
if (newerDate && olderDate) {
setCallOutText(t('Prune_Warning_between', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, name, moment(fromDate).format('L LT'), moment(toDate).format('L LT')],
}) + exceptPinned + ifFrom);
} else if (newerDate) {
setCallOutText(t('Prune_Warning_after', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, name, moment(fromDate).format('L LT')],
}) + exceptPinned + ifFrom);
} else if (olderDate) {
setCallOutText(t('Prune_Warning_before', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, name, moment(toDate).format('L LT')],
}) + exceptPinned + ifFrom);
} else {
setCallOutText(t('Prune_Warning_all', {
postProcess: 'sprintf',
sprintf: [filesOrMessages, name],
}) + exceptPinned + ifFrom);
}
if (fromDate > toDate) {
return setValidateText(t('Newer_than_may_not_exceed_Older_than', {
postProcess: 'sprintf',
sprintf: [],
}));
}
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
return setValidateText(t('error-invalid-date', {
postProcess: 'sprintf',
sprintf: [],
}));
}
setValidateText();
}, [newerDate, olderDate, fromDate, toDate, attached, name, t, pinned, users]);
return (
<PruneMessages
callOutText={callOutText}
validateText={validateText}
newerDateTime={{ date: newerDate, time: newerTime }}
handleNewerDateTime={{ date: handleNewerDate, time: handleNewerTime }}
olderDateTime={{ date: olderDate, time: olderTime }}
handleOlderDateTime={{ date: handleOlderDate, time: handleOlderTime }}
users={users}
inclusive={inclusive}
pinned={pinned}
discussion={discussion}
threads={threads}
attached={attached}
handleInclusive={handleInclusive}
handlePinned={handlePinned}
handleDiscussion={handleDiscussion}
handleThreads={handleThreads}
handleAttached={handleAttached}
onClickClose={onClickClose}
onClickPrune={handleModal}
onChangeUsers={onChangeUsers}
/>
);
};

@ -0,0 +1,23 @@
import React from 'react';
import { PruneMessages } from './PruneMessages';
import VerticalBar from '../../../../components/VerticalBar';
export default {
title: 'components/PruneMessages',
component: PruneMessages,
};
export const Default = () => <VerticalBar>
<PruneMessages
onClickClose={alert}
/>
</VerticalBar>;
export const withCallout = () => <VerticalBar>
<PruneMessages
onClickClose={alert}
pinned={true}
callOutText='Lorem Ipsum Ipsum Ipsum'
/>
</VerticalBar>;

@ -0,0 +1,3 @@
import PruneMessages from './PruneMessages';
export default PruneMessages;

@ -1,4 +1,4 @@
import { useMemo, lazy, LazyExoticComponent, FC } from 'react';
import { useMemo, lazy } from 'react';
import { usePermission } from '../../../../contexts/AuthorizationContext';
@ -19,7 +19,7 @@ addAction('user-info', {
id: 'user-info',
title: 'User_Info',
icon: 'user',
template: lazy(() => import('../../MemberListRouter')) as LazyExoticComponent<FC>,
template: lazy(() => import('../../MemberListRouter')),
order: 5,
});
@ -28,7 +28,7 @@ addAction('user-info-group', {
id: 'user-info-group',
title: 'Members',
icon: 'team',
template: lazy(() => import('../../MemberListRouter')) as LazyExoticComponent<FC>,
template: lazy(() => import('../../MemberListRouter')),
order: 5,
});
@ -39,7 +39,7 @@ addAction('members-list', ({ room }) => {
id: 'members-list',
title: 'Members',
icon: 'team',
template: lazy(() => import('../../MemberListRouter')) as LazyExoticComponent<FC>,
template: lazy(() => import('../../MemberListRouter')),
order: 5,
} : null), [hasPermission, room.broadcast]);
});
@ -49,7 +49,7 @@ addAction('uploaded-files-list', {
id: 'uploaded-files-list',
title: 'Files',
icon: 'clip',
template: lazy(() => import('../../contextualBar/RoomFiles')) as LazyExoticComponent<FC>,
template: lazy(() => import('../../contextualBar/RoomFiles')),
order: 6,
});

@ -31,7 +31,7 @@ export type ToolboxActionConfig = {
groups: Array<'group' | 'channel' | 'live' | 'direct' | 'direct_multiple'>;
hotkey?: string;
action?: (e: MouseEvent<HTMLElement>) => void;
template?: string | FC | JSX.Element | LazyExoticComponent<FC>;
template?: string | FC | LazyExoticComponent<FC<{ rid: string; tabBar: any }>>;
}
export type ToolboxAction = ToolboxHook | ToolboxActionConfig;

@ -2764,6 +2764,7 @@
"No_Limit": "No Limit",
"No_livechats": "You have no livechats",
"No_mentions_found": "No mentions found",
"No_messages_found_to_prune": "No messages found to prune",
"No_messages_yet": "No messages yet",
"No_pages_yet_Try_hitting_Reload_Pages_button": "No pages yet. Try hitting \"Reload Pages\" button.",
"No_pinned_messages": "No pinned messages",
@ -2944,6 +2945,7 @@
"Please_add_a_comment": "Please add a comment",
"Please_add_a_comment_to_close_the_room": "Please, add a comment to close the room",
"Please_answer_survey": "Please take a moment to answer a quick survey about this chat",
"Please_enter_usernames": "Please enter usernames...",
"please_enter_valid_domain": "Please enter a valid domain",
"Please_enter_value_for_url": "Please enter a value for the url of your avatar.",
"Please_enter_your_new_password_below": "Please enter your new password below:",

Loading…
Cancel
Save