From ef5001d38bd8118335b5bf54e6ffba2f6c4800c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 Feb 2023 12:24:41 -0600 Subject: [PATCH 1/6] Chats: support multiple attachments --- .../chats/components/chat-composer.tsx | 2 ++ .../features/chats/components/chat.tsx | 26 +++++++++++++------ app/soapbox/normalizers/instance.ts | 3 ++- app/soapbox/queries/chats.ts | 10 ++++--- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/soapbox/features/chats/components/chat-composer.tsx b/app/soapbox/features/chats/components/chat-composer.tsx index 165e7ce9d..3432a7533 100644 --- a/app/soapbox/features/chats/components/chat-composer.tsx +++ b/app/soapbox/features/chats/components/chat-composer.tsx @@ -73,6 +73,7 @@ const ChatComposer = React.forwardRef const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by'])); const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking'])); const maxCharacterCount = useAppSelector((state) => state.instance.getIn(['configuration', 'chats', 'max_characters']) as number); + const attachmentLimit = useAppSelector(state => state.instance.configuration.getIn(['chats', 'max_media_attachments']) as number); const [suggestions, setSuggestions] = useState(initialSuggestionState); const isSuggestionsAvailable = suggestions.list.length > 0; @@ -172,6 +173,7 @@ const ChatComposer = React.forwardRef resetFileKey={resetFileKey} iconClassName='w-5 h-5' className='text-primary-500' + disabled={attachments.length >= attachmentLimit} /> )} diff --git a/app/soapbox/features/chats/components/chat.tsx b/app/soapbox/features/chats/components/chat.tsx index 55b0cf2cb..5e0d918a1 100644 --- a/app/soapbox/features/chats/components/chat.tsx +++ b/app/soapbox/features/chats/components/chat.tsx @@ -5,17 +5,21 @@ import { defineMessages, useIntl } from 'react-intl'; import { uploadMedia } from 'soapbox/actions/media'; import { Stack } from 'soapbox/components/ui'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { normalizeAttachment } from 'soapbox/normalizers'; import { IChat, useChatActions } from 'soapbox/queries/chats'; +import toast from 'soapbox/toast'; import ChatComposer from './chat-composer'; import ChatMessageList from './chat-message-list'; +import type { Attachment } from 'soapbox/types/entities'; + const fileKeyGen = (): number => Math.floor((Math.random() * 0x10000)); const messages = defineMessages({ failedToSend: { id: 'chat.failed_to_send', defaultMessage: 'Message failed to send.' }, + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, }); interface ChatInterface { @@ -49,18 +53,19 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { const dispatch = useAppDispatch(); const { createChatMessage, acceptChat } = useChatActions(chat.id); + const attachmentLimit = useAppSelector(state => state.instance.configuration.getIn(['chats', 'max_media_attachments']) as number); const [content, setContent] = useState(''); - const [attachment, setAttachment] = useState(undefined); + const [attachments, setAttachments] = useState([]); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [resetFileKey, setResetFileKey] = useState(fileKeyGen()); const [errorMessage, setErrorMessage] = useState(); - const isSubmitDisabled = content.length === 0 && !attachment; + const isSubmitDisabled = content.length === 0 && attachments.length === 0; const submitMessage = () => { - createChatMessage.mutate({ chatId: chat.id, content, mediaId: attachment?.id }, { + createChatMessage.mutate({ chatId: chat.id, content, mediaIds: attachments.map(a => a.id) }, { onSuccess: () => { setErrorMessage(undefined); }, @@ -79,7 +84,7 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { clearNativeInputValue(inputRef.current); } setContent(''); - setAttachment(undefined); + setAttachments([]); setIsUploading(false); setUploadProgress(0); setResetFileKey(fileKeyGen()); @@ -127,7 +132,7 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { const handleMouseOver = () => markRead(); const handleRemoveFile = () => { - setAttachment(undefined); + setAttachments([]); setResetFileKey(fileKeyGen()); }; @@ -137,13 +142,18 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { }; const handleFiles = (files: FileList) => { + if (files.length + attachments.length > attachmentLimit) { + toast.error(messages.uploadErrorLimit); + return; + } + setIsUploading(true); const data = new FormData(); data.append('file', files[0]); dispatch(uploadMedia(data, onUploadProgress)).then((response: any) => { - setAttachment(normalizeAttachment(response.data)); + setAttachments([...attachments, normalizeAttachment(response.data)]); setIsUploading(false); }).catch(() => { setIsUploading(false); @@ -172,7 +182,7 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { onSelectFile={handleFiles} resetFileKey={resetFileKey} onPaste={handlePaste} - attachments={attachment ? [attachment] : []} + attachments={attachments} onDeleteAttachment={handleRemoveFile} isUploading={isUploading} uploadProgress={uploadProgress} diff --git a/app/soapbox/normalizers/instance.ts b/app/soapbox/normalizers/instance.ts index fa7700fb5..b75f99f9f 100644 --- a/app/soapbox/normalizers/instance.ts +++ b/app/soapbox/normalizers/instance.ts @@ -22,7 +22,8 @@ export const InstanceRecord = ImmutableRecord({ configuration: ImmutableMap({ media_attachments: ImmutableMap(), chats: ImmutableMap({ - max_characters: 500, + max_characters: 5000, + max_media_attachments: 1, }), polls: ImmutableMap({ max_options: 4, diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index 28fb403b3..abe5738a3 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -233,9 +233,13 @@ const useChatActions = (chatId: string) => { }; const createChatMessage = useMutation( - ( - { chatId, content, mediaId }: { chatId: string, content: string, mediaId?: string }, - ) => api.post(`/api/v1/pleroma/chats/${chatId}/messages`, { content, media_id: mediaId, media_ids: [mediaId] }), + ({ chatId, content, mediaIds }: { chatId: string, content: string, mediaIds?: string[] }) => { + return api.post(`/api/v1/pleroma/chats/${chatId}/messages`, { + content, + media_id: (mediaIds && mediaIds.length === 1) ? mediaIds[0] : undefined, // Pleroma backwards-compat + media_ids: mediaIds, + }); + }, { retry: false, onMutate: async (variables) => { From 072e058764f03056bd62e5a93758f463c9032707 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 Feb 2023 12:50:18 -0600 Subject: [PATCH 2/6] ChatUploadPreview: support gif attachments --- app/soapbox/features/chats/components/chat-upload-preview.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/chats/components/chat-upload-preview.tsx b/app/soapbox/features/chats/components/chat-upload-preview.tsx index 14864fd91..a67d470b6 100644 --- a/app/soapbox/features/chats/components/chat-upload-preview.tsx +++ b/app/soapbox/features/chats/components/chat-upload-preview.tsx @@ -21,6 +21,7 @@ const ChatUploadPreview: React.FC = ({ className, attachment switch (attachment.type) { case 'image': + case 'gifv': return ( Date: Wed, 8 Feb 2023 12:50:35 -0600 Subject: [PATCH 3/6] ChatTextarea: add gaps between uploads, put pending upload at end of list --- .../chats/components/chat-textarea.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/app/soapbox/features/chats/components/chat-textarea.tsx b/app/soapbox/features/chats/components/chat-textarea.tsx index 22b0877e9..7a15a036a 100644 --- a/app/soapbox/features/chats/components/chat-textarea.tsx +++ b/app/soapbox/features/chats/components/chat-textarea.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Textarea } from 'soapbox/components/ui'; +import { HStack, Textarea } from 'soapbox/components/ui'; import { Attachment } from 'soapbox/types/entities'; import ChatPendingUpload from './chat-pending-upload'; @@ -35,19 +35,23 @@ const ChatTextarea: React.FC = ({ `} > {(!!attachments?.length || isUploading) && ( -
- {isUploading && ( - - )} - + {attachments?.map(attachment => ( - +
+ +
))} -
+ + {isUploading && ( +
+ +
+ )} + )}