WIP image upload, this needs cleanup

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-07-21 01:13:42 +02:00
parent 3c5025c7f3
commit f55a76886f
5 changed files with 212 additions and 33 deletions

View file

@ -33,6 +33,11 @@ const { CancelToken, isCancel } = axios;
let cancelFetchComposeSuggestions: Canceler;
const FILES_UPLOAD_REQUEST = 'FILES_UPLOAD_REQUEST' as const;
const FILES_UPLOAD_SUCCESS = 'FILES_UPLOAD_SUCCESS' as const;
const FILES_UPLOAD_FAIL = 'FILES_UPLOAD_FAIL' as const;
const FILES_UPLOAD_PROGRESS = 'FILES_UPLOAD_PROGRESS' as const;
const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const;
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const;
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const;
@ -388,6 +393,102 @@ const submitComposeFail = (composeId: string, error: AxiosError) => ({
error: error,
});
const uploadFiles = (files: FileList, intl: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined;
const maxVideoDuration = getState().instance.configuration.getIn(['media_attachments', 'video_duration_limit']) as number | undefined;
const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
dispatch(uploadFilesRequest());
return Array.from(files).forEach(async(f, i) => {
const isImage = f.type.match(/image.*/);
const isVideo = f.type.match(/video.*/);
const videoDurationInSeconds = (isVideo && maxVideoDuration) ? await getVideoDuration(f) : 0;
if (isImage && maxImageSize && (f.size > maxImageSize)) {
const limit = formatBytes(maxImageSize);
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
toast.error(message);
dispatch(uploadFilesFail(true));
return;
} else if (isVideo && maxVideoSize && (f.size > maxVideoSize)) {
const limit = formatBytes(maxVideoSize);
const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit });
toast.error(message);
dispatch(uploadFilesFail(true));
return;
} else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) {
const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration });
toast.error(message);
dispatch(uploadFilesFail(true));
return;
}
// FIXME: Don't define const in loop
resizeImage(f).then(file => {
const data = new FormData();
data.append('file', file);
// Account for disparity in size of original image and resized data
total += file.size - f.size;
const onUploadProgress = ({ loaded }: any) => {
progress[i] = loaded;
dispatch(uploadFilesProgress(progress.reduce((a, v) => a + v, 0), total));
};
return dispatch(uploadMedia(data, onUploadProgress))
.then(({ status, data }) => {
// If server-side processing of the media attachment has not completed yet,
// poll the server until it is, before showing the media attachment as uploaded
if (status === 200) {
dispatch(uploadFilesSuccess(data, f));
} else if (status === 202) {
const poll = () =>
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
if (status === 200) {
dispatch(uploadFilesSuccess(data, f));
return data;
} else if (status === 206) {
setTimeout(() => poll(), 1000);
}
}).catch(error => dispatch(uploadFilesFail(error)));
poll();
}
});
}).catch(error => dispatch(uploadFilesFail(error)));
});
};
const uploadFilesRequest = () => ({
type: FILES_UPLOAD_REQUEST,
skipLoading: true,
});
const uploadFilesProgress = (loaded: number, total: number) => ({
type: FILES_UPLOAD_PROGRESS,
loaded: loaded,
total: total,
});
const uploadFilesSuccess = (media: APIEntity, file: File) => ({
type: FILES_UPLOAD_SUCCESS,
media: media,
file,
skipLoading: true,
});
const uploadFilesFail = (error: AxiosError | true) => ({
type: FILES_UPLOAD_FAIL,
error: error,
skipLoading: true,
});
const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
@ -436,7 +537,6 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
}
// FIXME: Don't define const in loop
/* eslint-disable no-loop-func */
resizeImage(f).then(file => {
const data = new FormData();
data.append('file', file);
@ -469,10 +569,37 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
}
});
}).catch(error => dispatch(uploadComposeFail(composeId, error)));
/* eslint-enable no-loop-func */
});
};
const uploadComposeRequest = (composeId: string) => ({
type: COMPOSE_UPLOAD_REQUEST,
id: composeId,
skipLoading: true,
});
const uploadComposeProgress = (composeId: string, loaded: number, total: number) => ({
type: COMPOSE_UPLOAD_PROGRESS,
id: composeId,
loaded: loaded,
total: total,
});
const uploadComposeSuccess = (composeId: string, media: APIEntity, file: File) => ({
type: COMPOSE_UPLOAD_SUCCESS,
id: composeId,
media: media,
file,
skipLoading: true,
});
const uploadComposeFail = (composeId: string, error: AxiosError | true) => ({
type: COMPOSE_UPLOAD_FAIL,
id: composeId,
error: error,
skipLoading: true,
});
const changeUploadCompose = (composeId: string, id: string, params: Record<string, any>) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
@ -507,34 +634,6 @@ const changeUploadComposeFail = (composeId: string, id: string, error: AxiosErro
skipLoading: true,
});
const uploadComposeRequest = (composeId: string) => ({
type: COMPOSE_UPLOAD_REQUEST,
id: composeId,
skipLoading: true,
});
const uploadComposeProgress = (composeId: string, loaded: number, total: number) => ({
type: COMPOSE_UPLOAD_PROGRESS,
id: composeId,
loaded: loaded,
total: total,
});
const uploadComposeSuccess = (composeId: string, media: APIEntity, file: File) => ({
type: COMPOSE_UPLOAD_SUCCESS,
id: composeId,
media: media,
file,
skipLoading: true,
});
const uploadComposeFail = (composeId: string, error: AxiosError | true) => ({
type: COMPOSE_UPLOAD_FAIL,
id: composeId,
error: error,
skipLoading: true,
});
const undoUploadCompose = (composeId: string, media_id: string) => ({
type: COMPOSE_UPLOAD_UNDO,
id: composeId,
@ -1001,6 +1100,11 @@ export {
submitComposeRequest,
submitComposeSuccess,
submitComposeFail,
uploadFiles,
uploadFilesRequest,
uploadFilesSuccess,
uploadFilesProgress,
uploadFilesFail,
uploadCompose,
changeUploadCompose,
changeUploadComposeRequest,

View file

@ -68,9 +68,9 @@
@apply w-5 h-5 m-0;
}
/* Hide Markdown images (Pleroma) */
/* Markdown inline images (Pleroma) */
[data-markup] img:not(.emojione) {
@apply hidden;
@apply max-h-[500px] rounded-sm;
}
/* User setting to underline links */

View file

@ -10,7 +10,7 @@ const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media attachment' },
});
const onlyImages = (types: ImmutableList<string>) => {
export const onlyImages = (types: ImmutableList<string>) => {
return Boolean(types && types.every(type => type.startsWith('image/')));
};

View file

@ -17,6 +17,7 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { useFeatures, useInstance } from 'soapbox/hooks';
import { EmojiNode } from './emoji-node';
import { ImageNode } from './image-node';
import { MentionNode } from './mention-node';
import type { Klass, LexicalNode } from 'lexical';
@ -45,6 +46,7 @@ const useNodes = () => {
}
if (instance.pleroma.getIn(['metadata', 'markup', 'allow_headings'])) nodes.push(HeadingNode);
if (instance.pleroma.getIn(['metadata', 'markup', 'allow_inline_images'])) nodes.push(ImageNode);
return nodes;
};

