Skip to content

Commit

Permalink
feat: Export selected room messages as JSON file (#34076)
Browse files Browse the repository at this point in the history
  • Loading branch information
dougfabris authored Dec 19, 2024
1 parent 082d3ce commit 2d41274
Show file tree
Hide file tree
Showing 35 changed files with 647 additions and 391 deletions.
7 changes: 7 additions & 0 deletions .changeset/shaggy-bulldogs-beg.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion apps/meteor/app/ui/client/lib/ChatMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/lib/chats/ChatAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/views/room/Header/icons/Encrypted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? <HeaderState title={t('Encrypted')} icon='key' color={colors.g500} tiny /> : null;
return e2eEnabled && room?.encrypted ? <HeaderState title={t('Encrypted')} icon='key' color={colors.g500} /> : null;
};

export default memo(Encrypted);
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 => {
Expand All @@ -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);

Expand All @@ -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);
};
5 changes: 4 additions & 1 deletion apps/meteor/client/views/room/body/RoomBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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, {
Expand All @@ -133,7 +135,7 @@ const RoomBody = (): ReactElement => {
leaderBannerInnerRef,
unreadBarInnerRef,
getMoreInnerRef,

selectAndScrollRef,
messageListRef,
);

Expand Down Expand Up @@ -313,6 +315,7 @@ const RoomBody = (): ReactElement => {
onNavigateToPreviousMessage={handleNavigateToPreviousMessage}
onNavigateToNextMessage={handleNavigateToNextMessage}
onUploadFiles={handleUploadFiles}
onClickSelectAll={selectAllAndScrollToTop}
// TODO: send previewUrls param
// previewUrls={}
/>
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/client/views/room/body/RoomBodyV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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, {
Expand All @@ -128,7 +130,7 @@ const RoomBody = (): ReactElement => {
sectionScrollRef,
unreadBarInnerRef,
getMoreInnerRef,

selectAndScrollRef,
messageListRef,
);

Expand Down Expand Up @@ -285,6 +287,7 @@ const RoomBody = (): ReactElement => {
onNavigateToPreviousMessage={handleNavigateToPreviousMessage}
onNavigateToNextMessage={handleNavigateToNextMessage}
onUploadFiles={handleUploadFiles}
onClickSelectAll={selectAllAndScrollToTop}
// TODO: send previewUrls param
// previewUrls={}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useRef } from 'react';

import { useToggleSelectAll } from '../../MessageList/contexts/SelectedMessagesContext';

export const useSelectAllAndScrollToTop = () => {
const ref = useRef<HTMLElement>(null);
const handleToggleAll = useToggleSelectAll();

const selectAllAndScrollToTop = () => {
ref.current?.scrollTo({ top: 0, behavior: 'smooth' });
handleToggleAll();
};

return { innerRef: ref, selectAllAndScrollToTop };
};
7 changes: 7 additions & 0 deletions apps/meteor/client/views/room/composer/ComposerContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ 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';
import { useMessageComposerIsArchived } from './hooks/useMessageComposerIsArchived';
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();
Expand All @@ -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);
Expand Down Expand Up @@ -74,6 +77,10 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE
return <ComposerBlocked />;
}

if (isSelectingMessages) {
return <ComposerSelectMessages {...props} />;
}

return (
<>
{children}
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/client/views/room/composer/ComposerMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
34 changes: 34 additions & 0 deletions apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<MessageFooterCallout>
<MessageFooterCalloutContent textAlign='left'>
{t('__count__messages_selected', { count: countSelected })}
</MessageFooterCalloutContent>
<ButtonGroup>
<Button small disabled={countSelected === 0} onClick={clearSelection}>
{t('Clear_selection')}
</Button>
<Button icon='arrow-up' small primary disabled={countAvailable === 0} onClick={onClickSelectAll}>
{t('Select__count__messages', { count: countAvailable })}
</Button>
</ButtonGroup>
</MessageFooterCallout>
);
};

export default ComposerSelectMessages;
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading

0 comments on commit 2d41274

Please sign in to comment.