Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Export selected room messages as JSON file #34076

Merged
merged 18 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/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 };
};
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
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
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;
Loading
Loading