From 4e000f128f9d07aa57329c256edc2c3b0edd56d5 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 11 Dec 2024 18:10:08 -0300 Subject: [PATCH] fix: review Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .../contexts/SelectedMessagesContext.tsx | 20 ++++++-- .../client/views/room/body/RoomBody.tsx | 8 ++-- .../client/views/room/body/RoomBodyV2.tsx | 8 ++-- .../views/room/body/hooks/useGetMore.ts | 13 +---- .../body/hooks/useSelectAllAndScrollToTop.ts | 15 ++++++ .../views/room/composer/ComposerMessage.tsx | 2 +- .../room/composer/ComposerSelectMessages.tsx | 18 +++---- .../ExportMessages/ExportMessages.tsx | 10 ++-- .../providers/SelectedMessagesProvider.tsx | 47 +++++++------------ packages/i18n/src/locales/en.i18n.json | 4 +- 10 files changed, 76 insertions(+), 69 deletions(-) create mode 100644 apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts diff --git a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx index 5cfbc3352274..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 => { @@ -71,7 +83,7 @@ export const useCountSelected = (): number => { return useSyncExternalStore(subscribe, getSnapshot); }; -export const useSelectedMessages = (): string[] => { +export const useAvailableMessagesCount = () => { const { selectedMessageStore } = useContext(SelectedMessageContext); const subscribe = useCallback( @@ -79,7 +91,7 @@ export const useSelectedMessages = (): string[] => { [selectedMessageStore], ); - const getSnapshot = () => selectedMessageStore.getSelectedMessages(); + 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 e8352c93783c..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 => { @@ -102,7 +103,7 @@ const RoomBody = (): ReactElement => { const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef); + const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); const { wrapperRef: leaderBannerWrapperRef, hideLeaderHeader, innerRef: leaderBannerInnerRef } = useLeaderBanner(); @@ -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,7 +315,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} - onGetMore={handleGetMore} + 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 7de9eb92113a..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 => { @@ -97,7 +98,7 @@ const RoomBody = (): ReactElement => { const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef); + const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection(); @@ -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,7 +287,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} - onGetMore={handleGetMore} + onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index 5fd4775603e6..c2f182e5131f 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,28 +1,17 @@ import type { MutableRefObject } from 'react'; -import { useCallback, useRef } from 'react'; +import { useCallback } from 'react'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; -import { useToggleSelectAll } from '../../MessageList/contexts/SelectedMessagesContext'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { - const ref: MutableRefObject = useRef(null); - const handleToggleAll = useToggleSelectAll(); - - const handleGetMore = () => { - ref.current?.scrollTo({ top: 0, behavior: 'smooth' }); - handleToggleAll(); - }; - return { - handleGetMore, innerRef: useCallback( (wrapper: HTMLElement | null) => { if (!wrapper) { return; } - ref.current = wrapper; let lastScrollTopRef = 0; wrapper.addEventListener( 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/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 47f9f21bf67e..a5fd473f788b 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -22,7 +22,7 @@ export type ComposerMessageProps = { onNavigateToNextMessage?: () => void; onNavigateToPreviousMessage?: () => void; onUploadFiles?: (files: readonly File[]) => void; - onGetMore?: () => 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 index 1c108df6119f..2a77e250e5ba 100644 --- a/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx +++ b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx @@ -1,30 +1,30 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { MessageFooterCallout, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; import type { ReactElement } from 'react'; -import React, { useContext } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import type { ComposerMessageProps } from './ComposerMessage'; -import { useCountSelected, useClearSelection, SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; +import { useCountSelected, useClearSelection, useAvailableMessagesCount } from '../MessageList/contexts/SelectedMessagesContext'; -const ComposerSelectMessages = ({ onGetMore }: ComposerMessageProps): ReactElement => { +const ComposerSelectMessages = ({ onClickSelectAll }: ComposerMessageProps): ReactElement => { const { t } = useTranslation(); - const { selectedMessageStore } = useContext(SelectedMessageContext); const clearSelection = useClearSelection(); const countSelected = useCountSelected(); - const countAvailable = selectedMessageStore.availableMessages.size; - const noMessagesAvailable = countAvailable <= countSelected; + const countAvailable = useAvailableMessagesCount(); return ( - {t('number_messages_selected', { count: countSelected })} + + {t('__count__messages_selected', { count: countSelected })} + - diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index 96a53675d78a..8736c8a1bec3 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -32,7 +32,7 @@ import { } from '../../../../components/Contextualbar'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import { SelectedMessageContext, useSelectedMessages } from '../../MessageList/contexts/SelectedMessagesContext'; +import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; import { useRoom } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; @@ -102,7 +102,7 @@ const ExportMessages = () => { const downloadExportMutation = useDownloadExportMutation(); const { selectedMessageStore } = useContext(SelectedMessageContext); - const messages = useSelectedMessages(); + const messageCount = useCountSelected(); const { type, toUsers } = watch(); @@ -125,10 +125,12 @@ const ExportMessages = () => { setValue('format', 'json'); } - setValue('messagesCount', messages.length); - }, [type, setValue, messages.length]); + 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, diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index 5687f83e5e3b..e70156126df0 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -1,12 +1,8 @@ import { Emitter } from '@rocket.chat/emitter'; import type { ReactNode } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; -import { useMessages } from '../MessageList/hooks/useMessages'; -import { useRoom } from '../contexts/RoomContext'; - -// data-qa-select export const selectedMessageStore = new (class SelectMessageStore extends Emitter< { @@ -16,17 +12,18 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte > { store = new Set(); - private storeArray = Array.from(this.store); - availableMessages = new Set(); isSelecting = false; - constructor() { - super(); - this.on('change', () => { - this.storeArray = Array.from(this.store); - }); + 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 { @@ -43,7 +40,7 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte } getSelectedMessages(): string[] { - return this.storeArray; + return Array.from(this.store); } toggle(mid: string): void { @@ -58,20 +55,14 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte this.emit('change'); } - select(mid: string): void { - if (this.store.has(mid)) { - return; - } - - this.store.add(mid); - this.emit(mid, true); - this.emit('change'); - } - count(): number { return this.store.size; } + availableMessagesCount(): number { + return this.availableMessages.size; + } + clearStore(): void { const selectedMessages = this.getSelectedMessages(); this.store.clear(); @@ -86,7 +77,8 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte } toggleAll(mids: string[]): void { - mids.forEach((mid) => this.select(mid)); + this.store = new Set([...this.store, ...mids]); + this.emit('change'); } })(); @@ -95,13 +87,6 @@ type SelectedMessagesProviderProps = { }; export const SelectedMessagesProvider = ({ children }: SelectedMessagesProviderProps) => { - const room = useRoom(); - const messages = useMessages({ rid: room._id }); - - useEffect(() => { - selectedMessageStore.availableMessages = new Set(messages.map((message) => message._id)); - }, [messages]); - const value = useMemo( () => ({ selectedMessageStore, diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index ec8deee584b3..09286ad00c02 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3775,7 +3775,7 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "number_messages_selected": "{{count}} messages selected", + "__count__messages_selected": "{{count}} messages selected", "Messages_exported_successfully": "Messages exported successfully", "Messages_sent": "Messages sent", "Message_sent": "Message sent", @@ -6688,7 +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_number_messages": "Select {{count}} messages", + "Select__count__messages": "Select {{count}} messages", "Name_cannot_have_special_characters": "Name cannot have spaces or special characters", "Resize": "Resize", "Zoom_out": "Zoom out",