View file

@ -21,11 +21,73 @@ import {
import { useCallback, useEffect, useRef, useState } from 'react';
import * as React from 'react';
import { createPortal } from 'react-dom';
import { useIntl } from 'react-intl';
import { uploadFiles } from 'soapbox/actions/compose';
import { useAppDispatch, useInstance } from 'soapbox/hooks';
import { onlyImages } from '../../components/upload-button';
import { $createImageNode } from '../nodes/image-node';
import { setFloatingElemPosition } from '../utils/set-floating-elem-position';
import { ToolbarButton } from './floating-text-format-toolbar-plugin';
import type { List as ImmutableList } from 'immutable';
interface IUploadButton {
onSelectFile: (src: string) => void
}
const UploadButton: React.FC<IUploadButton> = ({ onSelectFile }) => {
const intl = useIntl();
const { configuration } = useInstance();
const dispatch = useAppDispatch();
const fileElement = useRef<HTMLInputElement>(null);
const attachmentTypes = configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>;
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (e.target.files?.length) {
// @ts-ignore
dispatch(uploadFiles([e.target.files.item(0)] as any, intl));
}
};
const handleClick = () => {
fileElement.current?.click();
};
// if (unavailable) {
// return null;
// }
const src = (
onlyImages(attachmentTypes)
? require('@tabler/icons/photo.svg')
: require('@tabler/icons/paperclip.svg')
);
return (
<label>
<ToolbarButton
onClick={handleClick}
aria-label='Upload media'
icon={src}
/>
<input
// key={resetFileKey}
ref={fileElement}
type='file'
multiple
accept={attachmentTypes && attachmentTypes.toArray().join(',')}
onChange={handleChange}
// disabled={disabled}
className='hidden'
/>
</label>
);
};
const BlockTypeFloatingToolbar = ({
editor,
anchorElem,
@ -112,6 +174,16 @@ const BlockTypeFloatingToolbar = ({
});
};
const createImage = (src: string) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
const selectionNode = selection.anchor.getNode();
selectionNode.replace($createImageNode({ src }));
}
});
};
return (
<div
ref={popupCharStylesEditorRef}
@ -124,6 +196,7 @@ const BlockTypeFloatingToolbar = ({
aria-label='Insert horizontal line'
icon={require('@tabler/icons/line-dashed.svg')}
/>
<UploadButton onSelectFile={createImage} />
</>
)}
</div>