Merge remote-tracking branch 'origin/multiple-attachments' into gleasonator

gleasonator
Alex Gleason 2023-02-08 21:08:35 -06:00
commit bdb8ce8004
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
8 zmienionych plików z 65 dodań i 31 usunięć

Wyświetl plik

@ -43,7 +43,7 @@ interface IChatComposer extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaEl
onSelectFile: (files: FileList, intl: IntlShape) => void onSelectFile: (files: FileList, intl: IntlShape) => void
resetFileKey: number | null resetFileKey: number | null
attachments?: Attachment[] attachments?: Attachment[]
onDeleteAttachment?: () => void onDeleteAttachment?: (i: number) => void
isUploading?: boolean isUploading?: boolean
uploadProgress?: number uploadProgress?: number
} }
@ -73,13 +73,14 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by'])); const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking'])); 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 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<Suggestion>(initialSuggestionState); const [suggestions, setSuggestions] = useState<Suggestion>(initialSuggestionState);
const isSuggestionsAvailable = suggestions.list.length > 0; const isSuggestionsAvailable = suggestions.list.length > 0;
const hasAttachment = attachments.length > 0; const hasAttachment = attachments.length > 0;
const isOverCharacterLimit = maxCharacterCount && value?.length > maxCharacterCount; const isOverCharacterLimit = maxCharacterCount && value?.length > maxCharacterCount;
const isSubmitDisabled = disabled || isOverCharacterLimit || (value.length === 0 && !hasAttachment); const isSubmitDisabled = disabled || isUploading || isOverCharacterLimit || (value.length === 0 && !hasAttachment);
const overLimitText = maxCharacterCount ? maxCharacterCount - value?.length : ''; const overLimitText = maxCharacterCount ? maxCharacterCount - value?.length : '';
@ -172,6 +173,7 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
resetFileKey={resetFileKey} resetFileKey={resetFileKey}
iconClassName='w-5 h-5' iconClassName='w-5 h-5'
className='text-primary-500' className='text-primary-500'
disabled={isUploading || (attachments.length >= attachmentLimit)}
/> />
</Stack> </Stack>
)} )}

