From 082d3cece0812ed53f24d33e206a27ebd15d6e3f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 19 Dec 2024 09:10:51 -0600 Subject: [PATCH 1/5] fix: Start cronjobs after the database has been migrated (#34215) --- .changeset/lucky-wolves-turn.md | 5 +++++ apps/meteor/server/startup/cron.ts | 8 +++----- apps/meteor/server/startup/index.ts | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/lucky-wolves-turn.md diff --git a/.changeset/lucky-wolves-turn.md b/.changeset/lucky-wolves-turn.md new file mode 100644 index 000000000000..0dbb08af9e0f --- /dev/null +++ b/.changeset/lucky-wolves-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a UI issue that showed the incorrect migration number on the `Information` page. This was caused by a function calculating the stats before the server had migrated the database and updated the control. diff --git a/apps/meteor/server/startup/cron.ts b/apps/meteor/server/startup/cron.ts index 308d1297a01a..8951038f80fe 100644 --- a/apps/meteor/server/startup/cron.ts +++ b/apps/meteor/server/startup/cron.ts @@ -1,5 +1,4 @@ import { Logger } from '@rocket.chat/logger'; -import { Meteor } from 'meteor/meteor'; import { federationCron } from '../cron/federation'; import { npsCron } from '../cron/nps'; @@ -12,14 +11,13 @@ import { videoConferencesCron } from '../cron/videoConferences'; const logger = new Logger('SyncedCron'); -Meteor.defer(async () => { +export const startCronJobs = async (): Promise => { await startCron(); - await oembedCron(); await usageReportCron(logger); await npsCron(); await temporaryUploadCleanupCron(); await federationCron(); await videoConferencesCron(); - await userDataDownloadsCron(); -}); + userDataDownloadsCron(); +}; diff --git a/apps/meteor/server/startup/index.ts b/apps/meteor/server/startup/index.ts index 001af2c12be5..048735bba752 100644 --- a/apps/meteor/server/startup/index.ts +++ b/apps/meteor/server/startup/index.ts @@ -1,6 +1,6 @@ import './appcache'; import './callbacks'; -import './cron'; +import { startCronJobs } from './cron'; import './initialData'; import './serverRunning'; import './coreApps'; @@ -13,6 +13,8 @@ import { isRunningMs } from '../lib/isRunningMs'; export const startup = async () => { await performMigrationProcedure(); + + setImmediate(() => startCronJobs()); // only starts network broker if running in micro services mode if (!isRunningMs()) { require('./localServices'); From 2d41274ae21ec51e7382da92e2f4e0c36186f814 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 19 Dec 2024 13:03:26 -0300 Subject: [PATCH 2/5] feat: Export selected room messages as JSON file (#34076) --- .changeset/shaggy-bulldogs-beg.md | 7 + apps/meteor/app/ui/client/lib/ChatMessages.ts | 2 +- .../message/variants/RoomMessage.tsx | 2 +- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../views/room/Header/icons/Encrypted.tsx | 2 +- .../contexts/SelectedMessagesContext.tsx | 43 ++- .../client/views/room/body/RoomBody.tsx | 5 +- .../client/views/room/body/RoomBodyV2.tsx | 5 +- .../body/hooks/useSelectAllAndScrollToTop.ts | 15 + .../views/room/composer/ComposerContainer.tsx | 7 + .../views/room/composer/ComposerMessage.tsx | 1 + .../room/composer/ComposerSelectMessages.tsx | 34 ++ .../room/composer/messageBox/MessageBox.tsx | 6 +- .../ExportMessages/ExportMessages.tsx | 318 ++++++++++++++++-- .../ExportMessages/FileExport.tsx | 105 ------ .../ExportMessages/MailExportForm.tsx | 218 ------------ .../useDownloadExportMutation.ts | 50 +++ .../providers/SelectedMessagesProvider.tsx | 23 +- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 14 + apps/meteor/tests/e2e/export-messages.spec.ts | 72 ++++ .../page-objects/fragments/home-content.ts | 2 +- .../fragments/home-flextab-exportMessages.ts | 33 ++ .../page-objects/fragments/home-flextab.ts | 8 + apps/meteor/tests/e2e/page-objects/utils.ts | 6 + packages/i18n/src/locales/de.i18n.json | 3 +- packages/i18n/src/locales/en.i18n.json | 12 +- packages/i18n/src/locales/fi.i18n.json | 3 +- packages/i18n/src/locales/hi-IN.i18n.json | 3 +- packages/i18n/src/locales/hu.i18n.json | 3 +- packages/i18n/src/locales/nn.i18n.json | 3 +- packages/i18n/src/locales/no.i18n.json | 3 +- packages/i18n/src/locales/pl.i18n.json | 4 +- packages/i18n/src/locales/se.i18n.json | 4 +- packages/i18n/src/locales/sv.i18n.json | 3 +- .../MessageFooterCalloutContent.tsx | 17 +- 35 files changed, 647 insertions(+), 391 deletions(-) create mode 100644 .changeset/shaggy-bulldogs-beg.md create mode 100644 apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts create mode 100644 apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts create mode 100644 apps/meteor/tests/e2e/export-messages.spec.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts diff --git a/.changeset/shaggy-bulldogs-beg.md b/.changeset/shaggy-bulldogs-beg.md new file mode 100644 index 000000000000..211d11d7b67c --- /dev/null +++ b/.changeset/shaggy-bulldogs-beg.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-composer': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduces a new option when exporting messages, allowing users to select and download a JSON file directly from client diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index f7fff0b2a2aa..3745864061f4 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -31,7 +31,7 @@ export class ChatMessages implements ChatAPI { public composer: ComposerAPI | undefined; - public setComposerAPI = (composer: ComposerAPI): void => { + public setComposerAPI = (composer?: ComposerAPI): void => { this.composer?.release(); this.composer = composer; }; diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 8dec6c9abbaa..90bc2236e837 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -67,7 +67,7 @@ const RoomMessage = ({ ref={messageRef} id={message._id} role='listitem' - aria-roledescription={sequential ? t('sequential_message') : t('message')} + aria-roledescription={t('message')} tabIndex={0} aria-labelledby={`${message._id}-displayName ${message._id}-time ${message._id}-content ${message._id}-read-status`} onClick={selecting ? toggleSelected : undefined} diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 6a782faafa1f..dbdaa1b04ac7 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -111,7 +111,7 @@ export type UploadsAPI = { export type ChatAPI = { readonly uid: string | null; readonly composer?: ComposerAPI; - readonly setComposerAPI: (composer: ComposerAPI) => void; + readonly setComposerAPI: (composer?: ComposerAPI) => void; readonly data: DataAPI; readonly uploads: UploadsAPI; readonly readStateManager: ReadStateManager; diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index ca21153126fd..8af62f3fdd15 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -9,7 +9,7 @@ import { HeaderState } from '../../../../components/Header'; const Encrypted = ({ room }: { room: IRoom }) => { const { t } = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); - return e2eEnabled && room?.encrypted ? : null; + return e2eEnabled && room?.encrypted ? : null; }; export default memo(Encrypted); diff --git a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx index f9fda919d88c..800f7155082e 100644 --- a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx +++ b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useContext } from 'react'; +import { createContext, useCallback, useContext, useEffect } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { selectedMessageStore } from '../../providers/SelectedMessagesProvider'; @@ -21,7 +21,19 @@ export const useIsSelectedMessage = (mid: string): boolean => { const getSnapshot = (): boolean => selectedMessageStore.isSelected(mid); - return useSyncExternalStore(subscribe, getSnapshot); + const isSelected = useSyncExternalStore(subscribe, getSnapshot); + + useEffect(() => { + if (isSelected) { + return; + } + + selectedMessageStore.addAvailableMessage(mid); + + return () => selectedMessageStore.removeAvailableMessage(mid); + }, [mid, selectedMessageStore, isSelected]); + + return isSelected; }; export const useIsSelecting = (): boolean => { @@ -44,6 +56,20 @@ export const useToggleSelect = (mid: string): (() => void) => { }, [mid, selectedMessageStore]); }; +export const useToggleSelectAll = (): (() => void) => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + return useCallback(() => { + selectedMessageStore.toggleAll(Array.from(selectedMessageStore.availableMessages)); + }, [selectedMessageStore]); +}; + +export const useClearSelection = (): (() => void) => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + return useCallback(() => { + selectedMessageStore.clearStore(); + }, [selectedMessageStore]); +}; + export const useCountSelected = (): number => { const { selectedMessageStore } = useContext(SelectedMessageContext); @@ -56,3 +82,16 @@ export const useCountSelected = (): number => { return useSyncExternalStore(subscribe, getSnapshot); }; + +export const useAvailableMessagesCount = () => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + + const subscribe = useCallback( + (callback: () => void): (() => void) => selectedMessageStore.on('change', callback), + [selectedMessageStore], + ); + + const getSnapshot = () => selectedMessageStore.availableMessagesCount(); + + return useSyncExternalStore(subscribe, getSnapshot); +}; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 13c111592c39..b78ed29ad4d1 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -40,6 +40,7 @@ import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; +import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; const RoomBody = (): ReactElement => { @@ -116,6 +117,7 @@ const RoomBody = (): ReactElement => { const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); const { messageListRef } = useMessageListNavigation(); + const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); const { handleNewMessageButtonClick, handleJumpToRecentButtonClick, handleComposerResize, hasNewMessages, newMessagesScrollRef } = useHasNewMessages(room._id, user?._id, atBottomRef, { @@ -133,7 +135,7 @@ const RoomBody = (): ReactElement => { leaderBannerInnerRef, unreadBarInnerRef, getMoreInnerRef, - + selectAndScrollRef, messageListRef, ); @@ -313,6 +315,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 3f1ecb0fd2bc..3b92e9b7910d 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -37,6 +37,7 @@ import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; +import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; const RoomBody = (): ReactElement => { @@ -111,6 +112,7 @@ const RoomBody = (): ReactElement => { const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); const { messageListRef } = useMessageListNavigation(); + const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); const { handleNewMessageButtonClick, handleJumpToRecentButtonClick, handleComposerResize, hasNewMessages, newMessagesScrollRef } = useHasNewMessages(room._id, user?._id, atBottomRef, { @@ -128,7 +130,7 @@ const RoomBody = (): ReactElement => { sectionScrollRef, unreadBarInnerRef, getMoreInnerRef, - + selectAndScrollRef, messageListRef, ); @@ -285,6 +287,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts b/apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts new file mode 100644 index 000000000000..bf53178fa67e --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts @@ -0,0 +1,15 @@ +import { useRef } from 'react'; + +import { useToggleSelectAll } from '../../MessageList/contexts/SelectedMessagesContext'; + +export const useSelectAllAndScrollToTop = () => { + const ref = useRef(null); + const handleToggleAll = useToggleSelectAll(); + + const selectAllAndScrollToTop = () => { + ref.current?.scrollTo({ top: 0, behavior: 'smooth' }); + handleToggleAll(); + }; + + return { innerRef: ref, selectAllAndScrollToTop }; +}; diff --git a/apps/meteor/client/views/room/composer/ComposerContainer.tsx b/apps/meteor/client/views/room/composer/ComposerContainer.tsx index e027ff1f5b87..77001827032b 100644 --- a/apps/meteor/client/views/room/composer/ComposerContainer.tsx +++ b/apps/meteor/client/views/room/composer/ComposerContainer.tsx @@ -13,6 +13,7 @@ import type { ComposerMessageProps } from './ComposerMessage'; import ComposerMessage from './ComposerMessage'; import ComposerOmnichannel from './ComposerOmnichannel'; import ComposerReadOnly from './ComposerReadOnly'; +import ComposerSelectMessages from './ComposerSelectMessages'; import ComposerVoIP from './ComposerVoIP'; import { useRoom } from '../contexts/RoomContext'; import { useMessageComposerIsAnonymous } from './hooks/useMessageComposerIsAnonymous'; @@ -20,6 +21,7 @@ import { useMessageComposerIsArchived } from './hooks/useMessageComposerIsArchiv import { useMessageComposerIsBlocked } from './hooks/useMessageComposerIsBlocked'; import { useMessageComposerIsReadOnly } from './hooks/useMessageComposerIsReadOnly'; import { useAirGappedRestriction } from '../../../hooks/useAirGappedRestriction'; +import { useIsSelecting } from '../MessageList/contexts/SelectedMessagesContext'; const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactElement => { const room = useRoom(); @@ -28,6 +30,7 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE const mustJoinWithCode = !props.subscription && room.joinCodeRequired && !canJoinWithoutCode; const isAnonymous = useMessageComposerIsAnonymous(); + const isSelectingMessages = useIsSelecting(); const isBlockedOrBlocker = useMessageComposerIsBlocked({ subscription: props.subscription }); const isArchived = useMessageComposerIsArchived(room._id, props.subscription); const isReadOnly = useMessageComposerIsReadOnly(room._id); @@ -74,6 +77,10 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE return ; } + if (isSelectingMessages) { + return ; + } + return ( <> {children} diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 9148185da50c..a5fd473f788b 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -22,6 +22,7 @@ export type ComposerMessageProps = { onNavigateToNextMessage?: () => void; onNavigateToPreviousMessage?: () => void; onUploadFiles?: (files: readonly File[]) => void; + onClickSelectAll?: () => void; }; const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): ReactElement => { diff --git a/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx new file mode 100644 index 000000000000..2a77e250e5ba --- /dev/null +++ b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx @@ -0,0 +1,34 @@ +import { Button, ButtonGroup } from '@rocket.chat/fuselage'; +import { MessageFooterCallout, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; +import type { ReactElement } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { ComposerMessageProps } from './ComposerMessage'; +import { useCountSelected, useClearSelection, useAvailableMessagesCount } from '../MessageList/contexts/SelectedMessagesContext'; + +const ComposerSelectMessages = ({ onClickSelectAll }: ComposerMessageProps): ReactElement => { + const { t } = useTranslation(); + + const clearSelection = useClearSelection(); + const countSelected = useCountSelected(); + const countAvailable = useAvailableMessagesCount(); + + return ( + + + {t('__count__messages_selected', { count: countSelected })} + + + + + + + ); +}; + +export default ComposerSelectMessages; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 6546a6be9245..0a52355ac58a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -131,7 +131,11 @@ const MessageBox = ({ const callbackRef = useCallback( (node: HTMLTextAreaElement) => { - if (node === null || chat.composer) { + if (node === null && chat.composer) { + return chat.setComposerAPI(); + } + + if (chat.composer) { return; } chat.setComposerAPI(createComposerAPI(node, storageID)); diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index f6a621200119..8736c8a1bec3 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -1,18 +1,43 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { + FieldError, + Field, + FieldLabel, + FieldRow, + TextAreaInput, + TextInput, + ButtonGroup, + Button, + Icon, + FieldGroup, + Select, + InputBox, + Callout, +} from '@rocket.chat/fuselage'; +import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import React, { useContext, useEffect, useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import FileExport from './FileExport'; -import MailExportForm from './MailExportForm'; -import { ContextualbarHeader, ContextualbarIcon, ContextualbarTitle, ContextualbarClose } from '../../../../components/Contextualbar'; +import { useDownloadExportMutation } from './useDownloadExportMutation'; +import { useRoomExportMutation } from './useRoomExportMutation'; +import { validateEmail } from '../../../../../lib/emailValidator'; +import { + ContextualbarHeader, + ContextualbarScrollableContent, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarFooter, +} from '../../../../components/Contextualbar'; +import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; import { useRoom } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; -export type MailExportFormValues = { - type: 'email' | 'file'; +export type ExportMessagesFormValues = { + type: 'email' | 'file' | 'download'; dateFrom: string; dateTo: string; format: 'html' | 'json'; @@ -24,16 +49,25 @@ export type MailExportFormValues = { const ExportMessages = () => { const { t } = useTranslation(); - const room = useRoom(); - const { closeTab } = useRoomToolbox(); + const formFocus = useAutoFocus(); + const room = useRoom(); + const isE2ERoom = room.encrypted; const roomName = room?.t && roomCoordinator.getRoomName(room.t, room); - const methods = useForm({ + const { + control, + formState: { errors, isSubmitting }, + watch, + register, + setValue, + handleSubmit, + clearErrors, + } = useForm({ mode: 'onBlur', defaultValues: { - type: 'email', + type: isE2ERoom ? 'download' : 'email', dateFrom: '', dateTo: '', toUsers: [], @@ -43,18 +77,94 @@ const ExportMessages = () => { postProcess: 'sprintf', sprintf: [roomName], }), - format: 'html', + format: isE2ERoom ? 'json' : 'html', }, }); + const exportOptions = useMemo( () => [ - ['email', t('Send_via_email')], - ['file', t('Export_as_file')], + ['email', t('Send_email')], + ['file', t('Send_file_via_email')], + ['download', t('Download_file')], ], [t], ); + const outputOptions = useMemo( + () => [ + ['html', t('HTML')], + ['json', t('JSON')], + ], + [t], + ); + + const roomExportMutation = useRoomExportMutation(); + const downloadExportMutation = useDownloadExportMutation(); + + const { selectedMessageStore } = useContext(SelectedMessageContext); + const messageCount = useCountSelected(); + + const { type, toUsers } = watch(); + + useEffect(() => { + if (type !== 'file') { + selectedMessageStore.setIsSelecting(true); + } + + return (): void => { + selectedMessageStore.reset(); + }; + }, [type, selectedMessageStore]); + + useEffect(() => { + if (type === 'email') { + setValue('format', 'html'); + } + + if (type === 'download') { + setValue('format', 'json'); + } + + setValue('messagesCount', messageCount); + }, [type, setValue, messageCount]); + + const handleExport = async ({ type, toUsers, dateFrom, dateTo, format, subject, additionalEmails }: ExportMessagesFormValues) => { + const messages = selectedMessageStore.getSelectedMessages(); + + if (type === 'download') { + return downloadExportMutation.mutateAsync({ + mids: messages, + }); + } + + if (type === 'file') { + return roomExportMutation.mutateAsync({ + rid: room._id, + type: 'file', + ...(dateFrom && { dateFrom }), + ...(dateTo && { dateTo }), + format, + }); + } + + roomExportMutation.mutateAsync({ + rid: room._id, + type: 'email', + toUsers, + toEmails: additionalEmails?.split(','), + subject, + messages, + }); + }; + const formId = useUniqueId(); + const methodField = useUniqueId(); + const formatField = useUniqueId(); + const toUsersField = useUniqueId(); + const dateFromField = useUniqueId(); + const dateToField = useUniqueId(); + const additionalEmailsField = useUniqueId(); + const subjectField = useUniqueId(); return ( <> @@ -63,14 +173,176 @@ const ExportMessages = () => { {t('Export_Messages')} - - {methods.watch('type') === 'email' && ( - - )} - {methods.watch('type') === 'file' && ( - - )} - + +
+ + + {t('Method')} + + ( + + )} + /> + + + {type === 'file' && ( + <> + + {t('Date_From')} + + } + /> + + + + {t('Date_to')} + + } + /> + + + + )} + {type === 'email' && ( + <> + + {t('To_users')} + + ( + { + onChange(value); + clearErrors('additionalEmails'); + }} + onBlur={onBlur} + name={name} + /> + )} + /> + + + + {t('To_additional_emails')} + + { + if (additionalEmails === '') { + return undefined; + } + + const emails = additionalEmails?.split(',').map((email) => email.trim()); + if (Array.isArray(emails) && emails.every((email) => validateEmail(email.trim()))) { + return undefined; + } + + return t('Mail_Message_Invalid_emails', { postProcess: 'sprintf', sprintf: [additionalEmails] }); + }, + validateToUsers: (additionalEmails) => { + if (additionalEmails !== '' || toUsers?.length > 0) { + return undefined; + } + + return t('Mail_Message_Missing_to'); + }, + }, + }} + render={({ field }) => ( + } + aria-describedby={`${additionalEmailsField}-error`} + aria-invalid={Boolean(errors?.additionalEmails?.message)} + error={errors?.additionalEmails?.message} + /> + )} + /> + + {errors?.additionalEmails && ( + + {errors.additionalEmails.message} + + )} + + + {t('Subject')} + + ( + } /> + )} + /> + + + + )} + {type !== 'file' && ( + <> + (messagesCount > 0 ? undefined : t('Mail_Message_No_messages_selected_select_all')), + })} + /> + {errors.messagesCount && ( + + + {errors.messagesCount.message} + + + )} + + )} + +
+
+ + + + + + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx deleted file mode 100644 index 2d4a3bf0030c..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, FieldLabel, FieldRow, Select, ButtonGroup, Button, FieldGroup, InputBox } from '@rocket.chat/fuselage'; -import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import type { MailExportFormValues } from './ExportMessages'; -import { useRoomExportMutation } from './useRoomExportMutation'; -import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../components/Contextualbar'; - -type FileExportProps = { - formId: string; - rid: IRoom['_id']; - onCancel: () => void; - exportOptions: SelectOption[]; -}; - -const FileExport = ({ formId, rid, exportOptions, onCancel }: FileExportProps) => { - const { t } = useTranslation(); - const { control, handleSubmit } = useFormContext(); - const roomExportMutation = useRoomExportMutation(); - const formFocus = useAutoFocus(); - - const outputOptions = useMemo( - () => [ - ['html', t('HTML')], - ['json', t('JSON')], - ], - [t], - ); - - const handleExport = ({ dateFrom, dateTo, format }: MailExportFormValues) => { - roomExportMutation.mutateAsync({ - rid, - type: 'file', - ...(dateFrom && { dateFrom }), - ...(dateTo && { dateTo }), - format, - }); - }; - - const typeField = useUniqueId(); - const dateFromField = useUniqueId(); - const dateToField = useUniqueId(); - const formatField = useUniqueId(); - - return ( - <> - -
- - - {t('Method')} - - } - /> - - - -
-
- - - - - - - - ); -}; - -export default FileExport; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx deleted file mode 100644 index b6f0e4b88bf2..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { css } from '@rocket.chat/css-in-js'; -import type { SelectOption } from '@rocket.chat/fuselage'; -import { - FieldError, - Field, - FieldLabel, - FieldRow, - TextAreaInput, - TextInput, - ButtonGroup, - Button, - Box, - Icon, - Callout, - FieldGroup, - Select, -} from '@rocket.chat/fuselage'; -import { useAutoFocus, useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useEffect, useContext } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import type { MailExportFormValues } from './ExportMessages'; -import { useRoomExportMutation } from './useRoomExportMutation'; -import { validateEmail } from '../../../../../lib/emailValidator'; -import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../components/Contextualbar'; -import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; -import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; - -type MailExportFormProps = { - formId: string; - rid: IRoom['_id']; - onCancel: () => void; - exportOptions: SelectOption[]; -}; - -const MailExportForm = ({ formId, rid, onCancel, exportOptions }: MailExportFormProps) => { - const { t } = useTranslation(); - const formFocus = useAutoFocus(); - - const { - watch, - setValue, - control, - register, - formState: { errors, isDirty, isSubmitting }, - handleSubmit, - clearErrors, - } = useFormContext(); - const roomExportMutation = useRoomExportMutation(); - - const { selectedMessageStore } = useContext(SelectedMessageContext); - const messages = selectedMessageStore.getSelectedMessages(); - - const count = useCountSelected(); - - const clearSelection = useMutableCallback(() => { - selectedMessageStore.clearStore(); - }); - - useEffect(() => { - selectedMessageStore.setIsSelecting(true); - return (): void => { - selectedMessageStore.reset(); - }; - }, [selectedMessageStore]); - - const { toUsers } = watch(); - - useEffect(() => { - setValue('messagesCount', messages.length); - }, [setValue, messages.length]); - - const handleExport = async ({ toUsers, subject, additionalEmails }: MailExportFormValues) => { - roomExportMutation.mutateAsync({ - rid, - type: 'email', - toUsers, - toEmails: additionalEmails?.split(','), - subject, - messages, - }); - }; - - const clickable = css` - cursor: pointer; - `; - - const methodField = useUniqueId(); - const toUsersField = useUniqueId(); - const additionalEmailsField = useUniqueId(); - const subjectField = useUniqueId(); - - return ( - <> - -
- - - {t('Method')} - - (messagesCount > 0 ? undefined : t('Mail_Message_No_messages_selected_select_all')), - })} - /> - {errors.messagesCount && {errors.messagesCount.message}} - - - {t('To_users')} - - ( - { - onChange(value); - clearErrors('additionalEmails'); - }} - onBlur={onBlur} - name={name} - /> - )} - /> - - - - {t('To_additional_emails')} - - { - const emails = additionalEmails?.split(',').map((email) => email.trim()); - if (Array.isArray(emails) && emails.every((email) => validateEmail(email.trim()))) { - return undefined; - } - - return t('Mail_Message_Invalid_emails', { postProcess: 'sprintf', sprintf: [additionalEmails] }); - }, - validateToUsers: (additionalEmails) => { - if (additionalEmails !== '' || toUsers?.length > 0) { - return undefined; - } - - return t('Mail_Message_Missing_to'); - }, - }, - }} - render={({ field }) => ( - } - aria-describedby={`${additionalEmailsField}-error`} - aria-invalid={Boolean(errors?.additionalEmails?.message)} - error={errors?.additionalEmails?.message} - /> - )} - /> - - {errors?.additionalEmails && ( - - {errors.additionalEmails.message} - - )} - - - {t('Subject')} - - } />} - /> - - - -
-
- - - - - - - - ); -}; - -export default MailExportForm; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts new file mode 100644 index 000000000000..f35f153da8bb --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -0,0 +1,50 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import type { FindOptions } from 'mongodb'; +import { useTranslation } from 'react-i18next'; + +import { Messages } from '../../../../../app/models/client'; +import { downloadJsonAs } from '../../../../lib/download'; +import { useRoom } from '../../contexts/RoomContext'; + +const messagesFields: FindOptions = { projection: { _id: 1, ts: 1, u: 1, msg: 1, _updatedAt: 1, tlm: 1, replies: 1, tmid: 1 } }; + +export const useDownloadExportMutation = () => { + const { t } = useTranslation(); + const room = useRoom(); + const user = useUser(); + const dispatchToastMessage = useToastMessageDispatch(); + + return useMutation({ + mutationFn: async ({ mids }: { mids: IMessage['_id'][] }) => { + const messages = Messages.find( + { + $or: [{ _id: { $in: mids } }, { tmid: { $in: mids } }], + }, + messagesFields, + ).fetch(); + + const fileData = { + roomId: room._id, + roomName: room.fname || room.name, + userExport: { + id: user?._id, + username: user?.username, + name: user?.name, + roles: user?.roles, + }, + exportDate: new Date().toISOString(), + messages, + }; + + return downloadJsonAs(fileData, `exportedMessages-${new Date().toISOString()}`); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Messages_exported_successfully') }); + }, + }); +}; diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index 4100751037ff..e70156126df0 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -4,8 +4,6 @@ import React, { useMemo } from 'react'; import { SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; -// data-qa-select - export const selectedMessageStore = new (class SelectMessageStore extends Emitter< { change: undefined; @@ -14,8 +12,20 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte > { store = new Set(); + availableMessages = new Set(); + isSelecting = false; + addAvailableMessage(mid: string): void { + this.availableMessages.add(mid); + this.emit('change'); + } + + removeAvailableMessage(mid: string): void { + this.availableMessages.delete(mid); + this.emit('change'); + } + setIsSelecting(isSelecting: boolean): void { this.isSelecting = isSelecting; this.emit('toggleIsSelecting', isSelecting); @@ -49,6 +59,10 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte return this.store.size; } + availableMessagesCount(): number { + return this.availableMessages.size; + } + clearStore(): void { const selectedMessages = this.getSelectedMessages(); this.store.clear(); @@ -61,6 +75,11 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte this.isSelecting = false; this.emit('toggleIsSelecting', false); } + + toggleAll(mids: string[]): void { + this.store = new Set([...this.store, ...mids]); + this.emit('change'); + } })(); type SelectedMessagesProviderProps = { diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 04d01d5a7c71..cecb6fd525d4 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -231,6 +231,20 @@ test.describe.serial('e2e-encryption initial setup', () => { ); await expect(poHomeChannel.content.nthMessage(0).locator('.rcx-icon--name-key')).toBeVisible(); }); + + test('should display only the download file method when exporting messages in an e2ee room', async ({ page }) => { + await page.goto('/home'); + const channelName = faker.string.uuid(); + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + await expect(poHomeChannel.tabs.exportMessages.downloadFileMethod).toBeVisible(); + }); }); test.describe.serial('e2e-encryption', () => { diff --git a/apps/meteor/tests/e2e/export-messages.spec.ts b/apps/meteor/tests/e2e/export-messages.spec.ts new file mode 100644 index 000000000000..dffb6b4d5edb --- /dev/null +++ b/apps/meteor/tests/e2e/export-messages.spec.ts @@ -0,0 +1,72 @@ +import { Users } from './fixtures/userStates'; +import { HomeChannel, Utils } from './page-objects'; +import { createTargetChannel } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('export-messages', () => { + let poHomeChannel: HomeChannel; + let poUtils: Utils; + let targetChannel: string; + + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + poUtils = new Utils(page); + + await page.goto('/home'); + }); + + test('should all export methods be available in targetChannel', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + await expect(poHomeChannel.tabs.exportMessages.sendEmailMethod).not.toBeDisabled(); + + await poHomeChannel.tabs.exportMessages.sendEmailMethod.click(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Send email')).toBeVisible(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Send file via email')).toBeVisible(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Download file')).toBeVisible(); + }); + + test('should display an error when trying to send email without filling to users or to additional emails', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello world'); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.content.getMessageByText('hello world').click(); + await poHomeChannel.tabs.exportMessages.btnSend.click(); + + await expect( + poUtils.getAlertByText('You must select one or more users or provide one or more email addresses, separated by commas'), + ).toBeVisible(); + }); + + test('should display an error when trying to send email without selecting any message', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.tabs.exportMessages.textboxAdditionalEmails.fill('mail@mail.com'); + await poHomeChannel.tabs.exportMessages.btnSend.click(); + + await expect(poUtils.getAlertByText(`You haven't selected any messages`)).toBeVisible(); + }); + + test('should be able to send messages after closing export messages', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.content.getMessageByText('hello world').click(); + await poHomeChannel.tabs.exportMessages.btnCancel.click(); + await poHomeChannel.content.sendMessage('hello export'); + + await expect(poHomeChannel.content.getMessageByText('hello export')).toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 59df066d8163..088d8dd3d647 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -391,7 +391,7 @@ export class HomeContent { } getSystemMessageByText(text: string): Locator { - return this.page.locator('[aria-roledescription="system message"]', { hasText: text }); + return this.page.locator('[role="listitem"][aria-roledescription="system message"]', { hasText: text }); } getMessageByText(text: string): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts new file mode 100644 index 000000000000..ddf78b7f4388 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts @@ -0,0 +1,33 @@ +import type { Page } from '@playwright/test'; + +export class HomeFlextabExportMessages { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get sendEmailMethod() { + return this.page.getByLabel('Send email'); + } + + get downloadFileMethod() { + return this.page.getByLabel('Download file'); + } + + getMethodByName(name: string) { + return this.page.getByRole('option', { name }); + } + + get textboxAdditionalEmails() { + return this.page.getByRole('textbox', { name: 'To additional emails' }); + } + + get btnSend() { + return this.page.locator('role=button[name="Send"]'); + } + + get btnCancel() { + return this.page.locator('role=button[name="Cancel"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts index 6e22bea99faf..a19cf72d5172 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts @@ -1,6 +1,7 @@ import type { Locator, Page } from '@playwright/test'; import { HomeFlextabChannels } from './home-flextab-channels'; +import { HomeFlextabExportMessages } from './home-flextab-exportMessages'; import { HomeFlextabMembers } from './home-flextab-members'; import { HomeFlextabNotificationPreferences } from './home-flextab-notificationPreferences'; import { HomeFlextabRoom } from './home-flextab-room'; @@ -16,12 +17,15 @@ export class HomeFlextab { readonly notificationPreferences: HomeFlextabNotificationPreferences; + readonly exportMessages: HomeFlextabExportMessages; + constructor(page: Page) { this.page = page; this.members = new HomeFlextabMembers(page); this.room = new HomeFlextabRoom(page); this.channels = new HomeFlextabChannels(page); this.notificationPreferences = new HomeFlextabNotificationPreferences(page); + this.exportMessages = new HomeFlextabExportMessages(page); } get btnTabMembers(): Locator { @@ -48,6 +52,10 @@ export class HomeFlextab { return this.page.locator('role=menuitem[name="Notifications Preferences"]'); } + get btnExportMessages(): Locator { + return this.page.locator('role=menuitem[name="Export messages"]'); + } + get btnE2EERoomSetupDisableE2E(): Locator { return this.page.locator('[data-qa-id=ToolBoxAction-key]'); } diff --git a/apps/meteor/tests/e2e/page-objects/utils.ts b/apps/meteor/tests/e2e/page-objects/utils.ts index 066c5eac153f..15fb0b88b986 100644 --- a/apps/meteor/tests/e2e/page-objects/utils.ts +++ b/apps/meteor/tests/e2e/page-objects/utils.ts @@ -26,4 +26,10 @@ export class Utils { get btnModalConfirmDelete() { return this.page.locator('.rcx-modal >> button >> text="Delete"'); } + + getAlertByText(text: string): Locator { + return this.page.locator('[role="alert"]', { + hasText: text, + }); + } } diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 0d8218149ad6..d3c5cb1eae9d 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -3272,7 +3272,6 @@ "Message_VideoRecorderEnabledDescription": "Erfordert, dass der Medientyp 'video/webm' in den \"Datei-Upload\"-Einstellungen als Medientyp akzeptiert wird", "messages": "Nachrichten", "Messages": "Nachrichten", - "Messages_selected": "Ausgewählte Nachrichten", "Messages_sent": "Nachrichten versandt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht", "Meta": "Metadaten", @@ -5506,4 +5505,4 @@ "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8b8a0b5e4b1b..e9c78cc644f9 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -26,7 +26,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} encryption keys need to be updated to give you access. Another room member needs to be online for this to happen.", "removed__username__as__role_": "removed {{username}} as {{role}}", "set__username__as__role_": "set {{username}} as {{role}}", - "sequential_message": "sequential message", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by {{username}}", "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", "Third_party_login": "Third-party login", @@ -1067,6 +1066,7 @@ "clean-channel-history_description": "Permission to Clear the history from channels", "clear": "Clear", "Clear_all_unreads_question": "Clear all unreads?", + "Clear_selection": "Clear selection", "clear_cache_now": "Clear Cache Now", "Clear_filters": "Clear filters", "clear_history": "Clear History", @@ -1813,6 +1813,7 @@ "Download": "Download", "Download_Destkop_App": "Download Desktop App", "Download_Disabled": "Download disabled", + "Download_file": "Download file", "Download_Info": "Download info", "Download_My_Data": "Download My Data (HTML)", "Download_Pending_Avatars": "Download Pending Avatars", @@ -2319,7 +2320,7 @@ "Expiration": "Expiration", "Expiration_(Days)": "Expiration (Days)", "Export_as_file": "Export as file", - "Export_Messages": "Export Messages", + "Export_Messages": "Export messages", "Export_My_Data": "Export My Data (JSON)", "expression": "Expression", "Extended": "Extended", @@ -3774,7 +3775,8 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "Messages_selected": "Messages selected", + "__count__messages_selected": "{{count}} messages selected", + "Messages_exported_successfully": "Messages exported successfully", "Messages_sent": "Messages sent", "Message_sent": "Message sent", "Message_viewed": "Message viewed", @@ -4975,8 +4977,9 @@ "Send_a_test_push_to_my_user": "Send a test push to my user", "Send_confirmation_email": "Send confirmation email", "Send_data_into_RocketChat_in_realtime": "Send data into Rocket.Chat in real-time.", - "Send_email": "Send Email", + "Send_email": "Send email", "Send_Email_SMTP_Warning": "Set up the SMTP server in email settings to enable.", + "Send_file_via_email": "Send file via email", "Send_invitation_email": "Send invitation email", "Send_invitation_email_error": "You haven't provided any valid email address.", "Send_invitation_email_info": "You can send multiple email invitations at once.", @@ -6685,6 +6688,7 @@ "Go_to_href": "Go to: {{href}}", "Anyone_can_send_new_messages": "Anyone can send new messages", "Select_messages_to_hide": "Select messages to hide", + "Select__count__messages": "Select {{count}} messages", "Name_cannot_have_special_characters": "Name cannot have spaces or special characters", "Resize": "Resize", "Zoom_out": "Zoom out", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 0f7abeb50c71..6365656bf6cc 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -3319,7 +3319,6 @@ "Message_VideoRecorderEnabledDescription": "Vaatii 'video/webm'-tiedostot hyväksytyksi mediatyypiksi 'Tiedoston lataus'-asetuksissa.", "messages": "viestit", "Messages": "Viestit", - "Messages_selected": "Valitut viestit", "Messages_sent": "Lähetetyt viestit", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Saapuvaan WebHookiin lähetetyt viestit julkaistaan tässä.", "Meta": "Meta", @@ -5719,4 +5718,4 @@ "Theme_Appearence": "Teeman ulkoasu", "Enterprise": "Yritys", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index cbf6c1a11f13..1a6a29a71cd6 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -3454,7 +3454,6 @@ "Message_VideoRecorderEnabledDescription": "'फ़ाइल अपलोड' सेटिंग्स के अंतर्गत 'वीडियो/वेबएम' फ़ाइलों को एक स्वीकृत मीडिया प्रकार होना आवश्यक है।", "messages": "संदेशों", "Messages": "संदेशों", - "Messages_selected": "संदेश चयनित", "Messages_sent": "संदेश भेजे गए", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "इनकमिंग वेबहुक पर भेजे गए संदेश यहां पोस्ट किए जाएंगे।", "Meta": "मेटा", @@ -6103,4 +6102,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index e5b68aeddbee..90b0c1d8ef82 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -3199,7 +3199,6 @@ "Message_VideoRecorderEnabledDescription": "Azt igényli, hogy a „video/webm” fájlok elfogadott médiatípus legyen a „Fájlfeltöltés” beállításaiban.", "messages": "üzenetek", "Messages": "Üzenetek", - "Messages_selected": "Üzenetek kijelölve", "Messages_sent": "Üzenetek elküldve", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "A bejövő webhorogra küldött üzenetek itt lesznek beküldve.", "Meta": "Meta", @@ -5407,4 +5406,4 @@ "Enterprise": "Vállalati", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "UpgradeToGetMore_auditing_Title": "Üzenet ellenőrzés" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index d81230c554d1..9b9f6935a254 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -2855,7 +2855,6 @@ "Message_VideoRecorderEnabledDescription": "Krever at video / webm-filer skal være en akseptert medietype i \"Filopplastings\" -innstillinger.", "messages": "meldinger", "Messages": "meldinger", - "Messages_selected": "Meldinger er valgt", "Messages_sent": "Meldinger sendt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meldinger som sendes til Incoming WebHook vil bli lagt ut her.", "Meta": "Meta", @@ -4559,4 +4558,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/no.i18n.json b/packages/i18n/src/locales/no.i18n.json index 3baf4ebc7215..952b0ca9b711 100644 --- a/packages/i18n/src/locales/no.i18n.json +++ b/packages/i18n/src/locales/no.i18n.json @@ -2855,7 +2855,6 @@ "Message_VideoRecorderEnabledDescription": "Krever at video / webm-filer skal være en akseptert medietype i \"Filopplastings\" -innstillinger.", "messages": "meldinger", "Messages": "meldinger", - "Messages_selected": "Meldinger er valgt", "Messages_sent": "Meldinger sendt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meldinger som sendes til Incoming WebHook vil bli lagt ut her.", "Meta": "Meta", @@ -4561,4 +4560,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 3a58a1a4c4eb..f43c37775fa3 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -26,7 +26,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} klucze szyfrowania muszą zostać zaktualizowane, aby umożliwić dostęp. Aby tak się stało, inny członek pokoju musi być online.", "removed__username__as__role_": "usunięto {{username}} jako {{role}}", "set__username__as__role_": "ustaw {{username}} jako {{role}}", - "sequential_message": "komunikat sekwencyjny", "This_room_encryption_has_been_enabled_by__username_": "Użytkownik {{username}} włączył szyfrowanie w tym pokoju", "This_room_encryption_has_been_disabled_by__username_": "Użytkownik {{username}} wyłączył szyfrowanie w tym pokoju", "Third_party_login": "Logowanie przez stronę trzecią", @@ -3204,7 +3203,6 @@ "Message_VideoRecorderEnabledDescription": "Wymaga plików \"wideo / webm\", aby były akceptowanym typem mediów w ustawieniach \"Przesyłanie pliku\".", "messages": "Wiadomości", "Messages": "Wiadomości", - "Messages_selected": "Wybrane wiadomości", "Messages_sent": "Wiadomości wysłane", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Wiadomości, które zostaną przesłane przez WebHook będą publikowane tutaj.", "Meta": "Meta", @@ -5405,4 +5403,4 @@ "Broadcast_hint_enabled": "Tylko właściciele {{roomType}} mogą pisać nowe wiadomości, ale każdy może odpowiadać w wątku", "Anyone_can_send_new_messages": "Każdy może wysyłać nowe wiadomości", "Select_messages_to_hide": "Wybierz wiadomości do ukrycia" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json index 39a5595b0525..3ac677c4dcbd 100644 --- a/packages/i18n/src/locales/se.i18n.json +++ b/packages/i18n/src/locales/se.i18n.json @@ -24,7 +24,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} encryption keys need to be updated to give you access. Another room member needs to be online for this to happen.", "removed__username__as__role_": "removed {{username}} as {{role}}", "set__username__as__role_": "set {{username}} as {{role}}", - "sequential_message": "sequential message", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by {{username}}", "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", "Third_party_login": "Third-party login", @@ -3679,7 +3678,6 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "Messages_selected": "Messages selected", "Messages_sent": "Messages sent", "Message_sent": "Message sent", "Message_viewed": "Message viewed", @@ -6576,4 +6574,4 @@ "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", "Show_discussions_description": "Show team discussions in second sidebar" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 8615418f947c..69b59f03c321 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -3324,7 +3324,6 @@ "Message_VideoRecorderEnabledDescription": "Kräver \"video/webm\"-filer för att vara en accepterad medietyp inom inställningarna \"Filuppladdning\".", "messages": "Meddelanden", "Messages": "Meddelanden", - "Messages_selected": "Valda meddelanden", "Messages_sent": "Skickade meddelanden", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meddelanden som skickas till inkommande WebHook kommer att publiceras här.", "Meta": "Meta", @@ -5721,4 +5720,4 @@ "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx index deab14822f83..2d5c87b57d34 100644 --- a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx +++ b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx @@ -1,14 +1,13 @@ import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement, ReactNode } from 'react'; +import type { ComponentProps } from 'react'; import { forwardRef } from 'react'; -const MessageFooterCalloutContent = forwardRef< - HTMLButtonElement, - { - children: ReactNode; - } ->(function MessageFooterCalloutContent(props, ref): ReactElement { - return ; -}); +type MessageFooterCalloutContentProps = ComponentProps; + +const MessageFooterCalloutContent = forwardRef( + function MessageFooterCalloutContent(props, ref) { + return ; + }, +); export default MessageFooterCalloutContent; From 278af14a55da31111af72217f7ee013604d5a134 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Dec 2024 16:09:00 -0300 Subject: [PATCH 3/5] regression: fix error observe (#34222) --- .../client/providers/OmnichannelProvider.tsx | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index aeb83530e059..0080c340b5d5 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -52,6 +52,8 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { OmnichannelSortingMechanismSettingType.Timestamp, ); + const lastQueueSize = useRef(0); + const loggerRef = useRef(new ClientLogger('OmnichannelProvider')); const hasAccess = usePermission('view-l-room'); const canViewOmnichannelQueue = usePermission('view-livechat-queue'); @@ -151,20 +153,11 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { ); useEffect(() => { - const observer = LivechatInquiry.find( - { status: LivechatInquiryStatus.QUEUED }, - { - sort: getOmniChatSortQuery(omnichannelSortingMechanism), - limit: omnichannelPoolMaxIncoming, - }, - ).observe({ - added: (_inquiry) => { - KonchatNotification.newRoom(); - }, - }); - - return () => observer.stop(); - }, [omnichannelPoolMaxIncoming, omnichannelSortingMechanism]); + if (lastQueueSize.current < (queue?.length ?? 0)) { + KonchatNotification.newRoom(); + } + lastQueueSize.current = queue?.length ?? 0; + }, [queue?.length]); useOmnichannelContinuousSoundNotification(queue ?? []); From ed02a335e45797ba142d2592c417e7a0a4ab59d5 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 19 Dec 2024 17:01:35 -0300 Subject: [PATCH 4/5] fix: race condition in livechat popout mode cross origin (#34158) --- .changeset/new-mails-add.md | 5 +++++ .../src/components/Screen/ScreenProvider.tsx | 18 +++++++++++++++--- packages/livechat/src/lib/hooks.ts | 4 ++-- packages/livechat/src/widget.ts | 13 +++---------- 4 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 .changeset/new-mails-add.md diff --git a/.changeset/new-mails-add.md b/.changeset/new-mails-add.md new file mode 100644 index 000000000000..87446ac8b4c1 --- /dev/null +++ b/.changeset/new-mails-add.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes issue that caused different sessions when opening a livechat popover in cross domain diff --git a/packages/livechat/src/components/Screen/ScreenProvider.tsx b/packages/livechat/src/components/Screen/ScreenProvider.tsx index aa1dd61c59be..2bccafd60c6f 100644 --- a/packages/livechat/src/components/Screen/ScreenProvider.tsx +++ b/packages/livechat/src/components/Screen/ScreenProvider.tsx @@ -4,6 +4,7 @@ import { useCallback, useContext, useEffect, useState } from 'preact/hooks'; import { parse } from 'query-string'; import { isActiveSession } from '../../helpers/isActiveSession'; +import { createOrUpdateGuest, evaluateChangesAndLoadConfigByFields } from '../../lib/hooks'; import { loadConfig } from '../../lib/main'; import { parentCall } from '../../lib/parentCall'; import { loadMessages } from '../../lib/room'; @@ -76,7 +77,7 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => { } = useContext(StoreContext); const { department, name, email } = iframe.guest || {}; const { color, position: configPosition, background } = config.theme || {}; - const { livechatLogo, hideWatermark = false } = config.settings || {}; + const { livechatLogo, hideWatermark = false, registrationForm } = config.settings || {}; const { color: customColor, @@ -137,15 +138,26 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => { const dismissNotification = () => !isActiveSession(); - const checkPoppedOutWindow = useCallback(() => { + const checkPoppedOutWindow = useCallback(async () => { // Checking if the window is poppedOut and setting parent minimized if yes for the restore purpose const poppedOut = parse(window.location.search).mode === 'popout'; + const { token = '' } = parse(window.location.search); setPopedOut(poppedOut); if (poppedOut) { dispatch({ minimized: false, undocked: true }); } - }, [dispatch]); + + if (token && typeof token === 'string') { + if (registrationForm && !name && !email) { + dispatch({ token }); + return; + } + await evaluateChangesAndLoadConfigByFields(async () => { + await createOrUpdateGuest({ token }); + }); + } + }, [dispatch, email, name, registrationForm]); useEffect(() => { checkPoppedOutWindow(); diff --git a/packages/livechat/src/lib/hooks.ts b/packages/livechat/src/lib/hooks.ts index e1a980fe65e3..cdfde40f16e5 100644 --- a/packages/livechat/src/lib/hooks.ts +++ b/packages/livechat/src/lib/hooks.ts @@ -11,7 +11,7 @@ import { createToken } from './random'; import { loadMessages } from './room'; import Triggers from './triggers'; -const evaluateChangesAndLoadConfigByFields = async (fn: () => Promise) => { +export const evaluateChangesAndLoadConfigByFields = async (fn: () => Promise) => { const oldStore = JSON.parse( JSON.stringify({ user: store.state.user || {}, @@ -42,7 +42,7 @@ const evaluateChangesAndLoadConfigByFields = async (fn: () => Promise) => } }; -const createOrUpdateGuest = async (guest: StoreState['guest']) => { +export const createOrUpdateGuest = async (guest: StoreState['guest']) => { if (!guest) { return; } diff --git a/packages/livechat/src/widget.ts b/packages/livechat/src/widget.ts index 234d61f87096..ca46ab944913 100644 --- a/packages/livechat/src/widget.ts +++ b/packages/livechat/src/widget.ts @@ -489,20 +489,13 @@ const api: InternalWidgetAPI = { if (!config.url) { throw new Error('Config.url is not set!'); } + const urlToken = token && `&token=${token}`; + api.popup = window.open( - `${config.url}${config.url.lastIndexOf('?') > -1 ? '&' : '?'}mode=popout`, + `${config.url}${config.url.lastIndexOf('?') > -1 ? '&' : '?'}mode=popout${urlToken}`, 'livechat-popout', `width=${WIDGET_OPEN_WIDTH}, height=${widgetHeight}, toolbars=no`, ); - - const data = { - src: 'rocketchat', - fn: 'setGuestToken', - args: [token], - }; - - api.popup?.postMessage(data, '*'); - api.popup?.focus(); }, removeWidget() { From 76f6239ff1a9f34f163c03c140c4ceba62563b4e Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 19 Dec 2024 18:21:34 -0300 Subject: [PATCH 5/5] fix(apps): runtime orchestration fixes (#34205) --- .changeset/blue-items-raise.md | 22 +++++++++++++++++++ .changeset/gold-comics-taste.md | 22 +++++++++++++++++++ .changeset/proud-planets-applaud.md | 22 +++++++++++++++++++ .../server/lib/getAppsStatistics.ts | 8 +++---- packages/apps-engine/deno-runtime/deno.lock | 15 ++----------- .../deno-runtime/lib/metricsCollector.ts | 15 +++++++++++-- .../apps-engine/deno-runtime/lib/parseArgs.ts | 11 ++++++++++ packages/apps-engine/deno-runtime/main.ts | 5 ++++- packages/apps-engine/src/server/AppManager.ts | 2 +- packages/apps-engine/src/server/ProxiedApp.ts | 4 ++-- .../runtime/deno/AppsEngineDenoRuntime.ts | 8 +++++++ .../server/runtime/deno/LivenessManager.ts | 13 +++++++++-- 12 files changed, 121 insertions(+), 26 deletions(-) create mode 100644 .changeset/blue-items-raise.md create mode 100644 .changeset/gold-comics-taste.md create mode 100644 .changeset/proud-planets-applaud.md create mode 100644 packages/apps-engine/deno-runtime/lib/parseArgs.ts diff --git a/.changeset/blue-items-raise.md b/.changeset/blue-items-raise.md new file mode 100644 index 000000000000..7b5592a9efb2 --- /dev/null +++ b/.changeset/blue-items-raise.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/instance-status': patch +'@rocket.chat/ui-theming': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/uikit-playground': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/rest-typings': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-composer': patch +'@rocket.chat/ui-contexts': patch +'@rocket.chat/gazzodown': patch +'@rocket.chat/ui-avatar': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/livechat': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes an error where the engine would not retry a subprocess restart if the last attempt failed diff --git a/.changeset/gold-comics-taste.md b/.changeset/gold-comics-taste.md new file mode 100644 index 000000000000..c9f864a0ffb5 --- /dev/null +++ b/.changeset/gold-comics-taste.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/instance-status': patch +'@rocket.chat/ui-theming': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/uikit-playground': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/rest-typings': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-composer': patch +'@rocket.chat/ui-contexts': patch +'@rocket.chat/gazzodown': patch +'@rocket.chat/ui-avatar': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/livechat': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes error propagation when trying to get the status of apps in some cases diff --git a/.changeset/proud-planets-applaud.md b/.changeset/proud-planets-applaud.md new file mode 100644 index 000000000000..7f692f780271 --- /dev/null +++ b/.changeset/proud-planets-applaud.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/instance-status': patch +'@rocket.chat/ui-theming': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/uikit-playground': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/rest-typings': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-composer': patch +'@rocket.chat/ui-contexts': patch +'@rocket.chat/gazzodown': patch +'@rocket.chat/ui-avatar': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/livechat': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes wrong data being reported to total failed apps metrics and statistics diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts index 40c8e1946e36..90e05d192356 100644 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts @@ -41,7 +41,7 @@ async function _getAppsStatistics(): Promise { totalInstalled++; const status = await app.getStatus(); - const storageItem = await app.getStorageItem(); + const storageItem = app.getStorageItem(); if (storageItem.installationSource === AppInstallationSource.PRIVATE) { totalPrivateApps++; @@ -51,12 +51,10 @@ async function _getAppsStatistics(): Promise { } } - if (status === AppStatus.MANUALLY_DISABLED) { - totalFailed++; - } - if (AppStatusUtils.isEnabled(status)) { totalActive++; + } else if (status !== AppStatus.MANUALLY_DISABLED) { + totalFailed++; } }), ); diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 86cebf98f63a..1154e7709f11 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -90,18 +90,7 @@ "https://deno.land/std@0.203.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", - "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038" - }, - "workspace": { - "dependencies": [ - "npm:@msgpack/msgpack@3.0.0-beta2", - "npm:@rocket.chat/ui-kit@^0.31.22", - "npm:acorn-walk@8.2.0", - "npm:acorn@8.10.0", - "npm:astring@1.8.6", - "npm:jsonrpc-lite@2.2.0", - "npm:stack-trace@0.0.10", - "npm:uuid@8.3.2" - ] + "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" } } diff --git a/packages/apps-engine/deno-runtime/lib/metricsCollector.ts b/packages/apps-engine/deno-runtime/lib/metricsCollector.ts index c257b6c8a35b..273ef2463d59 100644 --- a/packages/apps-engine/deno-runtime/lib/metricsCollector.ts +++ b/packages/apps-engine/deno-runtime/lib/metricsCollector.ts @@ -3,6 +3,7 @@ import { Queue } from "./messenger.ts"; export function collectMetrics() { return { + pid: Deno.pid, queueSize: Queue.getCurrentSize(), } }; @@ -15,6 +16,16 @@ export async function sendMetrics() { await writeAll(Deno.stderr, encoder.encode(JSON.stringify(metrics))); } -export function startMetricsReport() { - setInterval(sendMetrics, 5000); +let intervalId: number; + +export function startMetricsReport(frequencyInMs = 5000) { + if (intervalId) { + throw new Error('There is already an active metrics report'); + } + + intervalId = setInterval(sendMetrics, frequencyInMs); +} + +export function abortMetricsReport() { + clearInterval(intervalId); } diff --git a/packages/apps-engine/deno-runtime/lib/parseArgs.ts b/packages/apps-engine/deno-runtime/lib/parseArgs.ts new file mode 100644 index 000000000000..10c59cbca3a7 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/parseArgs.ts @@ -0,0 +1,11 @@ +import { parseArgs as $parseArgs } from "https://jsr.io/@std/cli/1.0.9/parse_args.ts"; + +export type ParsedArgs = { + subprocess: string; + spawnId: number; + metricsReportFrequencyInMs?: number; +} + +export function parseArgs(args: string[]): ParsedArgs { + return $parseArgs(args); +} diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts index 3983c8d52407..596128952168 100644 --- a/packages/apps-engine/deno-runtime/main.ts +++ b/packages/apps-engine/deno-runtime/main.ts @@ -23,6 +23,7 @@ import handleApp from './handlers/app/handler.ts'; import handleScheduler from './handlers/scheduler-handler.ts'; import registerErrorListeners from './error-handlers.ts'; import { startMetricsReport } from "./lib/metricsCollector.ts"; +import { parseArgs } from "./lib/parseArgs.ts"; type Handlers = { app: typeof handleApp; @@ -128,8 +129,10 @@ async function main() { } } +const mainArgs = parseArgs(Deno.args); + registerErrorListeners(); main(); -startMetricsReport(); +startMetricsReport(mainArgs.metricsReportFrequencyInMs); diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index 0bb930b723a1..0ea7e998e995 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -271,7 +271,7 @@ export class AppManager { const prl = new ProxiedApp(this, item, { // Maybe we should have an "EmptyRuntime" class for this? getStatus() { - return AppStatus.COMPILER_ERROR_DISABLED; + return Promise.resolve(AppStatus.COMPILER_ERROR_DISABLED); }, } as unknown as DenoRuntimeSubprocessController); diff --git a/packages/apps-engine/src/server/ProxiedApp.ts b/packages/apps-engine/src/server/ProxiedApp.ts index 4307f9c9fc93..7810ab362422 100644 --- a/packages/apps-engine/src/server/ProxiedApp.ts +++ b/packages/apps-engine/src/server/ProxiedApp.ts @@ -1,5 +1,5 @@ import type { AppManager } from './AppManager'; -import type { AppStatus } from '../definition/AppStatus'; +import { AppStatus } from '../definition/AppStatus'; import { AppsEngineException } from '../definition/exceptions'; import type { IAppAuthorInfo, IAppInfo } from '../definition/metadata'; import { AppMethod } from '../definition/metadata'; @@ -79,7 +79,7 @@ export class ProxiedApp { } public async getStatus(): Promise { - return this.appRuntime.getStatus(); + return this.appRuntime.getStatus().catch(() => AppStatus.UNKNOWN); } public async setStatus(status: AppStatus, silent?: boolean): Promise { diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts index 983b0a9343d3..fec78b835984 100644 --- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -88,6 +88,11 @@ export class DenoRuntimeSubprocessController extends EventEmitter { private state: 'uninitialized' | 'ready' | 'invalid' | 'restarting' | 'unknown' | 'stopped'; + /** + * Incremental id that keeps track of how many times we've spawned a process for this app + */ + private spawnId = 0; + private readonly debug: debug.Debugger; private readonly options = { @@ -149,6 +154,8 @@ export class DenoRuntimeSubprocessController extends EventEmitter { denoWrapperPath, '--subprocess', this.appPackage.info.id, + '--spawnId', + String(this.spawnId++), ]; // If the app doesn't request any permissions, it gets the default set of permissions, which includes "networking" @@ -296,6 +303,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter { logger.info('Successfully restarted app subprocess'); } catch (e) { logger.error("Failed to restart app's subprocess", { error: e.message || e }); + throw e; } finally { await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); } diff --git a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts b/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts index b4c8dfb5d520..3f363c5402f1 100644 --- a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts +++ b/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts @@ -7,10 +7,11 @@ import type { ProcessMessenger } from './ProcessMessenger'; const COMMAND_PING = '_zPING'; const defaultOptions: LivenessManager['options'] = { - pingRequestTimeout: 10000, + pingRequestTimeout: 1000, pingFrequencyInMS: 10000, consecutiveTimeoutLimit: 4, maxRestarts: Infinity, + restartAttemptDelayInMS: 1000, }; /** @@ -36,6 +37,9 @@ export class LivenessManager { // Limit of times we can try to restart a process maxRestarts: number; + + // Time to delay the next restart attempt after a failed one + restartAttemptDelayInMS: number; }; private subprocess: ChildProcess; @@ -198,7 +202,12 @@ export class LivenessManager { pid: this.subprocess.pid, }); - await this.controller.restartApp(); + try { + await this.controller.restartApp(); + } catch (e) { + this.debug('Restart attempt failed. Retrying in %dms', this.options.restartAttemptDelayInMS); + setTimeout(() => this.restartProcess('Failed restart attempt'), this.options.restartAttemptDelayInMS); + } this.pingTimeoutConsecutiveCount = 0; this.restartCount++;