<|start_filename|>client/components/SortList/SortModeList.js<|end_filename|>
import { RadioButton, OptionTitle } from '@rocket.chat/fuselage';
import React, { useCallback } from 'react';

import { useMethod } from '../../contexts/ServerContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useUserPreference } from '../../contexts/UserContext';
import ListItem from '../Sidebar/ListItem';

const style = {
	textTransform: 'uppercase',
};

const checkBoxStyle = {
	paddingLeft: '24px',
	paddingInlineStart: '24px',
};

function SortModeList() {
	const t = useTranslation();
	const saveUserPreferences = useMethod('saveUserPreferences');
	const sidebarSortBy = useUserPreference('sidebarSortby', 'activity');

	const useHandleChange = (value) =>
		useCallback(() => saveUserPreferences({ sidebarSortby: value }), [value]);

	const setToAlphabetical = useHandleChange('alphabetical');
	const setToActivity = useHandleChange('activity');

	return (
		<>
			<OptionTitle style={style}>{t('Sort_By')}</OptionTitle>
			<ul className='rc-popover__list'>
				<ListItem
					icon={'clock'}
					text={t('Activity')}
					input={
						<RadioButton
							style={checkBoxStyle}
							name='sidebarSortby'
							onChange={setToActivity}
							checked={sidebarSortBy === 'activity'}
						/>
					}
				/>
				<ListItem
					icon={'sort-az'}
					text={t('Name')}
					input={
						<RadioButton
							style={checkBoxStyle}
							name='sidebarSortby'
							onChange={setToAlphabetical}
							checked={sidebarSortBy === 'alphabetical'}
						/>
					}
				/>
			</ul>
		</>
	);
}

export default SortModeList;


<|start_filename|>client/components/UserCard/UserCard.js<|end_filename|>
import { Box, ActionButton, Skeleton } from '@rocket.chat/fuselage';
import React, { forwardRef } from 'react';

import MarkdownText from '../MarkdownText';
import * as Status from '../UserStatus';
import UserAvatar from '../avatar/UserAvatar';
import Info from './Info';
import Roles from './Roles';
import UserCardContainer from './UserCardContainer';
import Username from './Username';

const clampStyle = {
	display: '-webkit-box',
	overflow: 'hidden',
	WebkitLineClamp: 3,
	WebkitBoxOrient: 'vertical',
	wordBreak: 'break-all',
};

const UserCard = forwardRef(function UserCard(
	{
		className,
		style,
		open,
		name = <Skeleton width='100%' />,
		username,
		etag,
		customStatus = <Skeleton width='100%' />,
		roles = (
			<>
				<Skeleton width='32%' mi='x2' />
				<Skeleton width='32%' mi='x2' />
				<Skeleton width='32%' mi='x2' />
			</>
		),
		bio = (
			<>
				<Skeleton width='100%' />
				<Skeleton width='100%' />
				<Skeleton width='100%' />
			</>
		),
		status = <Status.Offline />,
		actions,
		localTime = <Skeleton width='100%' />,
		onClose,
		nickname,
		t = (e) => e,
	},
	ref,
) {
	return (
		<UserCardContainer className={className} ref={ref} style={style}>
			<Box>
				<UserAvatar username={username} etag={etag} size='x124' />
				{actions && (
					<Box flexGrow={0} display='flex' mb='x8' align='center' justifyContent='center'>
						{actions}
					</Box>
				)}
			</Box>
			<Box display='flex' flexDirection='column' flexGrow={1} flexShrink={1} mis='x24' width='1px'>
				<Box withTruncatedText display='flex'>
					<Username status={status} name={name} title={username !== name ? username : undefined} />
					{nickname && (
						<Box title={t('Nickname')} color='hint' mis='x8' fontScale='p2' withTruncatedText>
							({nickname})
						</Box>
					)}
				</Box>
				{customStatus && (
					<Info>
						{typeof customStatus === 'string' ? (
							<MarkdownText content={customStatus} />
						) : (
							customStatus
						)}
					</Info>
				)}
				<Roles>{roles}</Roles>
				<Info>{localTime}</Info>
				{bio && (
					<Info withTruncatedText={false} style={clampStyle} height='x60'>
						{typeof bio === 'string' ? <MarkdownText content={bio} /> : bio}
					</Info>
				)}
				{open && <a onClick={open}>{t('See_full_profile')}</a>}
			</Box>
			{onClose && (
				<Box>
					<ActionButton ghost icon='cross' onClick={onClose} />
				</Box>
			)}
		</UserCardContainer>
	);
});

export default UserCard;


<|start_filename|>client/views/room/contextualBar/OTR/OTR.js<|end_filename|>
import { Box, Button, ButtonGroup, Throbber } from '@rocket.chat/fuselage';
import React from 'react';

import VerticalBar from '../../../../components/VerticalBar';
import { useTranslation } from '../../../../contexts/TranslationContext';

const OTR = ({
	isEstablishing,
	isEstablished,
	isOnline,
	onClickClose,
	onClickStart,
	onClickEnd,
	onClickRefresh,
}) => {
	const t = useTranslation();

	return (
		<>
			<VerticalBar.Header>
				<VerticalBar.Icon name='shredder' />
				<VerticalBar.Text>{t('OTR')}</VerticalBar.Text>
				{onClickClose && <VerticalBar.Close onClick={onClickClose} />}
			</VerticalBar.Header>

			<VerticalBar.ScrollableContent p='x24'>
				<Box fontScale='h4'>{t('Off_the_record_conversation')}</Box>

				{!isEstablishing && !isEstablished && isOnline && (
					<Button onClick={onClickStart} primary>
						{t('Start_OTR')}
					</Button>
				)}
				{isEstablishing && !isEstablished && isOnline && (
					<>
						{' '}
						<Box fontScale='p2'>{t('Please_wait_while_OTR_is_being_established')}</Box>{' '}
						<Throbber inheritColor />{' '}
					</>
				)}
				{isEstablished && isOnline && (
					<ButtonGroup stretch>
						{onClickRefresh && (
							<Button width='50%' onClick={onClickRefresh}>
								{t('Refresh_keys')}
							</Button>
						)}
						{onClickEnd && (
							<Button width='50%' danger onClick={onClickEnd}>
								{t('End_OTR')}
							</Button>
						)}
					</ButtonGroup>
				)}

				{!isOnline && (
					<Box fontScale='p2m'>{t('OTR_is_only_available_when_both_users_are_online')}</Box>
				)}
			</VerticalBar.ScrollableContent>
		</>
	);
};

export default OTR;


<|start_filename|>client/views/admin/info/OfflineLicenseModal.js<|end_filename|>
import {
	Modal,
	Box,
	ButtonGroup,
	Button,
	Scrollable,
	Callout,
	Margins,
	Icon,
} from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState } from 'react';

import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointActionExperimental } from '../../../hooks/useEndpointActionExperimental';

const OfflineLicenseModal = ({ onClose, license, licenseStatus, ...props }) => {
	const t = useTranslation();

	const dispatchToastMessage = useToastMessageDispatch();

	const [newLicense, setNewLicense] = useState(license);
	const [isUpdating, setIsUpdating] = useState(false);
	const [status, setStatus] = useState(licenseStatus);
	const [lastSetLicense, setLastSetLicense] = useState(license);

	const handleNewLicense = (e) => {
		setNewLicense(e.currentTarget.value);
	};

	const hasChanges = lastSetLicense !== newLicense;

	const addLicense = useEndpointActionExperimental(
		'POST',
		'licenses.add',
		t('Cloud_License_applied_successfully'),
	);

	const handlePaste = useMutableCallback(async () => {
		try {
			const text = await navigator.clipboard.readText();
			setNewLicense(text);
		} catch (error) {
			dispatchToastMessage({ type: 'error', message: `${t('Paste_error')}: ${error}` });
		}
	});

	const handleApplyLicense = useMutableCallback(async () => {
		setIsUpdating(true);
		setLastSetLicense(newLicense);
		const data = await addLicense({ license: newLicense });
		if (data.success) {
			onClose();
			return;
		}
		setIsUpdating(false);
		setStatus('invalid');
	});

	return (
		<Modal {...props}>
			<Modal.Header>
				<Modal.Title>{t('Cloud_Apply_Offline_License')}</Modal.Title>
				<Modal.Close onClick={onClose} />
			</Modal.Header>
			<Modal.Content>
				<Box withRichContent>
					<p>{t('Cloud_register_offline_finish_helper')}</p>
				</Box>
				<Box
					display='flex'
					flexDirection='column'
					alignItems='stretch'
					paddingInline='x16'
					pb='x8'
					flexGrow={1}
					backgroundColor='neutral-800'
					mb={status === 'invalid' && 'x8'}
				>
					<Margins block='x8'>
						<Scrollable vertical>
							<Box
								is='textarea'
								height='x108'
								fontFamily='mono'
								fontScale='p2'
								color='alternative'
								style={{ wordBreak: 'break-all', resize: 'none' }}
								placeholder={t('Paste_here')}
								disabled={isUpdating}
								value={newLicense}
								autoComplete='off'
								autoCorrect='off'
								autoCapitalize='off'
								spellCheck='false'
								onChange={handleNewLicense}
							/>
						</Scrollable>
						<ButtonGroup align='start'>
							<Button primary small disabled={isUpdating} onClick={handlePaste}>
								<Icon name='clipboard' />
								{t('Paste')}
							</Button>
						</ButtonGroup>
					</Margins>
				</Box>
				{status === 'invalid' && <Callout type='danger'>{t('Cloud_Invalid_license')}</Callout>}
			</Modal.Content>
			<Modal.Footer>
				<ButtonGroup align='end'>
					<Button primary disabled={!hasChanges || isUpdating} onClick={handleApplyLicense}>
						{t('Cloud_Apply_license')}
					</Button>
				</ButtonGroup>
			</Modal.Footer>
		</Modal>
	);
};

