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 10 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
Expand Up @@ -44,6 +44,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 +70,16 @@ export const useCountSelected = (): number => {

return useSyncExternalStore(subscribe, getSnapshot);
};

export const useSelectedMessages = (): string[] => {
const { selectedMessageStore } = useContext(SelectedMessageContext);

const subscribe = useCallback(
(callback: () => void): (() => void) => selectedMessageStore.on('change', callback),
[selectedMessageStore],
);

const getSnapshot = () => selectedMessageStore.getSelectedMessages();

return useSyncExternalStore(subscribe, getSnapshot);
};
3 changes: 2 additions & 1 deletion apps/meteor/client/views/room/body/RoomBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const RoomBody = (): ReactElement => {

const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom();

const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef);
const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef);

const { wrapperRef: leaderBannerWrapperRef, hideLeaderHeader, innerRef: leaderBannerInnerRef } = useLeaderBanner();

Expand Down Expand Up @@ -313,6 +313,7 @@ const RoomBody = (): ReactElement => {
onNavigateToPreviousMessage={handleNavigateToPreviousMessage}
onNavigateToNextMessage={handleNavigateToNextMessage}
onUploadFiles={handleUploadFiles}
onGetMore={handleGetMore}
// TODO: send previewUrls param
// previewUrls={}
/>
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/client/views/room/body/RoomBodyV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const RoomBody = (): ReactElement => {

const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom();

const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef);
const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef);

const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection();

Expand Down Expand Up @@ -285,6 +285,7 @@ const RoomBody = (): ReactElement => {
onNavigateToPreviousMessage={handleNavigateToPreviousMessage}
onNavigateToNextMessage={handleNavigateToNextMessage}
onUploadFiles={handleUploadFiles}
onGetMore={handleGetMore}
// TODO: send previewUrls param
// previewUrls={}
/>
Expand Down
13 changes: 12 additions & 1 deletion apps/meteor/client/views/room/body/hooks/useGetMore.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import type { MutableRefObject } from 'react';
import { useCallback } from 'react';
import { useCallback, useRef } 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<boolean>) => {
const ref: MutableRefObject<HTMLElement | null> = useRef(null);
const handleToggleAll = useToggleSelectAll();

const handleGetMore = () => {
ref.current?.scrollTo({ top: 0, behavior: 'smooth' });
handleToggleAll();
};
dougfabris marked this conversation as resolved.
Show resolved Hide resolved

return {
handleGetMore,
innerRef: useCallback(
(wrapper: HTMLElement | null) => {
if (!wrapper) {
return;
}

ref.current = wrapper;
let lastScrollTopRef = 0;

wrapper.addEventListener(
Expand Down
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;
onGetMore?: () => 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, { useContext } from 'react';
import { useTranslation } from 'react-i18next';

import type { ComposerMessageProps } from './ComposerMessage';
import { useCountSelected, useClearSelection, SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext';

const ComposerSelectMessages = ({ onGetMore }: ComposerMessageProps): ReactElement => {
dougfabris marked this conversation as resolved.
Show resolved Hide resolved
const { t } = useTranslation();

const { selectedMessageStore } = useContext(SelectedMessageContext);
const clearSelection = useClearSelection();
const countSelected = useCountSelected();
const countAvailable = selectedMessageStore.availableMessages.size;
const noMessagesAvailable = countAvailable <= countSelected;

return (
<MessageFooterCallout>
<MessageFooterCalloutContent textAlign='left'>{t('number_messages_selected', { count: countSelected })}</MessageFooterCalloutContent>
<ButtonGroup>
<Button small disabled={countSelected === 0} onClick={clearSelection}>
{t('Clear_selection')}
</Button>
<Button icon='arrow-up' small primary disabled={noMessagesAvailable} onClick={onGetMore}>
{t('Select_number_messages', { count: countAvailable - countSelected })}
</Button>
</ButtonGroup>
</MessageFooterCallout>
);
};

export default ComposerSelectMessages;
Loading
Loading