Wyświetl plik

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Textarea } from 'soapbox/components/ui'; import { HStack, Textarea } from 'soapbox/components/ui';
import { Attachment } from 'soapbox/types/entities'; import { Attachment } from 'soapbox/types/entities';
import ChatPendingUpload from './chat-pending-upload'; import ChatPendingUpload from './chat-pending-upload';
@ -8,7 +8,7 @@ import ChatUpload from './chat-upload';
interface IChatTextarea extends React.ComponentProps<typeof Textarea> { interface IChatTextarea extends React.ComponentProps<typeof Textarea> {
attachments?: Attachment[] attachments?: Attachment[]
onDeleteAttachment?: () => void onDeleteAttachment?: (i: number) => void
isUploading?: boolean isUploading?: boolean
uploadProgress?: number uploadProgress?: number
} }
@ -21,6 +21,14 @@ const ChatTextarea: React.FC<IChatTextarea> = ({
uploadProgress = 0, uploadProgress = 0,
...rest ...rest
}) => { }) => {
const handleDeleteAttachment = (i: number) => {
return () => {
if (onDeleteAttachment) {
onDeleteAttachment(i);
}
};
};
return ( return (
<div className={` <div className={`
block block
@ -35,19 +43,23 @@ const ChatTextarea: React.FC<IChatTextarea> = ({
`} `}
> >
{(!!attachments?.length || isUploading) && ( {(!!attachments?.length || isUploading) && (
<div className='flex p-3 pb-0'> <HStack className='-ml-2 -mt-2 p-3 pb-0' wrap>
{isUploading && ( {attachments?.map((attachment, i) => (
<ChatPendingUpload progress={uploadProgress} /> <div className='ml-2 mt-2 flex'>
)} <ChatUpload
key={attachment.id}
{attachments?.map(attachment => ( attachment={attachment}
<ChatUpload onDelete={handleDeleteAttachment(i)}
key={attachment.id} />
attachment={attachment} </div>
onDelete={onDeleteAttachment}
/>
))} ))}
</div>
{isUploading && (
<div className='ml-2 mt-2 flex'>
<ChatPendingUpload progress={uploadProgress} />
</div>
)}
</HStack>
)} )}
<Textarea theme='transparent' {...rest} /> <Textarea theme='transparent' {...rest} />

Wyświetl plik

@ -21,6 +21,7 @@ const ChatUploadPreview: React.FC<IChatUploadPreview> = ({ className, attachment
switch (attachment.type) { switch (attachment.type) {
case 'image': case 'image':
case 'gifv':
return ( return (
<img <img
className='pointer-events-none h-full w-full object-cover' className='pointer-events-none h-full w-full object-cover'

Wyświetl plik

@ -5,17 +5,21 @@ import { defineMessages, useIntl } from 'react-intl';
import { uploadMedia } from 'soapbox/actions/media'; import { uploadMedia } from 'soapbox/actions/media';
import { Stack } from 'soapbox/components/ui'; import { Stack } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { normalizeAttachment } from 'soapbox/normalizers'; import { normalizeAttachment } from 'soapbox/normalizers';
import { IChat, useChatActions } from 'soapbox/queries/chats'; import { IChat, useChatActions } from 'soapbox/queries/chats';
import toast from 'soapbox/toast';
import ChatComposer from './chat-composer'; import ChatComposer from './chat-composer';
import ChatMessageList from './chat-message-list'; import ChatMessageList from './chat-message-list';
import type { Attachment } from 'soapbox/types/entities';
const fileKeyGen = (): number => Math.floor((Math.random() * 0x10000)); const fileKeyGen = (): number => Math.floor((Math.random() * 0x10000));
const messages = defineMessages({ const messages = defineMessages({
failedToSend: { id: 'chat.failed_to_send', defaultMessage: 'Message failed to send.' }, failedToSend: { id: 'chat.failed_to_send', defaultMessage: 'Message failed to send.' },
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
}); });
interface ChatInterface { interface ChatInterface {
@ -49,18 +53,19 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { createChatMessage, acceptChat } = useChatActions(chat.id); const { createChatMessage, acceptChat } = useChatActions(chat.id);
const attachmentLimit = useAppSelector(state => state.instance.configuration.getIn(['chats', 'max_media_attachments']) as number);
const [content, setContent] = useState<string>(''); const [content, setContent] = useState<string>('');
const [attachment, setAttachment] = useState<any>(undefined); const [attachments, setAttachments] = useState<Attachment[]>([]);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0); const [uploadProgress, setUploadProgress] = useState(0);
const [resetFileKey, setResetFileKey] = useState<number>(fileKeyGen()); const [resetFileKey, setResetFileKey] = useState<number>(fileKeyGen());
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const isSubmitDisabled = content.length === 0 && !attachment; const isSubmitDisabled = content.length === 0 && attachments.length === 0;
const submitMessage = () => { const submitMessage = () => {
createChatMessage.mutate({ chatId: chat.id, content, mediaId: attachment?.id }, { createChatMessage.mutate({ chatId: chat.id, content, mediaIds: attachments.map(a => a.id) }, {
onSuccess: () => { onSuccess: () => {
setErrorMessage(undefined); setErrorMessage(undefined);
}, },
@ -79,7 +84,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
clearNativeInputValue(inputRef.current); clearNativeInputValue(inputRef.current);
} }
setContent(''); setContent('');
setAttachment(undefined); setAttachments([]);
setIsUploading(false); setIsUploading(false);
setUploadProgress(0); setUploadProgress(0);
setResetFileKey(fileKeyGen()); setResetFileKey(fileKeyGen());
@ -126,8 +131,10 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
const handleMouseOver = () => markRead(); const handleMouseOver = () => markRead();
const handleRemoveFile = () => { const handleRemoveFile = (i: number) => {
setAttachment(undefined); const newAttachments = [...attachments];
newAttachments.splice(i, 1);
setAttachments(newAttachments);
setResetFileKey(fileKeyGen()); setResetFileKey(fileKeyGen());
}; };
@ -137,13 +144,18 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
}; };
const handleFiles = (files: FileList) => { const handleFiles = (files: FileList) => {
if (files.length + attachments.length > attachmentLimit) {
toast.error(messages.uploadErrorLimit);
return;
}
setIsUploading(true); setIsUploading(true);
const data = new FormData(); const data = new FormData();
data.append('file', files[0]); data.append('file', files[0]);
dispatch(uploadMedia(data, onUploadProgress)).then((response: any) => { dispatch(uploadMedia(data, onUploadProgress)).then((response: any) => {
setAttachment(normalizeAttachment(response.data)); setAttachments([...attachments, normalizeAttachment(response.data)]);
setIsUploading(false); setIsUploading(false);
}).catch(() => { }).catch(() => {
setIsUploading(false); setIsUploading(false);
@ -172,7 +184,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
onSelectFile={handleFiles} onSelectFile={handleFiles}
resetFileKey={resetFileKey} resetFileKey={resetFileKey}
onPaste={handlePaste} onPaste={handlePaste}
attachments={attachment ? [attachment] : []} attachments={attachments}
onDeleteAttachment={handleRemoveFile} onDeleteAttachment={handleRemoveFile}
isUploading={isUploading} isUploading={isUploading}
uploadProgress={uploadProgress} uploadProgress={uploadProgress}

Wyświetl plik

@ -10,7 +10,8 @@ describe('normalizeInstance()', () => {
configuration: { configuration: {
media_attachments: {}, media_attachments: {},
chats: { chats: {
max_characters: 500, max_characters: 5000,
max_media_attachments: 1,
}, },
polls: { polls: {
max_options: 4, max_options: 4,

Wyświetl plik

@ -22,7 +22,8 @@ export const InstanceRecord = ImmutableRecord({
configuration: ImmutableMap<string, any>({ configuration: ImmutableMap<string, any>({
media_attachments: ImmutableMap<string, any>(), media_attachments: ImmutableMap<string, any>(),
chats: ImmutableMap<string, number>({ chats: ImmutableMap<string, number>({
max_characters: 500, max_characters: 5000,
max_media_attachments: 1,
}), }),
polls: ImmutableMap<string, number>({ polls: ImmutableMap<string, number>({
max_options: 4, max_options: 4,

Wyświetl plik

@ -231,9 +231,13 @@ const useChatActions = (chatId: string) => {
}; };
const createChatMessage = useMutation( const createChatMessage = useMutation(
( ({ chatId, content, mediaIds }: { chatId: string, content: string, mediaIds?: string[] }) => {
{ chatId, content, mediaId }: { chatId: string, content: string, mediaId?: string }, return api.post<ChatMessage>(`/api/v1/pleroma/chats/${chatId}/messages`, {
) => api.post<ChatMessage>(`/api/v1/pleroma/chats/${chatId}/messages`, { content, media_id: mediaId, media_ids: [mediaId] }), content,
media_id: (mediaIds && mediaIds.length === 1) ? mediaIds[0] : undefined, // Pleroma backwards-compat
media_ids: mediaIds,
});
},
{ {
retry: false, retry: false,
onMutate: async (variables) => { onMutate: async (variables) => {

Wyświetl plik

@ -13,7 +13,8 @@ describe('instance reducer', () => {
description_limit: 1500, description_limit: 1500,
configuration: { configuration: {
chats: { chats: {
max_characters: 500, max_characters: 5000,
max_media_attachments: 1,
}, },
statuses: { statuses: {
max_characters: 500, max_characters: 500,