WIP image upload, this needs cleanup
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
3c5025c7f3
commit
f55a76886f
5 changed files with 212 additions and 33 deletions
|
@ -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,
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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/')));
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue