diff --git a/app/soapbox/features/chats/components/chat-composer.tsx b/app/soapbox/features/chats/components/chat-composer.tsx index 165e7ce9d5..3432a75332 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 55b0cf2cba..5e0d918a15 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 fa7700fb5f..b75f99f9fc 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 28fb403b38..abe5738a3f 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) => {