refactor: Simplify UploadsAPI usage across components (#39672)

Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>
pull/39995/head
Douglas Fabris 2 months ago committed by GitHub
parent f524f9e195
commit 9db6725198
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts
  2. 11
      apps/meteor/app/ui/client/lib/ChatMessages.ts
  3. 14
      apps/meteor/client/lib/chats/ChatAPI.ts
  4. 16
      apps/meteor/client/lib/chats/flows/processMessageUploads.ts
  5. 4
      apps/meteor/client/lib/chats/flows/processTooLongMessage.ts
  6. 5
      apps/meteor/client/lib/chats/flows/sendMessage.ts
  7. 9
      apps/meteor/client/lib/chats/flows/uploadFiles.ts
  8. 23
      apps/meteor/client/lib/chats/uploads.ts
  9. 6
      apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx
  10. 6
      apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx
  11. 4
      apps/meteor/client/views/room/body/RoomBody.tsx
  12. 54
      apps/meteor/client/views/room/body/hooks/useFileUpload.ts
  13. 7
      apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts
  14. 63
      apps/meteor/client/views/room/composer/ComposerUserActionIndicator/ComposerUserActionIndicator.tsx
  15. 30
      apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx
  16. 5
      apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx
  17. 7
      apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts
  18. 5
      apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx
  19. 28
      apps/meteor/client/views/room/composer/messageBox/MessageComposerFiles.tsx
  20. 2
      apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx

@ -8,6 +8,7 @@ import { limitQuoteChain } from './limitQuoteChain';
import type { FormattingButton } from './messageBoxFormatting';
import { formattingButtons } from './messageBoxFormatting';
import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI';
import { createUploadsAPI } from '../../../../client/lib/chats/uploads';
import { withDebouncing } from '../../../../lib/utils/highOrderFunctions';
export const createComposerAPI = (
@ -15,6 +16,7 @@ export const createComposerAPI = (
storageID: string,
quoteChainLimit: number,
composerRef: RefObject<HTMLElement>,
{ rid, tmid }: { rid: string; tmid?: string },
): ComposerAPI => {
const triggerEvent = (input: HTMLTextAreaElement, evt: string): void => {
const event = new Event(evt, { bubbles: true });
@ -351,5 +353,6 @@ export const createComposerAPI = (
formatters,
isMicrophoneDenied,
setIsMicrophoneDenied,
uploads: createUploadsAPI({ rid, tmid }),
};
};

@ -4,7 +4,7 @@ import type { IActionManager } from '@rocket.chat/ui-contexts';
import { CurrentEditingMessage } from './CurrentEditingMessage';
import { UserAction } from './UserAction';
import type { ChatAPI, ComposerAPI, DataAPI, UploadsAPI } from '../../../../client/lib/chats/ChatAPI';
import type { ChatAPI, ComposerAPI, DataAPI } from '../../../../client/lib/chats/ChatAPI';
import { createDataAPI } from '../../../../client/lib/chats/data';
import { processMessageEditing } from '../../../../client/lib/chats/flows/processMessageEditing';
import { processMessageUploads } from '../../../../client/lib/chats/flows/processMessageUploads';
@ -16,7 +16,6 @@ import { requestMessageDeletion } from '../../../../client/lib/chats/flows/reque
import { sendMessage } from '../../../../client/lib/chats/flows/sendMessage';
import { uploadFiles } from '../../../../client/lib/chats/flows/uploadFiles';
import { ReadStateManager } from '../../../../client/lib/chats/readStateManager';
import { createUploadsAPI } from '../../../../client/lib/chats/uploads';
import { setHighlightMessage } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription';
type DeepWritable<T> = T extends (...args: any) => any
@ -43,10 +42,6 @@ export class ChatMessages implements ChatAPI {
public readStateManager: ReadStateManager;
public uploads: UploadsAPI;
public threadUploads: UploadsAPI;
public ActionManager: any;
public emojiPicker: {
@ -124,7 +119,7 @@ export class ChatMessages implements ChatAPI {
await this.currentEditingMessage.stop();
},
editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => {
message.tmid ? this.threadUploads.clear() : this.uploads.clear();
this.composer?.uploads.clear();
const text = (await this.data.getDraft(message._id)) || message.attachments?.[0]?.description || message.msg;
await this.currentEditingMessage.stop();
@ -151,8 +146,6 @@ export class ChatMessages implements ChatAPI {
this.tmid = tmid;
this.uid = params.uid;
this.data = createDataAPI({ rid, tmid });
this.uploads = createUploadsAPI({ rid });
this.threadUploads = createUploadsAPI({ rid });
this.ActionManager = params.actionManager;
this.currentEditingMessage = new CurrentEditingMessage(this);

@ -65,6 +65,8 @@ export type ComposerAPI = {
readonly formatters: Subscribable<FormattingButton[]>;
readonly composerRef: RefObject<HTMLElement>;
readonly uploads: UploadsAPI;
};
export type DataAPI = {
@ -125,8 +127,6 @@ export type ChatAPI = {
readonly composer?: ComposerAPI;
readonly setComposerAPI: (composer?: ComposerAPI) => void;
readonly data: DataAPI;
readonly uploads: UploadsAPI;
readonly threadUploads: UploadsAPI;
readonly readStateManager: ReadStateManager;
readonly messageEditing: {
toPreviousMessage(): Promise<void>;
@ -156,15 +156,7 @@ export type ChatAPI = {
ActionManager: IActionManager;
readonly flows: {
readonly uploadFiles: ({
files,
uploadsStore,
resetFileInput,
}: {
files: readonly File[];
uploadsStore: UploadsAPI;
resetFileInput?: () => void;
}) => Promise<void>;
readonly uploadFiles: ({ files, resetFileInput }: { files: readonly File[]; resetFileInput?: () => void }) => Promise<void>;
readonly sendMessage: ({
text,
tshow,

@ -90,7 +90,7 @@ const getEncryptedContent = async (filesToUpload: readonly EncryptedUpload[], e2
});
};
async function continueSendingMessage(chat: ChatAPI, store: UploadsAPI, message: IMessage) {
async function continueSendingMessage(store: UploadsAPI, message: IMessage) {
const { msg, rid, tmid } = message;
const e2eRoom = await e2e.getInstanceByRoomId(rid);
const shouldConvertSentMessages = await e2eRoom?.shouldConvertSentMessages({ msg });
@ -144,22 +144,24 @@ async function continueSendingMessage(chat: ChatAPI, store: UploadsAPI, message:
store.setProcessingUploads(true);
for (const fileToConfirm of confirmFilesQueue) {
await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileToConfirm._id}`, fileToConfirm.composedMessage);
store.removeUpload(fileToConfirm._id);
}
store.clear();
} catch (error: unknown) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
store.setProcessingUploads(false);
chat.action.stop('uploading');
}
return true;
}
export const processMessageUploads = async (chat: ChatAPI, message: IMessage): Promise<boolean> => {
const { tmid } = message;
const store = chat.composer?.uploads;
if (!store) {
return false;
}
const store = tmid ? chat.threadUploads : chat.uploads;
const filesToUpload = store.get();
if (filesToUpload.length === 0) {
@ -169,7 +171,7 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage): P
const failedUploads = filesToUpload.filter((upload) => upload.error);
if (!failedUploads.length) {
return continueSendingMessage(chat, store, message);
return continueSendingMessage(store, message);
}
const allUploadsFailed = failedUploads.length === filesToUpload.length;
@ -197,7 +199,7 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage): P
onConfirm: () => {
imperativeModal.close();
failedUploads.forEach((upload) => store.removeUpload(upload.id));
resolve(continueSendingMessage(chat, store, message));
resolve(continueSendingMessage(store, message));
},
onCancel: () => {
imperativeModal.close();

@ -7,7 +7,7 @@ import { dispatchToastMessage } from '../../toast';
import { getUser } from '../../user';
import type { ChatAPI } from '../ChatAPI';
export const processTooLongMessage = async (chat: ChatAPI, { msg, tmid }: Pick<IMessage, 'msg' | 'tmid'>): Promise<boolean> => {
export const processTooLongMessage = async (chat: ChatAPI, { msg }: Pick<IMessage, 'msg'>): Promise<boolean> => {
const maxAllowedSize = settings.peek('Message_MaxAllowedSize');
if (msg.length <= maxAllowedSize) {
@ -34,7 +34,7 @@ export const processTooLongMessage = async (chat: ChatAPI, { msg, tmid }: Pick<I
chat.composer?.clear();
imperativeModal.close();
await chat.flows.uploadFiles({ files: [file], uploadsStore: tmid ? chat.threadUploads : chat.uploads });
await chat.flows.uploadFiles({ files: [file] });
resolve();
};

@ -60,7 +60,6 @@ export const sendMessage = async (
tshow,
previewUrls,
isSlashCommandAllowed,
tmid,
}: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; tmid?: IMessage['tmid'] },
): Promise<boolean> => {
if (!(await chat.data.isSubscribedToRoom())) {
@ -74,13 +73,13 @@ export const sendMessage = async (
chat.readStateManager.clearUnreadMark();
const uploadsStore = tmid ? chat.threadUploads : chat.uploads;
const uploadsStore = chat.composer?.uploads;
text = text.trim();
text = closeUnclosedCodeBlock(text);
const mid = chat.currentEditingMessage.getMID();
const hasFiles = uploadsStore.get().length > 0;
const hasFiles = uploadsStore && uploadsStore.get().length > 0;
if (!text && !mid && !hasFiles) {
// Nothing to do
return false;

@ -3,12 +3,17 @@ import { MAX_MULTIPLE_UPLOADED_FILES } from '../../../../lib/constants';
import { e2e } from '../../e2ee';
import { settings } from '../../settings';
import { dispatchToastMessage } from '../../toast';
import type { ChatAPI, UploadsAPI } from '../ChatAPI';
import type { ChatAPI } from '../ChatAPI';
export const uploadFiles = async (
chat: ChatAPI,
{ files, uploadsStore, resetFileInput }: { files: readonly File[]; uploadsStore: UploadsAPI; resetFileInput?: () => void },
{ files, resetFileInput }: { files: readonly File[]; resetFileInput?: () => void },
): Promise<void> => {
const uploadsStore = chat.composer?.uploads;
if (!uploadsStore) {
throw new Error('No uploads store found in composer');
}
const mergedFilesLength = files.length + uploadsStore.get().length;
if (mergedFilesLength > MAX_MULTIPLE_UPLOADED_FILES) {
return dispatchToastMessage({

@ -1,4 +1,4 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import { Random } from '@rocket.chat/random';
import fileSize from 'filesize';
@ -6,6 +6,7 @@ import fileSize from 'filesize';
import { getErrorMessage } from '../errorHandling';
import type { UploadsAPI, EncryptedFileUploadContent } from './ChatAPI';
import { isEncryptedUpload, type Upload } from './Upload';
import { USER_ACTIVITIES, UserAction } from '../../../app/ui/client/lib/UserAction';
import { fileUploadIsValidContentType } from '../../../app/utils/client';
import { sdk } from '../../../app/utils/client/lib/SDKClient';
import { i18n } from '../../../app/utils/lib/i18n';
@ -14,10 +15,12 @@ import { settings } from '../settings';
class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }> implements UploadsAPI {
private rid: string;
constructor({ rid }: { rid: string }) {
super();
private tmid?: string;
constructor({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }) {
super();
this.rid = rid;
this.tmid = tmid;
}
private uploads: readonly Upload[] = [];
@ -54,6 +57,10 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id'
removeUpload = (id: Upload['id']): void => {
this.set(this.uploads.filter((upload) => upload.id !== id));
if (this.uploads.length === 0) {
UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid });
}
};
editUploadFileName = (uploadId: Upload['id'], fileName: Upload['file']['name']) => {
@ -90,7 +97,10 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id'
}
};
clear = () => this.set([]);
clear = () => {
this.set([]);
UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid });
};
async send(file: File, encrypted?: EncryptedFileUploadContent): Promise<void> {
const maxFileSize = settings.peek('FileUpload_MaxFileSize');
@ -177,7 +187,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id'
this.once(`cancelling-${id}`, () => {
xhr.abort();
this.set(this.uploads.filter((upload) => upload.id !== id));
this.removeUpload(id);
reject(new Error(i18n.t('FileUpload_Canceled')));
});
});
@ -187,4 +197,5 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id'
}
}
export const createUploadsAPI = ({ rid }: { rid: IRoom['_id'] }): UploadsAPI => new UploadsStore({ rid });
export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI =>
new UploadsStore({ rid, tmid });

@ -7,18 +7,16 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AudioRecorder } from '../../../../app/ui/client/lib/recorderjs/AudioRecorder';
import type { UploadsAPI } from '../../../lib/chats/ChatAPI';
import { useChat } from '../../room/contexts/ChatContext';
const audioRecorder = new AudioRecorder();
type AudioMessageRecorderProps = {
rid: IRoom['_id'];
uploadsStore: UploadsAPI;
isMicrophoneDenied?: boolean;
};
const AudioMessageRecorder = ({ rid, uploadsStore, isMicrophoneDenied }: AudioMessageRecorderProps): ReactElement | null => {
const AudioMessageRecorder = ({ rid, isMicrophoneDenied }: AudioMessageRecorderProps): ReactElement | null => {
const { t } = useTranslation();
const [state, setState] = useState<'loading' | 'recording'>('recording');
@ -91,7 +89,7 @@ const AudioMessageRecorder = ({ rid, uploadsStore, isMicrophoneDenied }: AudioMe
const fileName = `${t('Audio_record')}.mp3`;
const file = new File([blob], fileName, { type: 'audio/mpeg' });
await chat?.flows.uploadFiles({ files: [file], uploadsStore });
await chat?.flows.uploadFiles({ files: [file] });
});
useEffect(() => {

@ -8,13 +8,11 @@ import { useRef, useEffect, useState } from 'react';
import { UserAction, USER_ACTIVITIES } from '../../../../app/ui/client/lib/UserAction';
import { VideoRecorder } from '../../../../app/ui/client/lib/recorderjs/videoRecorder';
import type { UploadsAPI } from '../../../lib/chats/ChatAPI';
import { useChat } from '../../room/contexts/ChatContext';
type VideoMessageRecorderProps = {
rid: IRoom['_id'];
tmid?: IMessage['_id'];
uploadsStore: UploadsAPI;
reference: RefObject<HTMLElement>;
} & Omit<AllHTMLAttributes<HTMLDivElement>, 'is'>;
@ -38,7 +36,7 @@ const getVideoRecordingExtension = () => {
return 'mp4';
};
const VideoMessageRecorder = ({ rid, tmid, uploadsStore, reference }: VideoMessageRecorderProps) => {
const VideoMessageRecorder = ({ rid, tmid, reference }: VideoMessageRecorderProps) => {
const t = useTranslation();
const videoRef = useRef<HTMLVideoElement>(null);
const dispatchToastMessage = useToastMessageDispatch();
@ -86,7 +84,7 @@ const VideoMessageRecorder = ({ rid, tmid, uploadsStore, reference }: VideoMessa
const cb = async (blob: Blob) => {
const fileName = `${t('Video_record')}.${getVideoRecordingExtension()}`;
const file = new File([blob], fileName, { type: VideoRecorder.getSupportedMimeTypes().split(';')[0] });
await chat?.flows.uploadFiles({ files: [file], uploadsStore });
await chat?.flows.uploadFiles({ files: [file] });
chat?.composer?.setRecordingVideo(false);
};

@ -117,8 +117,8 @@ const RoomBody = (): ReactElement => {
surroundingMessagesJumpTpRef,
);
const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat.uploads);
const { uploads, isUploading } = useFileUpload(chat.uploads);
const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget();
const { uploads, isUploading } = useFileUpload();
const { messageListRef } = useMessageListNavigation();
const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop();

@ -1,67 +1,49 @@
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';
import type { UploadsAPI } from '../../../../lib/chats/ChatAPI';
import type { Upload } from '../../../../lib/chats/Upload';
import { useChat } from '../../contexts/ChatContext';
export const useFileUpload = (store: UploadsAPI) => {
const emptySubscribe = () => () => undefined;
const emptyUploads: readonly Upload[] = [];
const getEmptyUploads = () => emptyUploads;
const getEmptyBool = () => false;
export const useFileUpload = () => {
const chat = useChat();
if (!chat || !store) {
if (!chat) {
throw new Error('No ChatContext provided');
}
useEffect(() => {
store.wipeFailedOnes();
}, [store]);
const uploads = useSyncExternalStore(store.subscribe, store.get);
const isProcessingUploads = useSyncExternalStore(store.subscribe, store.getProcessingUploads);
const store = chat.composer?.uploads;
const stopUploadingAction = useCallback(() => {
if (uploads.length === 1) {
chat.action.stop('uploading');
}
}, [chat.action, uploads.length]);
const uploads = useSyncExternalStore(store?.subscribe ?? emptySubscribe, store?.get ?? getEmptyUploads);
const isProcessingUploads = useSyncExternalStore(store?.subscribe ?? emptySubscribe, store?.getProcessingUploads ?? getEmptyBool);
const handleRemoveUpload = useCallback(
(id: Upload['id']) => {
store.removeUpload(id);
stopUploadingAction();
},
[stopUploadingAction, store],
);
const handleCancelUpload = useCallback(
(id: Upload['id']) => {
store.cancel(id);
stopUploadingAction();
},
[stopUploadingAction, store],
);
useEffect(() => {
store?.wipeFailedOnes();
const handleEditUpload = useCallback((id: Upload['id'], fileName: string) => store.editUploadFileName(id, fileName), [store]);
return () => store?.clear();
}, [chat.action, store]);
const handleUploadFiles = useCallback(
(files: readonly File[]): void => {
chat?.flows.uploadFiles({ files, uploadsStore: store });
chat.flows.uploadFiles({ files });
},
[chat, store],
[chat],
);
const isUploading = uploads.length > 0 && uploads.some((upload) => upload.percentage < 100 && !upload.error);
return useMemo(
() => ({
uploadsStore: store,
uploads,
hasUploads: uploads.length > 0,
isUploading,
isProcessingUploads,
handleRemoveUpload,
handleEditUpload,
handleCancelUpload,
handleUploadFiles,
}),
[uploads, isUploading, isProcessingUploads, handleRemoveUpload, handleEditUpload, handleCancelUpload, handleUploadFiles],
[store, uploads, isUploading, isProcessingUploads, handleUploadFiles],
);
};

@ -5,15 +5,12 @@ import { useCallback, useMemo, useSyncExternalStore } from 'react';
import { useDropTarget } from './useDropTarget';
import { useReactiveValue } from '../../../../hooks/useReactiveValue';
import type { UploadsAPI } from '../../../../lib/chats/ChatAPI';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
import { useIsRoomOverMacLimit } from '../../../omnichannel/hooks/useIsRoomOverMacLimit';
import { useChat } from '../../contexts/ChatContext';
import { useRoom, useRoomSubscription } from '../../contexts/RoomContext';
export const useFileUploadDropTarget = (
uploadsStore: UploadsAPI,
): readonly [
export const useFileUploadDropTarget = (): readonly [
fileUploadTriggerProps: {
onDragEnter: (event: DragEvent<Element>) => void;
},
@ -66,7 +63,7 @@ export const useFileUploadDropTarget = (
return file;
});
chat?.flows.uploadFiles({ files: uploads, uploadsStore });
chat?.flows.uploadFiles({ files: uploads });
});
const allOverlayProps = useMemo(() => {

@ -3,38 +3,55 @@ import type { ReactElement } from 'react';
import { useCallback, Fragment, useSyncExternalStore, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { UserAction } from '../../../../../app/ui/client/lib/UserAction';
import { UserAction, USER_ACTIVITIES } from '../../../../../app/ui/client/lib/UserAction';
const maxUsernames = 5;
const ACTION_PRIORITY: Record<string, number> = {
[USER_ACTIVITIES.USER_RECORDING]: 0,
[USER_ACTIVITIES.USER_UPLOADING]: 1,
[USER_ACTIVITIES.USER_TYPING]: 2,
[USER_ACTIVITIES.USER_PLAYING]: 3,
};
const ComposerUserActionIndicator = ({ rid, tmid }: { rid: string; tmid?: string }): ReactElement => {
const { t } = useTranslation();
const roomAction = useSyncExternalStore(
UserAction.subscribe,
useCallback(() => UserAction.get(tmid || rid), [rid, tmid]),
);
const actions = useMemo(
() =>
Object.entries(roomAction ?? {})
.map(([key, _users]) => {
const action = key.split('-')[1];
const users = Object.keys(_users);
if (users.length === 0) {
return;
}
return {
action,
users,
};
})
.filter(Boolean) as {
action: 'typing' | 'recording' | 'uploading' | 'playing';
users: string[];
}[],
[roomAction],
);
const actions = useMemo(() => {
const usersRendered = new Set<string>();
return Object.entries(roomAction ?? {})
.sort(([a], [b]) => ACTION_PRIORITY[a] - ACTION_PRIORITY[b])
.map(([key, _users]) => {
const action = key.split('-')[1];
const users = Object.keys(_users);
if (users.length === 0) {
return;
}
const filteredUsers = users.filter((user) => !usersRendered.has(user));
if (filteredUsers.length === 0) {
return;
}
for (const user of filteredUsers) {
usersRendered.add(user);
}
return {
action,
users: filteredUsers,
};
})
.filter(Boolean) as {
action: 'typing' | 'recording' | 'uploading' | 'playing';
users: string[];
}[];
}, [roomAction]);
return (
<Box

@ -137,9 +137,9 @@ const MessageBox = ({
if (chat.composer) {
return;
}
chat.setComposerAPI(createComposerAPI(node, storageID, quoteChainLimit, messageComposerRef));
chat.setComposerAPI(createComposerAPI(node, storageID, quoteChainLimit, messageComposerRef, { rid: room._id, tmid }));
},
[chat, storageID, quoteChainLimit],
[chat, storageID, quoteChainLimit, room._id, tmid],
);
const autofocusRef = useMessageBoxAutoFocus(!isMobile);
@ -158,17 +158,7 @@ const MessageBox = ({
chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `));
});
const uploadsStore = tmid ? chat.threadUploads : chat.uploads;
const {
uploads,
hasUploads,
handleUploadFiles,
handleEditUpload,
handleRemoveUpload,
handleCancelUpload,
isUploading,
isProcessingUploads,
} = useFileUpload(uploadsStore);
const { hasUploads, handleUploadFiles, isUploading, isProcessingUploads } = useFileUpload();
const handleSendMessage = useEffectEvent(() => {
if (isUploading || isProcessingUploads) {
@ -436,9 +426,9 @@ const MessageBox = ({
unencryptedMessagesAllowed={unencryptedMessagesAllowed}
isMobile={isMobile}
/>
{isRecordingVideo && <VideoMessageRecorder reference={messageComposerRef} uploadsStore={uploadsStore} rid={room._id} tmid={tmid} />}
{isRecordingVideo && <VideoMessageRecorder reference={messageComposerRef} rid={room._id} tmid={tmid} />}
<MessageComposer ref={messageComposerRef} variant={isEditing ? 'editing' : undefined}>
{isRecordingAudio && <AudioMessageRecorder rid={room._id} uploadsStore={uploadsStore} isMicrophoneDenied={isMicrophoneDenied} />}
{isRecordingAudio && <AudioMessageRecorder rid={room._id} isMicrophoneDenied={isMicrophoneDenied} />}
<MessageComposerInputExpandable
dimensions={sizes}
ref={mergedRefs}
@ -451,15 +441,7 @@ const MessageBox = ({
onPaste={handlePaste}
aria-activedescendant={popup.focused ? `popup-item-${popup.focused._id}` : undefined}
/>
{hasUploads && (
<MessageComposerFiles
uploads={uploads}
onEdit={handleEditUpload}
onRemove={handleRemoveUpload}
onCancel={handleCancelUpload}
disabled={isProcessingUploads}
/>
)}
<MessageComposerFiles />
<MessageComposerToolbar>
<MessageComposerToolbarActions aria-label={t('Message_composer_toolbox_primary_actions')}>
<MessageComposerAction

@ -56,12 +56,11 @@ const MessageBoxActionsToolbar = ({
}
const room = useRoom();
const uploadsStore = tmid ? chatContext.threadUploads : chatContext.uploads;
const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied);
const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording);
const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing, uploadsStore);
const webdavActions = useWebdavActions(!canSend || isRecording || isEditing, uploadsStore);
const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing);
const webdavActions = useWebdavActions(!canSend || isRecording || isEditing);
const createDiscussionAction = useCreateDiscussionAction(room);
const shareLocationAction = useShareLocationAction(room, tmid);
const timestampAction = useTimestampAction(chatContext.composer);

@ -4,12 +4,11 @@ import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useFileInput } from '../../../../../../hooks/useFileInput';
import type { UploadsAPI } from '../../../../../../lib/chats/ChatAPI';
import { useChat } from '../../../../contexts/ChatContext';
const fileInputProps = { type: 'file', multiple: true };
export const useFileUploadAction = (disabled: boolean, uploadsStore: UploadsAPI): GenericMenuItemProps => {
export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => {
const { t } = useTranslation();
const fileUploadEnabled = useSetting('FileUpload_Enabled', true);
const fileInputRef = useFileInput(fileInputProps);
@ -32,12 +31,12 @@ export const useFileUploadAction = (disabled: boolean, uploadsStore: UploadsAPI)
});
return file;
});
chat?.flows.uploadFiles({ files: filesToUpload, uploadsStore, resetFileInput });
chat?.flows.uploadFiles({ files: filesToUpload, resetFileInput });
};
fileInputRef.current?.addEventListener('change', handleUploadChange);
return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange);
}, [chat, fileInputRef, uploadsStore]);
}, [chat, fileInputRef]);
const handleUpload = () => {
fileInputRef?.current?.click();

@ -4,12 +4,11 @@ import { useSetModal, useSetting } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
import { useWebDAVAccountIntegrationsQuery } from '../../../../../../hooks/webdav/useWebDAVAccountIntegrationsQuery';
import type { UploadsAPI } from '../../../../../../lib/chats/ChatAPI';
import { useChat } from '../../../../contexts/ChatContext';
import AddWebdavAccountModal from '../../../../webdav/AddWebdavAccountModal';
import WebdavFilePickerModal from '../../../../webdav/WebdavFilePickerModal';
export const useWebdavActions = (disabled: boolean, uploadsStore: UploadsAPI): GenericMenuItemProps[] => {
export const useWebdavActions = (disabled: boolean): GenericMenuItemProps[] => {
const enabled = useSetting('Webdav_Integration_Enabled', false);
const { isSuccess, data } = useWebDAVAccountIntegrationsQuery({ enabled });
@ -20,7 +19,7 @@ export const useWebdavActions = (disabled: boolean, uploadsStore: UploadsAPI): G
const setModal = useSetModal();
const handleAddWebDav = () => setModal(<AddWebdavAccountModal onClose={() => setModal(null)} onConfirm={() => setModal(null)} />);
const handleUpload = async (file: File) => chat?.flows.uploadFiles({ files: [file], uploadsStore });
const handleUpload = async (file: File) => chat?.flows.uploadFiles({ files: [file] });
const handleOpenWebdav = (account: IWebdavAccountIntegration) =>
setModal(<WebdavFilePickerModal account={account} onUpload={handleUpload} onClose={() => setModal(null)} />);

@ -2,28 +2,26 @@ import { MessageComposerFileGroup } from '@rocket.chat/ui-composer';
import { useTranslation } from 'react-i18next';
import MessageComposerFileItem from './MessageComposerFileItem';
import type { Upload } from '../../../../lib/chats/Upload';
import { useFileUpload } from '../../body/hooks/useFileUpload';
type MessageComposerFileGroupProps = {
uploads?: readonly Upload[];
onRemove: (id: Upload['id']) => void;
onEdit: (id: Upload['id'], fileName: string) => void;
onCancel: (id: Upload['id']) => void;
disabled: boolean;
};
const MessageComposerFiles = ({ uploads, onRemove, onEdit, onCancel, disabled }: MessageComposerFileGroupProps) => {
const MessageComposerFiles = () => {
const { t } = useTranslation();
const { uploads, uploadsStore, isProcessingUploads, hasUploads } = useFileUpload();
if (!uploadsStore || !hasUploads) {
return null;
}
return (
<MessageComposerFileGroup aria-label={t('Uploads')}>
{uploads?.map((upload) => (
{uploads.map((upload) => (
<MessageComposerFileItem
key={upload.id}
upload={upload}
onRemove={onRemove}
onEdit={onEdit}
onCancel={onCancel}
disabled={disabled}
onRemove={uploadsStore.removeUpload}
onEdit={uploadsStore.editUploadFileName}
onCancel={uploadsStore.cancel}
disabled={isProcessingUploads}
/>
))}
</MessageComposerFileGroup>

@ -51,7 +51,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => {
closeTab();
}, [closeTab]);
const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat.threadUploads);
const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget();
const handleNavigateToPreviousMessage = useCallback((): void => {
chat?.messageEditing.toPreviousMessage();

Loading…
Cancel
Save