export default OfflineLicenseModal;


<|start_filename|>client/sidebar/RoomList/SideBarItemTemplateWithData.js<|end_filename|>
import { Badge, Sidebar } from '@rocket.chat/fuselage';
import React, { memo } from 'react';

import { roomTypes } from '../../../app/utils/client';
import { RoomIcon } from '../../components/RoomIcon';
import { useLayout } from '../../contexts/LayoutContext';
import RoomMenu from '../RoomMenu';
import { normalizeSidebarMessage } from './normalizeSidebarMessage';

const getMessage = (room, lastMessage, t) => {
	if (!lastMessage) {
		return t('No_messages_yet');
	}
	if (!lastMessage.u) {
		return normalizeSidebarMessage(lastMessage, t);
	}
	if (lastMessage.u?.username === room.u?.username) {
		return `${t('You')}: ${normalizeSidebarMessage(lastMessage, t)}`;
	}
	if (room.t === 'd' && room.uids && room.uids.length <= 2) {
		return normalizeSidebarMessage(lastMessage, t);
	}
	return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(
		lastMessage,
		t,
	)}`;
};

function SideBarItemTemplateWithData({
	room,
	id,
	extended,
	selected,
	SideBarItemTemplate,
	AvatarTemplate,
	t,
	style,
	// sidebarViewMode,
	isAnonymous,
}) {
	const { sidebar } = useLayout();

	const href = roomTypes.getRouteLink(room.t, room);
	const title = roomTypes.getRoomName(room.t, room);

	const {
		lastMessage,
		hideUnreadStatus,
		unread = 0,
		alert,
		userMentions,
		groupMentions,
		tunread = [],
		tunreadUser = [],
		rid,
		t: type,
		cl,
	} = room;

	const highlighted = !hideUnreadStatus && (alert || unread);
	const icon = (
		<Sidebar.Item.Icon highlighted={highlighted}>
			<RoomIcon highlighted={highlighted} room={room} placement='sidebar' />
		</Sidebar.Item.Icon>
	);

	const isQueued = room.status === 'queued';

	const threadUnread = tunread.length > 0;
	const message = extended && getMessage(room, lastMessage, t);

	const subtitle = message ? (
		<span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: message }} />
	) : null;
	const variant =
		((userMentions || tunreadUser.length) && 'danger') ||
		(threadUnread && 'primary') ||
		(groupMentions && 'warning') ||
		'ghost';
	const isUnread = unread > 0 || threadUnread;
	const badges =
		!hideUnreadStatus && isUnread ? (
			<Badge style={{ flexShrink: 0 }} variant={variant}>
				{unread + tunread?.length}
			</Badge>
		) : null;

	return (
		<SideBarItemTemplate
			is='a'
			id={id}
			data-qa='sidebar-item'
			aria-level='2'
			unread={highlighted}
			threadUnread={threadUnread}
			selected={selected}
			href={href}
			onClick={() => !selected && sidebar.toggle()}
			aria-label={title}
			title={title}
			time={lastMessage?.ts}
			subtitle={subtitle}
			icon={icon}
			style={style}
			badges={badges}
			avatar={AvatarTemplate && <AvatarTemplate {...room} />}
			menu={
				!isAnonymous &&
				!isQueued &&
				(() => (
					<RoomMenu
						alert={alert}
						threadUnread={threadUnread}
						rid={rid}
						unread={!!unread}
						roomOpen={false}
						type={type}
						cl={cl}
						name={title}
						status={room.status}
					/>
				))
			}
		/>
	);
}

const propsAreEqual = (prevProps, nextProps) => {
	if (
		[
			'id',
			'style',
			'extended',
			'selected',
			'SideBarItemTemplate',
			'AvatarTemplate',
			't',
			'sidebarViewMode',
		].some((key) => prevProps[key] !== nextProps[key])
	) {
		return false;
	}

	if (prevProps.room === nextProps.room) {
		return true;
	}

	if (prevProps.room._id !== nextProps.room._id) {
		return false;
	}
	if (prevProps.room._updatedAt?.toISOString() !== nextProps.room._updatedAt?.toISOString()) {
		return false;
	}
	if (
		prevProps.room.lastMessage?._updatedAt?.toISOString() !==
		nextProps.room.lastMessage?._updatedAt?.toISOString()
	) {
		return false;
	}
	if (prevProps.room.alert !== nextProps.room.alert) {
		return false;
	}
	if (prevProps.room.v?.status !== nextProps.room.v?.status) {
		return false;
	}
	if (prevProps.room.teamMain !== nextProps.room.teamMain) {
		return false;
	}

	return true;
};

export default memo(SideBarItemTemplateWithData, propsAreEqual);


<|start_filename|>app/models/server/raw/LivechatRooms.js<|end_filename|>
import { BaseRaw } from './BaseRaw';
import { getValue } from '../../../settings/server/raw';

export class LivechatRoomsRaw extends BaseRaw {
	getQueueMetrics({ departmentId, agentId, includeOfflineAgents, options = {} }) {
		const match = { $match: { t: 'l', open: true, servedBy: { $exists: true } } };
		const matchUsers = { $match: {} };
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		if (agentId) {
			matchUsers.$match['user._id'] = agentId;
		}
		if (!includeOfflineAgents) {
			matchUsers.$match['user.status'] = { $ne: 'offline' };
			matchUsers.$match['user.statusLivechat'] = { $eq: 'available' };
		}
		const departmentsLookup = {
			$lookup: {
				from: 'rocketchat_livechat_department',
				localField: 'departmentId',
				foreignField: '_id',
				as: 'departments',
			},
		};
		const departmentsUnwind = {
			$unwind: {
				path: '$departments',
				preserveNullAndEmptyArrays: true,
			},
		};
		const departmentsGroup = {
			$group: {
				_id: {
					departmentId: '$departmentId',
					name: '$departments.name',
					room: '$$ROOT',
				},
			},
		};
		const usersLookup = {
			$lookup: {
				from: 'users',
				localField: '_id.room.servedBy._id',
				foreignField: '_id',
				as: 'user',
			},
		};
		const usersUnwind = {
			$unwind: {
				path: '$user',
				preserveNullAndEmptyArrays: true,
			},
		};
		const usersGroup = {
			$group: {
				_id: {
					userId: '$user._id',
					username: '$user.username',
					status: '$user.status',
					departmentId: '$_id.departmentId',
					departmentName: '$_id.name',
				},
				chats: { $sum: 1 },
			},
		};
		const project = {
			$project: {
				_id: 0,
				user: {
					_id: '$_id.userId',
					username: '$_id.username',
					status: '$_id.status',
				},
				department: {
					_id: { $ifNull: ['$_id.departmentId', null] },
					name: { $ifNull: ['$_id.departmentName', null] },
				},
				chats: 1,
			},
		};
		const firstParams = [match, departmentsLookup, departmentsUnwind, departmentsGroup, usersLookup, usersUnwind];
		if (Object.keys(matchUsers.$match)) {
			firstParams.push(matchUsers);
		}
		const sort = { $sort: options.sort || { chats: -1 } };
		const params = [...firstParams, usersGroup, project, sort];
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params).toArray();
	}

	async findAllNumberOfAbandonedRooms({ start, end, departmentId, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				'metrics.visitorInactivity': { $gte: await getValue('Livechat_visitor_inactivity_timeout') },
				ts: { $gte: new Date(start) },
				closedAt: { $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departmentId',
				},
				abandonedRooms: { $sum: 1 },
			},
		};
		const project = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				abandonedRooms: 1,
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	async findPercentageOfAbandonedRooms({ start, end, departmentId, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departmentId',
				},
				rooms: { $sum: 1 },
				abandonedChats: {
					$sum: {
						$cond: [{
							$and: [
								{ $ifNull: ['$metrics.visitorInactivity', false] },
								{ $gte: ['$metrics.visitorInactivity', await getValue('Livechat_visitor_inactivity_timeout')] },
							],
						}, 1, 0],
					},
				},
			},
		};
		const project = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				percentageOfAbandonedChats: {
					$floor: {
						$cond: [
							{ $eq: ['$rooms', 0] },
							0,
							{ $divide: [{ $multiply: ['$abandonedChats', 100] }, '$rooms'] },
						],
					},
				},
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	findAllAverageOfChatDurationTime({ start, end, departmentId, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start) },
				closedAt: { $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departmentId',
				},
				rooms: { $sum: 1 },
				chatsDuration: { $sum: '$metrics.chatDuration' },
			},
		};
		const project = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				averageChatDurationTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$chatsDuration', '$rooms'] }] } },
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	findAllAverageWaitingTime({ start, end, departmentId, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
				waitingResponse: { $ne: true },
			},
		};
		const group = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departmentId',
				},
				rooms: { $sum: 1 },
				chatsFirstResponses: { $sum: '$metrics.response.ft' },
			},
		};
		const project = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				averageWaitingTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$chatsFirstResponses', '$rooms'] }] } },
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	findAllRooms({ start, end, answered, departmentId, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		if (answered !== undefined) {
			match.$match.waitingResponse = { [answered ? '$ne' : '$eq']: true };
		}
		const group = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departmentId',
				},
				rooms: { $sum: 1 },
			},
		};
		const project = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				rooms: 1,
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	findAllServiceTime({ start, end, departmentId, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start) },
				closedAt: { $lte: new Date(end) },
				'metrics.serviceTimeDuration': { $exists: true },
			},
		};
		const group = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departmentId',
				},
				rooms: { $sum: 1 },
				serviceTimeDuration: { $sum: '$metrics.serviceTimeDuration' },
			},
		};
		const project = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				chats: '$rooms',
				serviceTimeDuration: { $ceil: '$serviceTimeDuration' },
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	findAllNumberOfTransferredRooms({ start, end, departmentId, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const departmentsLookup = {
			$lookup: {
				from: 'rocketchat_livechat_department',
				localField: 'departmentId',
				foreignField: '_id',
				as: 'departments',
			},
		};
		const departmentsUnwind = {
			$unwind: {
				path: '$departments',
				preserveNullAndEmptyArrays: true,
			},
		};
		const departmentsGroup = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departments._id',
					name: '$departments.name',
				},
				rooms: { $push: '$$ROOT' },
			},
		};
		const departmentsProject = {
			$project: {
				_id: '$_id.departmentId',
				name: '$_id.name',
				rooms: 1,
			},
		};
		const roomsUnwind = {
			$unwind: {
				path: '$rooms',
				preserveNullAndEmptyArrays: true,
			},
		};
		const messagesLookup = {
			$lookup: {
				from: 'rocketchat_message',
				localField: 'rooms._id',
				foreignField: 'rid',
				as: 'messages',
			},
		};
		const messagesProject = {
			$project: {
				_id: 1,
				name: 1,
				messages: {
					$filter: {
						input: '$messages',
						as: 'message',
						cond: {
							$and: [{ $eq: ['$$message.t', 'livechat_transfer_history'] }],
						},
					},
				},
			},
		};
		const transferProject = {
			$project: {
				name: 1,
				transfers: { $size: { $ifNull: ['$messages', []] } },
			},
		};
		const transferGroup = {
			$group: {
				_id: {
					departmentId: '$_id',
					name: '$name',
				},
				numberOfTransferredRooms: { $sum: '$transfers' },
			},
		};
		const presentationProject = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				name: { $ifNull: ['$_id.name', null] },
				numberOfTransferredRooms: 1,
			},
		};
		const firstParams = [match, departmentsLookup, departmentsUnwind];
		if (departmentId && departmentId !== 'undefined') {
			firstParams.push({
				$match: {
					'departments._id': departmentId,
				},
			});
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [...firstParams, departmentsGroup, departmentsProject, roomsUnwind, messagesLookup, messagesProject, transferProject, transferGroup, presentationProject, sort];
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params, { allowDiskUse: true }).toArray();
	}

	countAllOpenChatsBetweenDate({ start, end, departmentId }) {
		const query = {
			t: 'l',
			'metrics.chatDuration': {
				$exists: false,
			},
			$or: [{
				onHold: {
					$exists: false,
				},
			}, {
				onHold: {
					$exists: true,
					$eq: false,
				},
			}],
			servedBy: { $exists: true },
			ts: { $gte: new Date(start), $lte: new Date(end) },
		};
		if (departmentId && departmentId !== 'undefined') {
			query.departmentId = departmentId;
		}
		return this.find(query).count();
	}

	countAllClosedChatsBetweenDate({ start, end, departmentId }) {
		const query = {
			t: 'l',
			'metrics.chatDuration': {
				$exists: true,
			},
			ts: { $gte: new Date(start), $lte: new Date(end) },
		};
		if (departmentId && departmentId !== 'undefined') {
			query.departmentId = departmentId;
		}
		return this.find(query).count();
	}

	countAllQueuedChatsBetweenDate({ start, end, departmentId }) {
		const query = {
			t: 'l',
			servedBy: { $exists: false },
			open: true,
			ts: { $gte: new Date(start), $lte: new Date(end) },
		};
		if (departmentId && departmentId !== 'undefined') {
			query.departmentId = departmentId;
		}
		return this.find(query).count();
	}

	countAllOpenChatsByAgentBetweenDate({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				'servedBy.username': { $exists: true },
				open: true,
				$or: [{
					onHold: {
						$exists: false,
					},
				}, {
					onHold: {
						$exists: true,
						$eq: false,
					},
				}],
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: '$servedBy.username',
				chats: { $sum: 1 },
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		return this.col.aggregate([match, group]).toArray();
	}

	countAllOnHoldChatsByAgentBetweenDate({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				'servedBy.username': { $exists: true },
				open: true,
				onHold: {
					$exists: true,
					$eq: true,
				},
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: '$servedBy.username',
				chats: { $sum: 1 },
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		return this.col.aggregate([match, group]).toArray();
	}

	countAllClosedChatsByAgentBetweenDate({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				open: { $exists: false },
				'servedBy.username': { $exists: true },
				ts: { $gte: new Date(start) },
				closedAt: { $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: '$servedBy.username',
				chats: { $sum: 1 },
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		return this.col.aggregate([match, group]).toArray();
	}

	countAllOpenChatsByDepartmentBetweenDate({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				open: true,
				departmentId: { $exists: true },
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const lookup = {
			$lookup: {
				from: 'rocketchat_livechat_department',
				localField: 'departmentId',
				foreignField: '_id',
				as: 'departments',
			},
		};
		const unwind = {
			$unwind: {
				path: '$departments',
				preserveNullAndEmptyArrays: true,
			},
		};
		const group = {
			$group: {
				_id: {
					_id: '$departments._id',
					name: '$departments.name',
				},
				chats: { $sum: 1 },
			},
		};
		const project = {
			$project: {
				_id: '$_id._id',
				name: '$_id.name',
				chats: 1,
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const params = [match, lookup, unwind, group, project];
		return this.col.aggregate(params).toArray();
	}

	countAllClosedChatsByDepartmentBetweenDate({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				open: { $exists: false },
				departmentId: { $exists: true },
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const lookup = {
			$lookup: {
				from: 'rocketchat_livechat_department',
				localField: 'departmentId',
				foreignField: '_id',
				as: 'departments',
			},
		};
		const unwind = {
			$unwind: {
				path: '$departments',
				preserveNullAndEmptyArrays: true,
			},
		};
		const group = {
			$group: {
				_id: {
					_id: '$departments._id',
					name: '$departments.name',
				},
				chats: { $sum: 1 },
			},
		};
		const project = {
			$project: {
				_id: '$_id._id',
				name: '$_id.name',
				chats: 1,
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const params = [match, lookup, unwind, group, project];
		return this.col.aggregate(params).toArray();
	}

	calculateResponseTimingsBetweenDates({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: null,
				sumResponseAvg: {
					$sum: '$metrics.response.avg',
				},
				roomsWithResponseTime: {
					$sum: {
						$cond: [{
							$and: [
								{ $ifNull: ['$metrics.response.avg', false] },
							],
						}, 1, 0],
					},
				},
				maxFirstResponse: { $max: '$metrics.response.ft' },
			},
		};
		const project = {
			$project: {
				avg: {
					$trunc: {
						$cond: [
							{ $eq: ['$roomsWithResponseTime', 0] },
							0,
							{ $divide: ['$sumResponseAvg', '$roomsWithResponseTime'] },
						],
					},
				},
				longest: '$maxFirstResponse',
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		return this.col.aggregate([match, group, project]).toArray();
	}

	calculateReactionTimingsBetweenDates({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
			},
		};
		const group = {
			$group: {
				_id: null,
				sumReactionFirstResponse: {
					$sum: '$metrics.reaction.ft',
				},
				roomsWithFirstReaction: {
					$sum: {
						$cond: [{
							$and: [
								{ $ifNull: ['$metrics.reaction.ft', false] },
							],
						}, 1, 0],
					},
				},
				maxFirstReaction: { $max: '$metrics.reaction.ft' },
			},
		};
		const project = {
			$project: {
				avg: {
					$trunc: {
						$cond: [
							{ $eq: ['$roomsWithFirstReaction', 0] },
							0,
							{ $divide: ['$sumReactionFirstResponse', '$roomsWithFirstReaction'] },
						],
					},
				},
				longest: '$maxFirstReaction',
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		return this.col.aggregate([match, group, project]).toArray();
	}

	calculateDurationTimingsBetweenDates({ start, end, departmentId }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
				'metrics.chatDuration': { $exists: true },
			},
		};
		const group = {
			$group: {
				_id: null,
				sumChatDuration: {
					$sum: '$metrics.chatDuration',
				},
				roomsWithChatDuration: {
					$sum: {
						$cond: [{
							$and: [
								{ $ifNull: ['$metrics.chatDuration', false] },
							],
						}, 1, 0],
					},
				},
				maxChatDuration: { $max: '$metrics.chatDuration' },
			},
		};
		const project = {
			$project: {
				avg: {
					$trunc: {
						$cond: [
							{ $eq: ['$roomsWithChatDuration', 0] },
							0,
							{ $divide: ['$sumChatDuration', '$roomsWithChatDuration'] },
						],
					},
				},
				longest: '$maxChatDuration',
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		return this.col.aggregate([match, group, project]).toArray();
	}

	findAllAverageOfServiceTime({ start, end, departmentId, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				ts: { $gte: new Date(start), $lte: new Date(end) },
				'responseBy.lastMessageTs': { $exists: true },
				'servedBy.ts': { $exists: true },
			},
		};
		const group = {
			$group: {
				_id: {
					_id: null,
					departmentId: '$departmentId',
				},
				rooms: { $sum: 1 },
				allServiceTime: { $sum: { $divide: [{ $subtract: ['$responseBy.lastMessageTs', '$servedBy.ts'] }, 1000] } },
			},
		};
		const project = {
			$project: {
				_id: { $ifNull: ['$_id.departmentId', null] },
				averageServiceTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$allServiceTime', '$rooms'] }] } },
			},
		};
		if (departmentId && departmentId !== 'undefined') {
			match.$match.departmentId = departmentId;
		}
		const sort = { $sort: options.sort || { name: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	findByVisitorId(visitorId, options) {
		const query = {
			t: 'l',
			'v._id': visitorId,
		};
		return this.find(query, options);
	}

	findRoomsByVisitorIdAndMessageWithCriteria({ visitorId, searchText, open, served, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				'v._id': visitorId,
				...open !== undefined && { open: { $exists: open } },
				...served !== undefined && { servedBy: { $exists: served } },
			},
		};
		const lookup = { $lookup: { from: 'rocketchat_message', localField: '_id', foreignField: 'rid', as: 'messages' } };
		const matchMessages = searchText && { $match: { 'messages.msg': { $regex: `.*${ searchText }.*` } } };

		const params = [match, lookup];

		if (matchMessages) {
			params.push(matchMessages);
		}

		const project = {
			$project: {
				fname: 1,
				ts: 1,
				v: 1,
				msgs: 1,
				servedBy: 1,
				closedAt: 1,
				closedBy: 1,
				closer: 1,
				tags: 1,
				closingMessage: {
					$filter: {
						input: '$messages',
						as: 'messages',
						cond: { $eq: ['$$messages.t', 'livechat-close'] },
					},
				},
			},
		};

		const unwindClosingMsg = { $unwind: { path: '$closingMessage', preserveNullAndEmptyArrays: true } };
		const sort = { $sort: options.sort || { ts: -1 } };

		params.push(project, unwindClosingMsg, sort);

		if (onlyCount) {
			params.push({ $count: 'count' });
			return this.col.aggregate(params);
		}

		if (options.skip) {
			params.push({ $skip: options.skip });
		}

		if (options.limit) {
			params.push({ $limit: options.limit });
		}

		return this.col.aggregate(params);
	}

	findRoomsWithCriteria({ agents, roomName, departmentId, open, served, createdAt, closedAt, tags, customFields, visitorId, roomIds, onhold, options = {} }) {
		const query = {
			t: 'l',
		};
		if (agents) {
			query.$or = [{ 'servedBy._id': { $in: agents } }, { 'servedBy.username': { $in: agents } }];
		}
		if (roomName) {
			query.fname = new RegExp(roomName, 'i');
		}
		if (departmentId && departmentId !== 'undefined') {
			query.departmentId = departmentId;
		}
		if (open !== undefined) {
			query.open = { $exists: open };
			query.onHold = { $ne: true };
		}
		if (served !== undefined) {
			query.servedBy = { $exists: served };
		}
		if (visitorId && visitorId !== 'undefined') {
			query['v._id'] = visitorId;
		}
		if (createdAt) {
			query.ts = {};
			if (createdAt.start) {
				query.ts.$gte = new Date(createdAt.start);
			}
			if (createdAt.end) {
				query.ts.$lte = new Date(createdAt.end);
			}
		}
		if (closedAt) {
			query.closedAt = {};
			if (closedAt.start) {
				query.closedAt.$gte = new Date(closedAt.start);
			}
			if (closedAt.end) {
				query.closedAt.$lte = new Date(closedAt.end);
			}
		}
		if (tags) {
			query.tags = { $in: tags };
		}
		if (customFields) {
			query.$and = Object.keys(customFields).map((key) => ({ [`livechatData.${ key }`]: new RegExp(customFields[key], 'i') }));
		}

		if (roomIds) {
			query._id = { $in: roomIds };
		}

		if (onhold) {
			query.onHold = {
				$exists: true,
				$eq: onhold,
			};
		}

		return this.find(query, { sort: options.sort || { name: 1 }, skip: options.offset, limit: options.count });
	}

	getOnHoldConversationsBetweenDate(from, to, departmentId) {
		const query = {
			onHold: {
				$exists: true,
				$eq: true,
			},
			ts: {
				$gte: new Date(from),	// ISO Date, ts >= date.gte
				$lt: new Date(to),	// ISODate, ts < date.lt
			},
		};

		if (departmentId && departmentId !== 'undefined') {
			query.departmentId = departmentId;
		}

		return this.find(query).count();
	}

	findAllServiceTimeByAgent({ start, end, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				'servedBy._id': { $exists: true },
				'metrics.serviceTimeDuration': { $exists: true },
				ts: {
					$gte: start,
					$lte: end,
				},
			},
		};
		const group = {
			$group: {
				_id: { _id: '$servedBy._id', username: '$servedBy.username' },
				chats: { $sum: 1 },
				serviceTimeDuration: { $sum: '$metrics.serviceTimeDuration' },
			},
		};
		const project = {
			$project: {
				_id: '$_id._id',
				username: '$_id.username',
				chats: 1,
				serviceTimeDuration: { $ceil: '$serviceTimeDuration' },
			},
		};
		const sort = { $sort: options.sort || { username: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	findAllAverageServiceTimeByAgents({ start, end, onlyCount = false, options = {} }) {
		const match = {
			$match: {
				t: 'l',
				'servedBy._id': { $exists: true },
				'metrics.serviceTimeDuration': { $exists: true },
				ts: {
					$gte: start,
					$lte: end,
				},
			},
		};
		const group = {
			$group: {
				_id: { _id: '$servedBy._id', username: '$servedBy.username' },
				chats: { $sum: 1 },
				serviceTimeDuration: { $sum: '$metrics.serviceTimeDuration' },
			},
		};
		const project = {
			$project: {
				_id: '$_id._id',
				username: '$_id.username',
				name: '$_id.name',
				active: '$_id.active',
				averageServiceTimeInSeconds: {
					$ceil: {
						$cond: [
							{ $eq: ['$chats', 0] },
							0,
							{ $divide: ['$serviceTimeDuration', '$chats'] },
						],
					},
				},
			},
		};
		const sort = { $sort: options.sort || { username: 1 } };
		const params = [match, group, project, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}
		return this.col.aggregate(params);
	}

	setDepartmentByRoomId(roomId, departmentId) {
		return this.update({ _id: roomId }, { $set: { departmentId } });
	}
}


<|start_filename|>client/views/room/contextualBar/Call/Jitsi/CallJitsiWithData.js<|end_filename|>
import { Skeleton } from '@rocket.chat/fuselage';
import { useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks';
import { clear } from '@rocket.chat/memo';
import React, { useRef, useEffect, useState, useMemo, useLayoutEffect, memo } from 'react';

import { HEARTBEAT, TIMEOUT, DEBOUNCE } from '../../../../../../app/videobridge/constants';
import { useConnectionStatus } from '../../../../../contexts/ConnectionStatusContext';
import { useSetModal } from '../../../../../contexts/ModalContext';
import { useMethod } from '../../../../../contexts/ServerContext';
import { useSettings } from '../../../../../contexts/SettingsContext';
import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import { useUser } from '../../../../../contexts/UserContext';
import { useRoom } from '../../../contexts/RoomContext';
import { useTabBarClose } from '../../../providers/ToolboxProvider';
import CallJitsi from './CallJitsi';
import CallModal from './components/CallModal';
import { JitsiBridge } from './lib/JitsiBridge';

export { default as CallJitsi } from './CallJitsi';

const querySettings = {
	_id: [
		'Jitsi_Open_New_Window',
		'Jitsi_Domain',
		'Jitsi_URL_Room_Hash',
		'uniqueID',
		'Jitsi_URL_Room_Prefix',
		'Jitsi_URL_Room_Suffix',
		'Jitsi_Chrome_Extension',
		'Jitsi_SSL',
		'Jitsi_Enabled_TokenAuth',
	],
};

const CallJitsiWithData = ({ rid }) => {
	const user = useUser();
	const { connected } = useConnectionStatus();
	const [accessToken, setAccessToken] = useSafely(useState());
	const [accepted, setAccepted] = useState(false);
	const room = useRoom();
	const setModal = useSetModal();
	const handleClose = useTabBarClose();
	const closeModal = useMutableCallback(() => setModal(null));
	const generateAccessToken = useMethod('jitsi:generateAccessToken');
	const updateTimeout = useMethod('jitsi:updateTimeout');
	const dispatchToastMessage = useToastMessageDispatch();
	const t = useTranslation();

	const handleCancel = useMutableCallback(() => {
		closeModal();
		handleClose();
	});

	const ref = useRef();

	const {
		Jitsi_Open_New_Window: openNewWindow,
		Jitsi_Domain: domain,
		Jitsi_SSL: ssl,
		Jitsi_Chrome_Extension: desktopSharingChromeExtId,
		Jitsi_URL_Room_Hash: useHashName,
		uniqueID,
		Jitsi_URL_Room_Prefix: prefix,
		Jitsi_URL_Room_Suffix: sufix,
		Jitsi_Enabled_TokenAuth: isEnabledTokenAuth,
	} = Object.fromEntries(useSettings(querySettings).map(({ _id, value }) => [_id, value]));

	useEffect(() => {
		let ignore = false;
		if (!isEnabledTokenAuth) {
			setAccessToken();
			return;
		}
		(async () => {
			const accessToken = await generateAccessToken(rid);
			!ignore && setAccessToken(accessToken);
		})();
		return () => {
			ignore = true;
		};
	}, [generateAccessToken, isEnabledTokenAuth, rid, setAccessToken]);

	useLayoutEffect(() => {
		if (!connected) {
			handleClose();
		}
	}, [connected, handleClose]);

	const rname = useHashName
		? uniqueID + rid
		: encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name);

	const jitsi = useMemo(() => {
		if (isEnabledTokenAuth && !accessToken) {
			return;
		}

		const jitsiRoomName = prefix + rname + sufix;

		return new JitsiBridge(
			{
				openNewWindow,
				ssl,
				domain,
				jitsiRoomName,
				accessToken,
				desktopSharingChromeExtId,
				name: user.name || user.username,
			},
			HEARTBEAT,
		);
	}, [
		accessToken,
		desktopSharingChromeExtId,
		domain,
		isEnabledTokenAuth,
		openNewWindow,
		prefix,
		rname,
		ssl,
		sufix,
		user.name,
		user.username,
	]);

	const testAndHandleTimeout = useMutableCallback(() => {
		if (jitsi.openNewWindow) {
			if (jitsi.window?.closed) {
				return jitsi.dispose();
			}
			try {
				return updateTimeout(rid, false);
			} catch (error) {
				dispatchToastMessage({ type: 'error', message: t(error.reason) });
				clear();
				handleClose();
				return jitsi.dispose();
			}
		}
		if (new Date() - new Date(room.jitsiTimeout) > TIMEOUT) {
			return jitsi.dispose();
		}

		if (new Date() - new Date(room.jitsiTimeout) + TIMEOUT > DEBOUNCE) {
			try {
				return updateTimeout(rid, false);
			} catch (error) {
				dispatchToastMessage({ type: 'error', message: t(error.reason) });
				clear();
				handleClose();
				return jitsi.dispose();
			}
		}
	});

	useEffect(() => {
		if (!accepted || !jitsi) {
			return;
		}

		const clear = () => {
			jitsi.off('HEARTBEAT', testAndHandleTimeout);
			jitsi.dispose();
		};

		try {
			if (jitsi.needsStart) {
				jitsi.start(ref.current);
				updateTimeout(rid, true);
			} else {
				updateTimeout(rid, false);
			}
		} catch (error) {
			dispatchToastMessage({ type: 'error', message: t(error.reason) });
			clear();
			handleClose();
		}
		jitsi.on('HEARTBEAT', testAndHandleTimeout);

		return () => {
			if (!jitsi.openNewWindow) clear();
		};
	}, [
		accepted,
		jitsi,
		rid,
		testAndHandleTimeout,
		updateTimeout,
		dispatchToastMessage,
		handleClose,
		t,
	]);

	const handleYes = useMutableCallback(() => {
		if (jitsi) {
			jitsi.needsStart = true;
		}

		setAccepted(true);

		if (openNewWindow) {
			handleClose();
		}
	});

	useLayoutEffect(() => {
		if (!accepted) {
			setModal(() => <CallModal handleYes={handleYes} handleCancel={handleCancel} />);
			return;
		}
		closeModal();
	}, [accepted, closeModal, handleCancel, handleYes, setModal]);

	return (
		<CallJitsi handleClose={handleClose} openNewWindow={openNewWindow} refContent={ref}>
			{!accepted && <Skeleton />}
		</CallJitsi>
	);
};

export default memo(CallJitsiWithData);


<|start_filename|>client/components/Omnichannel/modals/CloseChatModal.js<|end_filename|>
import { Field, Button, TextInput, Icon, ButtonGroup, Modal, Box } from '@rocket.chat/fuselage';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, useMemo, useEffect } from 'react';

import { useSetting } from '../../../contexts/SettingsContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate';
import { useForm } from '../../../hooks/useForm';
import GenericModal from '../../GenericModal';
import Tags from '../Tags';

const CloseChatModal = ({ department = {}, onCancel, onConfirm }) => {
	const t = useTranslation();

	const inputRef = useAutoFocus(true);

	const { values, handlers } = useForm({ comment: '', tags: [] });

	const commentRequired = useSetting('Livechat_request_comment_when_closing_conversation');

	const { comment, tags } = values;
	const { handleComment, handleTags } = handlers;
	const [commentError, setCommentError] = useState('');
	const [tagError, setTagError] = useState('');
	const [tagRequired, setTagRequired] = useState(false);

	const handleConfirm = useCallback(() => {
		onConfirm(comment, tags);
	}, [comment, onConfirm, tags]);

	useComponentDidUpdate(() => {
		setCommentError(!comment && commentRequired ? t('The_field_is_required', t('Comment')) : '');
	}, [commentRequired, comment, t]);

	const canConfirm = useMemo(() => {
		const canConfirmTag = !tagError && (tagRequired ? tags.length > 0 : true);
		const canConfirmComment = !commentError && (commentRequired ? !!comment : true);
		return canConfirmTag && canConfirmComment;
	}, [comment, commentError, commentRequired, tagError, tagRequired, tags.length]);

	useEffect(() => {
		department?.requestTagBeforeClosingChat && setTagRequired(true);
		setTagError(
			tagRequired && (!tags || tags.length === 0)
				? t('error-tags-must-be-assigned-before-closing-chat')
				: '',
		);
	}, [department, tagRequired, t, tags]);

	if (!commentRequired && !tagRequired) {
		return (
			<GenericModal
				variant='warning'
				title={t('Are_you_sure_you_want_to_close_this_chat')}
				onConfirm={handleConfirm}
				onCancel={onCancel}
				onClose={onCancel}
				confirmText={t('Confirm')}
			></GenericModal>
		);
	}

	return (
		<Modal>
			<Modal.Header>
				<Icon name='baloon-close-top-right' size={20} />
				<Modal.Title>{t('Closing_chat')}</Modal.Title>
				<Modal.Close onClick={onCancel} />
			</Modal.Header>
			<Modal.Content fontScale='p2'>
				<Box color='neutral-600'>{t('Close_room_description')}</Box>
				<Field marginBlock='x15'>
					<Field.Label required={commentRequired}>{t('Comment')}</Field.Label>
					<Field.Row>
						<TextInput
							ref={inputRef}
							error={commentError}
							flexGrow={1}
							value={comment}
							onChange={handleComment}
							placeholder={t('Please_add_a_comment')}
						/>
					</Field.Row>
					<Field.Error>{commentError}</Field.Error>
				</Field>
				{Tags && (
					<Field>
						<Tags tagRequired={tagRequired} tags={tags} handler={handleTags} error={tagError} />
						<Field.Error>{tagError}</Field.Error>
					</Field>
				)}
			</Modal.Content>
			<Modal.Footer>
				<ButtonGroup align='end'>
					<Button onClick={onCancel}>{t('Cancel')}</Button>
					<Button disabled={!canConfirm} primary onClick={handleConfirm}>
						{t('Confirm')}
					</Button>
				</ButtonGroup>
			</Modal.Footer>
		</Modal>
	);
};

export default CloseChatModal;


<|start_filename|>client/views/notFound/NotFoundPage.js<|end_filename|>
import { Box, Button, ButtonGroup, Flex, Margins } from '@rocket.chat/fuselage';
import React from 'react';

import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';

function NotFoundPage() {
	const t = useTranslation();
	const homeRoute = useRoute('home');

	const handleGoToPreviousPageClick = () => {
		window.history.back();
	};

	const handleGoHomeClick = () => {
		homeRoute.push();
	};

	return (
		<Flex.Container direction='column' justifyContent='center' alignItems='center'>
			<Box
				is='section'
				width='full'
				minHeight='sh'
				textAlign='center'
				backgroundColor='neutral-800'
				style={{
					backgroundImage: "url('/images/404.svg')",
					backgroundRepeat: 'no-repeat',
					backgroundPosition: 'center',
					backgroundSize: 'cover',
				}}
			>
				<Flex.Item>
					<Box>
						<Margins all='x12'>
							<Box fontWeight='p2m' fontSize='x64' color='alternative'>
								404
							</Box>

							<Box role='heading' aria-level='1' fontScale='h2' color='alternative'>
								{t('Oops_page_not_found')}
							</Box>

							<Box
								role='status'
								aria-label='Sorry_page_you_requested_does_not_exist_or_was_deleted'
								fontScale='p2'
								color='alternative'
							>
								{t('Sorry_page_you_requested_does_not_exist_or_was_deleted')}
							</Box>
						</Margins>

						<ButtonGroup align='center' margin='x64'>
							<Button type='button' primary onClick={handleGoToPreviousPageClick}>
								{t('Return_to_previous_page')}
							</Button>
							<Button type='button' primary onClick={handleGoHomeClick}>
								{t('Return_to_home')}
							</Button>
						</ButtonGroup>
					</Box>
				</Flex.Item>
			</Box>
		</Flex.Container>
	);
}

export default NotFoundPage;


<|start_filename|>client/.eslintrc.js<|end_filename|>
module.exports = {
	root: true,
	extends: ['@rocket.chat/eslint-config', 'prettier'],
	parser: 'babel-eslint',
	plugins: ['react', 'react-hooks', 'prettier', 'testing-library', 'anti-trojan-source'],
	rules: {
		'import/named': 'error',
		'import/order': [
			'error',
			{
				'newlines-between': 'always',
				'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
				'alphabetize': {
					order: 'asc',
				},
			},
		],
		'jsx-quotes': ['error', 'prefer-single'],
		'new-cap': [
			'error',
			{ capIsNewExceptions: ['HTML.Comment', 'HTML.Raw', 'HTML.DIV', 'SHA256'] },
		],
		'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
		'prettier/prettier': 2,
		'react/display-name': 'error',
		'react/jsx-uses-react': 'error',
		'react/jsx-uses-vars': 'error',
		'react/jsx-no-undef': 'error',
		'react/jsx-fragments': ['error', 'syntax'],
		'react/no-multi-comp': 'error',
		'react-hooks/rules-of-hooks': 'error',
		'react-hooks/exhaustive-deps': [
			'warn',
			{
				additionalHooks: '(useComponentDidUpdate)',
			},
		],
		'anti-trojan-source/no-bidi': 'error',
	},
	settings: {
		'import/resolver': {
			node: {
				extensions: ['.js', '.ts', '.tsx'],
			},
		},
		'react': {
			version: 'detect',
		},
	},
	env: {
		browser: true,
		es6: true,
	},
	overrides: [
		{
			files: ['**/*.ts', '**/*.tsx'],
			extends: [
				'plugin:@typescript-eslint/recommended',
				'plugin:@typescript-eslint/eslint-recommended',
				'@rocket.chat/eslint-config',
				'prettier',
			],
			parser: '@typescript-eslint/parser',
			plugins: ['@typescript-eslint', 'react', 'react-hooks', 'prettier'],
			rules: {
				'@typescript-eslint/ban-ts-ignore': 'off',
				'@typescript-eslint/explicit-function-return-type': 'warn',
				// '@typescript-eslint/explicit-module-boundary-types': 'off',
				'@typescript-eslint/indent': 'off',
				'@typescript-eslint/interface-name-prefix': ['error', 'always'],
				'@typescript-eslint/no-extra-parens': 'off',
				'@typescript-eslint/no-explicit-any': 'off',
				'@typescript-eslint/no-unused-vars': [
					'error',
					{
						argsIgnorePattern: '^_',
					},
				],
				'@typescript-eslint/prefer-optional-chain': 'warn',
				'func-call-spacing': 'off',
				'indent': 'off',
				'import/order': [
					'error',
					{
						'newlines-between': 'always',
						'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
						'alphabetize': {
							order: 'asc',
						},
					},
				],
				'jsx-quotes': ['error', 'prefer-single'],
				'new-cap': [
					'error',
					{ capIsNewExceptions: ['HTML.Comment', 'HTML.Raw', 'HTML.DIV', 'SHA256'] },
				],
				'no-extra-parens': 'off',
				'no-spaced-func': 'off',
				'no-unused-vars': 'off',
				'no-useless-constructor': 'off',
				'no-use-before-define': 'off',
				'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
				'prettier/prettier': 2,
				'react/display-name': 'error',
				'react/jsx-uses-react': 'error',
				'react/jsx-uses-vars': 'error',
				'react/jsx-no-undef': 'error',
				'react/jsx-fragments': ['error', 'syntax'],
				'react/no-multi-comp': 'error',
				'react-hooks/rules-of-hooks': 'error',
				'react-hooks/exhaustive-deps': [
					'warn',
					{
						additionalHooks: '(useComponentDidUpdate)',
					},
				],
				'anti-trojan-source/no-bidi': 'error',
			},
			env: {
				browser: true,
				es6: true,
			},
			settings: {
				'import/resolver': {
					node: {
						extensions: ['.js', '.ts', '.tsx'],
					},
				},
				'react': {
					version: 'detect',
				},
			},
		},
		{
			files: ['**/*.stories.js', '**/*.stories.jsx', '**/*.stories.ts', '**/*.stories.tsx'],
			rules: {
				'react/display-name': 'off',
				'react/no-multi-comp': 'off',
			},
		},
		{
			files: ['**/*.tests.js', '**/*.tests.ts', '**/*.spec.ts', '**/*.spec.tsx'],
			extends: ['plugin:testing-library/react'],
			rules: {
				'testing-library/no-await-sync-events': 'warn',
				'testing-library/no-manual-cleanup': 'warn',
				'testing-library/prefer-explicit-assert': 'warn',
				'testing-library/prefer-user-event': 'warn',
			},
			env: {
				mocha: true,
			},
		},
		{
			files: ['**/*.stories.ts', '**/*.stories.tsx'],
			rules: {
				'@typescript-eslint/explicit-function-return-type': 'off',
				'@typescript-eslint/explicit-module-boundary-types': 'off',
			},
		},
	],
};


<|start_filename|>app/ui/client/lib/chatMessages.js<|end_filename|>
import moment from 'moment';
import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { escapeHTML } from '@rocket.chat/string-helpers';

import { KonchatNotification } from './notification';
import { UserAction, USER_ACTIVITIES } from '../index';
import { fileUpload } from './fileUpload';
import { t, slashCommands } from '../../../utils/client';
import {
	messageProperties,
	MessageTypes,
	readMessage,
} from '../../../ui-utils/client';
import { settings } from '../../../settings/client';
import { callbacks } from '../../../callbacks/client';
import { hasAtLeastOnePermission } from '../../../authorization/client';
import { Messages, Rooms, ChatMessage, ChatSubscription } from '../../../models/client';
import { emoji } from '../../../emoji/client';
import { generateTriggerId } from '../../../ui-message/client/ActionManager';
import { imperativeModal } from '../../../../client/lib/imperativeModal';
import GenericModal from '../../../../client/components/GenericModal';
import { keyCodes } from '../../../../client/lib/utils/keyCodes';
import { prependReplies } from '../../../../client/lib/utils/prependReplies';
import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling';
import { handleError } from '../../../../client/lib/utils/handleError';
import { dispatchToastMessage } from '../../../../client/lib/toast';
import { onClientBeforeSendMessage } from '../../../../client/lib/onClientBeforeSendMessage';


const messageBoxState = {
	saveValue: _.debounce(({ rid, tmid }, value) => {
		const key = ['messagebox', rid, tmid].filter(Boolean).join('_');
		value ? Meteor._localStorage.setItem(key, value) : Meteor._localStorage.removeItem(key);
	}, 1000),

	restoreValue: ({ rid, tmid }) => {
		const key = ['messagebox', rid, tmid].filter(Boolean).join('_');
		return Meteor._localStorage.getItem(key);
	},

	restore: ({ rid, tmid }, input) => {
		const value = messageBoxState.restoreValue({ rid, tmid });
		if (typeof value === 'string') {
			messageBoxState.set(input, value);
		}
	},

	save: ({ rid, tmid }, input) => {
		messageBoxState.saveValue({ rid, tmid }, input.value);
	},

	set: (input, value) => {
		input.value = value;
		$(input).trigger('change').trigger('input');
	},

	purgeAll: () => {
		Object.keys(Meteor._localStorage)
			.filter((key) => key.indexOf('messagebox_') === 0)
			.forEach((key) => Meteor._localStorage.removeItem(key));
	},
};

callbacks.add('afterLogoutCleanUp', messageBoxState.purgeAll, callbacks.priority.MEDIUM, 'chatMessages-after-logout-cleanup');

export class ChatMessages {
	constructor(collection = ChatMessage) {
		this.collection = collection;
	}

	editing = {}

	records = {}

	initializeWrapper(wrapper) {
		this.wrapper = wrapper;
	}

	initializeInput(input, { rid, tmid }) {
		this.input = input;
		this.$input = $(this.input);

		if (!input || !rid) {
			return;
		}

		messageBoxState.restore({ rid, tmid }, input);
		this.restoreReplies();
		this.requestInputFocus();
	}

	async restoreReplies() {
		const mid = FlowRouter.getQueryParam('reply');
		if (!mid) {
			return;
		}

		const message = Messages.findOne(mid) || await callWithErrorHandling('getSingleMessage', mid);
		if (!message) {
			return;
		}

		this.$input.data('reply', [message]).trigger('dataChange');
	}

	requestInputFocus() {
		setTimeout(() => {
			if (this.input && window.matchMedia('screen and (min-device-width: 500px)').matches) {
				this.input.focus();
			}
		}, 200);
	}

	recordInputAsDraft() {
		const message = this.collection.findOne(this.editing.id);
		const record = this.records[this.editing.id] || {};
		const draft = this.input.value;

		if (draft === message.msg) {
			this.clearCurrentDraft();
			return;
		}

		record.draft = draft;
		this.records[this.editing.id] = record;
	}

	clearCurrentDraft() {
		const hasValue = this.records[this.editing.id];
		delete this.records[this.editing.id];
		return !!hasValue;
	}

	resetToDraft(id) {
		const message = this.collection.findOne(id);
		const oldValue = this.input.value;
		messageBoxState.set(this.input, message.msg);
		return oldValue !== message.msg;
	}

	toPrevMessage() {
		const { element } = this.editing;
		if (!element) {
			const messages = Array.from(this.wrapper.querySelectorAll('.own:not(.system)'));
			const message = messages.pop();
			return message && this.edit(message, false);
		}

		for (
			let previous = element.previousElementSibling;
			previous;
			previous = previous.previousElementSibling
		) {
			if (previous.matches('.own:not(.system)')) {
				return this.edit(previous, false);
			}
		}
		this.clearEditing();
	}

	toNextMessage() {
		const { element } = this.editing;
		if (element) {
			let next;
			for (next = element.nextElementSibling; next; next = next.nextElementSibling) {
				if (next.matches('.own:not(.system)')) {
					break;
				}
			}

			next ? this.edit(next, true) : this.clearEditing();
		} else {
			this.clearEditing();
		}
	}

	edit(element, isEditingTheNextOne) {
		const message = this.collection.findOne(element.dataset.id);

		const hasPermission = hasAtLeastOnePermission('edit-message', message.rid);
		const editAllowed = settings.get('Message_AllowEditing');
		const editOwn = message && message.u && message.u._id === Meteor.userId();

		if (!hasPermission && (!editAllowed || !editOwn)) {
			return;
		}

		if (element.classList.contains('system')) {
			return;
		}

		const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes');
		if (blockEditInMinutes && blockEditInMinutes !== 0) {
			let currentTsDiff;
			let msgTs;
			if (message.ts) {
				msgTs = moment(message.ts);
			}
			if (msgTs) {
				currentTsDiff = moment().diff(msgTs, 'minutes');
			}
			if (currentTsDiff > blockEditInMinutes) {
				return;
			}
		}

		const draft = this.records[message._id];
		let msg = draft && draft.draft;
		msg = msg || message.msg;

		this.clearEditing();

		this.editing.element = element;
		this.editing.id = message._id;
		this.input.parentElement.classList.add('editing');
		element.classList.add('editing');

		if (message.attachments && message.attachments[0].description) {
			messageBoxState.set(this.input, message.attachments[0].description);
		} else {
			messageBoxState.set(this.input, msg);
		}

		const cursorPosition = isEditingTheNextOne ? 0 : -1;
		this.input.focus();
		this.$input.setCursorPosition(cursorPosition);
	}

	clearEditing() {
		if (!this.editing.element) {
			this.editing.saved = this.input.value;
			this.editing.savedCursor = this.input.selectionEnd;
			return;
		}

		this.recordInputAsDraft();
		this.input.parentElement.classList.remove('editing');
		this.editing.element.classList.remove('editing');
		delete this.editing.id;
		delete this.editing.element;

		messageBoxState.set(this.input, this.editing.saved || '');
		const cursorPosition = this.editing.savedCursor ? this.editing.savedCursor : -1;
		this.$input.setCursorPosition(cursorPosition);
	}

	async send(event, { rid, tmid, value, tshow }, done = () => {}) {
		const threadsEnabled = settings.get('Threads_enabled');

		UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid });

		if (!ChatSubscription.findOne({ rid })) {
			await callWithErrorHandling('joinRoom', rid);
		}

		messageBoxState.save({ rid, tmid }, this.input);

		let msg = value.trim();
		if (msg) {
			const mention = this.$input.data('mention-user') || false;
			const replies = this.$input.data('reply') || [];
			if (!mention || !threadsEnabled) {
				msg = await prependReplies(msg, replies, mention);
			}

			if (mention && threadsEnabled && replies.length) {
				tmid = replies[0]._id;
			}
		}

		// don't add tmid or tshow if the message isn't part of a thread (it can happen if editing the main message of a thread)
		const originalMessage = this.collection.findOne({ _id: this.editing.id }, { fields: { tmid: 1 }, reactive: false });
		if (originalMessage && tmid && !originalMessage.tmid) {
			tmid = undefined;
			tshow = undefined;
		}

		if (msg) {
			readMessage.readNow(rid);
			readMessage.refreshUnreadMark(rid);

			const message = await onClientBeforeSendMessage({
				_id: Random.id(),
				rid,
				tshow,
				tmid,
				msg,
			});

			try {
				await this.processMessageSend(message);
				this.$input.removeData('reply').trigger('dataChange');
			} catch (error) {
				handleError(error);
			}
			return done();
		}

		if (this.editing.id) {
			const message = this.collection.findOne(this.editing.id);
			const isDescription = message.attachments && message.attachments[0] && message.attachments[0].description;

			try {
				if (isDescription) {
					await this.processMessageEditing({ _id: this.editing.id, rid, msg: '' });
					return done();
				}

				this.resetToDraft(this.editing.id);
				this.confirmDeleteMsg(message, done);
				return;
			} catch (error) {
				handleError(error);
			}
		}

		return done();
	}

	async processMessageSend(message) {
		if (await this.processSetReaction(message)) {
			return;
		}

		this.clearCurrentDraft();

		if (await this.processTooLongMessage(message)) {
			return;
		}

		if (await this.processMessageEditing({ ...message, _id: this.editing.id })) {
			return;
		}

		KonchatNotification.removeRoomNotification(message.rid);

		if (await this.processSlashCommand(message)) {
			return;
		}

		await callWithErrorHandling('sendMessage', message);
	}

	async processSetReaction({ rid, tmid, msg }) {
		if (msg.slice(0, 2) !== '+:') {
			return false;
		}

		const reaction = msg.slice(1).trim();
		if (!emoji.list[reaction]) {
			return false;
		}

		const lastMessage = this.collection.findOne({ rid, tmid }, { fields: { ts: 1 }, sort: { ts: -1 } });
		await callWithErrorHandling('setReaction', reaction, lastMessage._id);
		return true;
	}

	async processTooLongMessage({ msg, rid, tmid }) {
		const adjustedMessage = messageProperties.messageWithoutEmojiShortnames(msg);
		if (messageProperties.length(adjustedMessage) <= settings.get('Message_MaxAllowedSize') && msg) {
			return false;
		}

		if (!settings.get('FileUpload_Enabled') || !settings.get('Message_AllowConvertLongMessagesToAttachment') || this.editing.id) {
			throw new Error({ error: 'Message_too_long' });
		}

		const onConfirm = () => {
			const contentType = 'text/plain';
			const messageBlob = new Blob([msg], { type: contentType });
			const fileName = `${ Meteor.user().username } - ${ new Date() }.txt`;
			const file = new File([messageBlob], fileName, { type: contentType, lastModified: Date.now() });
			fileUpload([{ file, name: fileName }], this.input, { rid, tmid });
			imperativeModal.close();
		};

		const onClose = () => {
			messageBoxState.set(this.input, msg);
			imperativeModal.close();
		};

		imperativeModal.open({
			component: GenericModal,
			props: {
				title: t('Message_too_long'),
				children: t('Send_it_as_attachment_instead_question'),
				onConfirm,
				onClose,
				onCancel: onClose,
				variant: 'warning',
			},
		});

		return true;
	}

	async processMessageEditing(message) {
		if (!message._id) {
			return false;
		}

		if (MessageTypes.isSystemMessage(message)) {
			return false;
		}

		this.clearEditing();
		await callWithErrorHandling('updateMessage', message);
		return true;
	}

	async processSlashCommand(msgObject) {
		if (msgObject.msg[0] === '/') {
			const match = msgObject.msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m);
			if (match) {
				let command;
				if (slashCommands.commands[match[1]]) {
					const commandOptions = slashCommands.commands[match[1]];
					command = match[1];
					const param = match[2] || '';

					if (!commandOptions.permission || hasAtLeastOnePermission(commandOptions.permission, Session.get('openedRoom'))) {
						if (commandOptions.clientOnly) {
							commandOptions.callback(command, param, msgObject);
						} else {
							const triggerId = generateTriggerId(slashCommands.commands[command].appId);
							Meteor.call('slashCommand', { cmd: command, params: param, msg: msgObject, triggerId }, (err, result) => {
								typeof commandOptions.result === 'function' && commandOptions.result(err, result, { cmd: command, params: param, msg: msgObject });
							});
						}

						return true;
					}
				}

				if (!settings.get('Message_AllowUnrecognizedSlashCommand')) {
					const invalidCommandMsg = {
						_id: Random.id(),
						rid: msgObject.rid,
						ts: new Date(),
						msg: TAPi18n.__('No_such_command', { command: escapeHTML(match[1]) }),
						u: {
							username: settings.get('InternalHubot_Username') || 'rocket.cat',
						},
						private: true,
					};

					this.collection.upsert({ _id: invalidCommandMsg._id }, invalidCommandMsg);
					return true;
				}
			}
		}

		return false;
	}

	confirmDeleteMsg(message, done = () => {}) {
		if (MessageTypes.isSystemMessage(message)) {
			return done();
		}

		const room = message.drid && Rooms.findOne({
			_id: message.drid,
			prid: { $exists: true },
		});

		const onConfirm = () => {
			if (this.editing.id === message._id) {
				this.clearEditing();
			}

			this.deleteMsg(message);

			this.$input.focus();
			done();

			imperativeModal.close();
			dispatchToastMessage({ type: 'success', message: t('Your_entry_has_been_deleted') });
		};

		const onCloseModal = () => {
			imperativeModal.close();
			if (this.editing.id === message._id) {
				this.clearEditing();
			}
			this.$input.focus();
			done();
		};

		imperativeModal.open({
			component: GenericModal,
			props: {
				title: t('Are_you_sure'),
				children: room ? t('The_message_is_a_discussion_you_will_not_be_able_to_recover') : t('You_will_not_be_able_to_recover'),
				variant: 'danger',
				confirmText: t('Yes_delete_it'),
				onConfirm,
				onClose: onCloseModal,
				onCancel: onCloseModal,
			},
		});
	}

	async deleteMsg({ _id, rid, ts }) {
		const forceDelete = hasAtLeastOnePermission('force-delete-message', rid);
		const blockDeleteInMinutes = settings.get('Message_AllowDeleting_BlockDeleteInMinutes');
		if (blockDeleteInMinutes && forceDelete === false) {
			let msgTs;
			if (ts) {
				msgTs = moment(ts);
			}
			let currentTsDiff;
			if (msgTs) {
				currentTsDiff = moment().diff(msgTs, 'minutes');
			}
			if (currentTsDiff > blockDeleteInMinutes) {
				dispatchToastMessage({ type: 'error', message: t('Message_deleting_blocked') });
				return;
			}
		}


		await callWithErrorHandling('deleteMessage', { _id });
	}

	keydown(event) {
		const { currentTarget: input, which: keyCode } = event;

		if (keyCode === keyCodes.ESCAPE && this.editing.element) {
			event.preventDefault();
			event.stopPropagation();

			if (!this.resetToDraft(this.editing.id)) {
				this.clearCurrentDraft();
				this.clearEditing();
				return true;
			}

			return;
		}

		if (keyCode === keyCodes.ARROW_UP || keyCode === keyCodes.ARROW_DOWN) {
			if (event.shiftKey) {
				return;
			}

			const cursorPosition = input.selectionEnd;

			if (keyCode === keyCodes.ARROW_UP) {
				if (cursorPosition === 0) {
					this.toPrevMessage();
				} else if (!event.altKey) {
					return;
				}

				if (event.altKey) {
					this.$input.setCursorPosition(0);
				}
			} else {
				if (cursorPosition === input.value.length) {
					this.toNextMessage();
				} else if (!event.altKey) {
					return;
				}

				if (event.altKey) {
					this.$input.setCursorPosition(-1);
				}
			}

			event.preventDefault();
			event.stopPropagation();
		}
	}

	keyup(event, { rid, tmid }) {
		const { currentTarget: input, which: keyCode } = event;

		if (!Object.values(keyCodes).includes(keyCode)) {
			if (input.value.trim()) {
				UserAction.start(rid, USER_ACTIVITIES.USER_TYPING, { tmid });
			} else {
				UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid });
			}
		}

		messageBoxState.save({ rid, tmid }, input);
	}

	onDestroyed(rid, tmid) {
		UserAction.cancel(rid);
		// TODO: check why we need too many ?. here :(
		if (this.input?.parentElement?.classList.contains('editing') === true) {
			if (!tmid) {
				this.clearCurrentDraft();
				this.clearEditing();
			}
			messageBoxState.set(this.input, '');
			messageBoxState.save({ rid, tmid }, this.$input);
		}
	}
}
