Add uploadFile function, allow uploading images in Lexical

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-lexical-ujdd17/deployments/3671
marcin mikołajczak 2023-07-21 20:24:28 +02:00
rodzic f55a76886f
commit 79200cae0f
6 zmienionych plików z 535 dodań i 213 usunięć

Wyświetl plik

@ -11,12 +11,10 @@ import { tagHistory } from 'soapbox/settings';
import toast from 'soapbox/toast';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures, parseVersion } from 'soapbox/utils/features';
import { formatBytes, getVideoDuration } from 'soapbox/utils/media';
import resizeImage from 'soapbox/utils/resize-image';
import { useEmoji } from './emojis';
import { importFetchedAccounts } from './importer';
import { uploadMedia, fetchMedia, updateMedia } from './media';
import { uploadFile, updateMedia } from './media';
import { openModal, closeModal } from './modals';
import { getSettings } from './settings';
import { createStatus } from './statuses';
@ -33,11 +31,6 @@ 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;
@ -96,9 +89,6 @@ const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const;
const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
const messages = defineMessages({
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' },
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
@ -393,109 +383,10 @@ 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;
const attachmentLimit = getState().instance.configuration.getIn(['statuses', 'max_media_attachments']) as number;
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 media = getState().compose.get(composeId)?.media_attachments;
const progress = new Array(files.length).fill(0);
@ -513,62 +404,18 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
Array.from(files).forEach(async(f, i) => {
if (mediaCount + i > attachmentLimit - 1) return;
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(uploadComposeFail(composeId, true));
return;
} else if (isVideo && maxVideoSize && (f.size > maxVideoSize)) {
const limit = formatBytes(maxVideoSize);
const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit });
toast.error(message);
dispatch(uploadComposeFail(composeId, true));
return;
} else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) {
const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration });
toast.error(message);
dispatch(uploadComposeFail(composeId, 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) => {
dispatch(uploadFile(
f,
intl,
(data) => dispatch(uploadComposeSuccess(composeId, data, f)),
(error) => dispatch(uploadComposeFail(composeId, error)),
({ loaded }: any) => {
progress[i] = loaded;
dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), total));
};
},
(value) => total += value,
));
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(uploadComposeSuccess(composeId, data, f));
} else if (status === 202) {
const poll = () => {
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
if (status === 200) {
dispatch(uploadComposeSuccess(composeId, data, f));
} else if (status === 206) {
setTimeout(() => poll(), 1000);
}
}).catch(error => dispatch(uploadComposeFail(composeId, error)));
};
poll();
}
});
}).catch(error => dispatch(uploadComposeFail(composeId, error)));
});
};
@ -1100,11 +947,7 @@ export {
submitComposeRequest,
submitComposeSuccess,
submitComposeFail,
uploadFiles,
uploadFilesRequest,
uploadFilesSuccess,
uploadFilesProgress,
uploadFilesFail,
uploadFile,
uploadCompose,
changeUploadCompose,
changeUploadComposeRequest,

Wyświetl plik

@ -2,11 +2,9 @@ import { defineMessages, IntlShape } from 'react-intl';
import api, { getLinks } from 'soapbox/api';
import toast from 'soapbox/toast';
import { formatBytes } from 'soapbox/utils/media';
import resizeImage from 'soapbox/utils/resize-image';
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
import { fetchMedia, uploadMedia } from './media';
import { uploadFile } from './media';
import { closeModal, openModal } from './modals';
import {
STATUS_FETCH_SOURCE_FAIL,
@ -154,52 +152,21 @@ const changeEditEventLocation = (value: string | null) =>
};
const uploadEventBanner = (file: File, intl: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
(dispatch: AppDispatch) => {
let progress = 0;
dispatch(uploadEventBannerRequest());
if (maxImageSize && (file.size > maxImageSize)) {
const limit = formatBytes(maxImageSize);
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
toast.error(message);
dispatch(uploadEventBannerFail(true));
return;
}
resizeImage(file).then(file => {
const data = new FormData();
data.append('file', file);
// Account for disparity in size of original image and resized data
const onUploadProgress = ({ loaded }: any) => {
dispatch(uploadFile(
file,
intl,
(data) => dispatch(uploadEventBannerSuccess(data, file)),
(error) => dispatch(uploadEventBannerFail(error)),
({ loaded }: any) => {
progress = loaded;
dispatch(uploadEventBannerProgress(progress));
};
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(uploadEventBannerSuccess(data, file));
} else if (status === 202) {
const poll = () => {
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
if (status === 200) {
dispatch(uploadEventBannerSuccess(data, file));
} else if (status === 206) {
setTimeout(() => poll(), 1000);
}
}).catch(error => dispatch(uploadEventBannerFail(error)));
};
poll();
}
});
}).catch(error => dispatch(uploadEventBannerFail(error)));
},
));
};
const uploadEventBannerRequest = () => ({

Wyświetl plik

@ -1,8 +1,22 @@
import { defineMessages, type IntlShape } from 'react-intl';
import toast from 'soapbox/toast';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
import { formatBytes, getVideoDuration } from 'soapbox/utils/media';
import resizeImage from 'soapbox/utils/resize-image';
import api from '../api';
import type { AxiosError } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities';
const messages = defineMessages({
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' },
});
const noOp = (e: any) => {};
@ -41,10 +55,78 @@ const uploadMedia = (data: FormData, onUploadProgress = noOp) =>
}
};
const uploadFile = (
file: File,
intl: IntlShape,
onSuccess: (data: APIEntity) => void = () => {},
onFail: (error: AxiosError | true) => void = () => {},
onProgress: (loaded: number) => void = () => {},
changeTotal: (value: number) => void = () => {},
) =>
async (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 isImage = file.type.match(/image.*/);
const isVideo = file.type.match(/video.*/);
const videoDurationInSeconds = (isVideo && maxVideoDuration) ? await getVideoDuration(file) : 0;
if (isImage && maxImageSize && (file.size > maxImageSize)) {
const limit = formatBytes(maxImageSize);
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
toast.error(message);
onFail(true);
return;
} else if (isVideo && maxVideoSize && (file.size > maxVideoSize)) {
const limit = formatBytes(maxVideoSize);
const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit });
toast.error(message);
onFail(true);
return;
} else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) {
const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration });
toast.error(message);
onFail(true);
return;
}
// FIXME: Don't define const in loop
resizeImage(file).then(resized => {
const data = new FormData();
data.append('file', resized);
// Account for disparity in size of original image and resized data
changeTotal(resized.size - file.size);
return dispatch(uploadMedia(data, onProgress))
.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) {
onSuccess(data);
} else if (status === 202) {
const poll = () => {
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
if (status === 200) {
onSuccess(data);
} else if (status === 206) {
setTimeout(() => poll(), 1000);
}
}).catch(error => onFail(error));
};
poll();
}
});
}).catch(error => onFail(error));
};
export {
fetchMedia,
updateMedia,
uploadMediaV1,
uploadMediaV2,
uploadMedia,
uploadFile,
};

Wyświetl plik

@ -0,0 +1,253 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
import { mergeRegister } from '@lexical/utils';
import {
$getNodeByKey,
$getSelection,
$isNodeSelection,
$setSelection,
CLICK_COMMAND,
COMMAND_PRIORITY_LOW,
DRAGSTART_COMMAND,
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
KEY_ENTER_COMMAND,
KEY_ESCAPE_COMMAND,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import * as React from 'react';
import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { $isImageNode } from './image-node';
import type {
GridSelection,
LexicalEditor,
NodeKey,
NodeSelection,
RangeSelection,
} from 'lexical';
const imageCache = new Set();
function useSuspenseImage(src: string) {
if (!imageCache.has(src)) {
throw new Promise((resolve) => {
const img = new Image();
img.src = src;
img.onload = () => {
imageCache.add(src);
resolve(null);
};
});
}
}
function LazyImage({
altText,
className,
imageRef,
src,
}: {
altText: string
className: string | null
imageRef: {current: null | HTMLImageElement}
src: string
}): JSX.Element {
useSuspenseImage(src);
return (
<img
className={className || undefined}
src={src}
alt={altText}
ref={imageRef}
draggable='false'
/>
);
}
export default function ImageComponent({
src,
altText,
nodeKey,
}: {
altText: string
nodeKey: NodeKey
src: string
}): JSX.Element {
const imageRef = useRef<null | HTMLImageElement>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const [isSelected, setSelected, clearSelection] =
useLexicalNodeSelection(nodeKey);
const [editor] = useLexicalComposerContext();
const [selection, setSelection] = useState<
RangeSelection | NodeSelection | GridSelection | null
>(null);
const activeEditorRef = useRef<LexicalEditor | null>(null);
const onDelete = useCallback(
(payload: KeyboardEvent) => {
if (isSelected && $isNodeSelection($getSelection())) {
const event: KeyboardEvent = payload;
event.preventDefault();
const node = $getNodeByKey(nodeKey);
if ($isImageNode(node)) {
node.remove();
}
}
return false;
},
[isSelected, nodeKey],
);
const onEnter = useCallback(
(event: KeyboardEvent) => {
const latestSelection = $getSelection();
const buttonElem = buttonRef.current;
if (
isSelected &&
$isNodeSelection(latestSelection) &&
latestSelection.getNodes().length === 1
) {
if (
buttonElem !== null &&
buttonElem !== document.activeElement
) {
event.preventDefault();
buttonElem.focus();
return true;
}
}
return false;
},
[isSelected],
);
const onEscape = useCallback(
(event: KeyboardEvent) => {
if (buttonRef.current === event.target) {
$setSelection(null);
editor.update(() => {
setSelected(true);
const parentRootElement = editor.getRootElement();
if (parentRootElement !== null) {
parentRootElement.focus();
}
});
return true;
}
return false;
},
[editor, setSelected],
);
useEffect(() => {
let isMounted = true;
const unregister = mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
if (isMounted) {
setSelection(editorState.read(() => $getSelection()));
}
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_, activeEditor) => {
activeEditorRef.current = activeEditor;
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand<MouseEvent>(
CLICK_COMMAND,
(payload) => {
const event = payload;
if (event.target === imageRef.current) {
if (event.shiftKey) {
setSelected(!isSelected);
} else {
clearSelection();
setSelected(true);
}
return true;
}
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
DRAGSTART_COMMAND,
(event) => {
if (event.target === imageRef.current) {
// TODO This is just a temporary workaround for FF to behave like other browsers.
// Ideally, this handles drag & drop too (and all browsers).
event.preventDefault();
return true;
}
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
KEY_DELETE_COMMAND,
onDelete,
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
KEY_BACKSPACE_COMMAND,
onDelete,
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(KEY_ENTER_COMMAND, onEnter, COMMAND_PRIORITY_LOW),
editor.registerCommand(
KEY_ESCAPE_COMMAND,
onEscape,
COMMAND_PRIORITY_LOW,
),
);
return () => {
isMounted = false;
unregister();
};
}, [
clearSelection,
editor,
isSelected,
nodeKey,
onDelete,
onEnter,
onEscape,
setSelected,
]);
const draggable = isSelected && $isNodeSelection(selection);
const isFocused = isSelected;
return (
<Suspense fallback={null}>
<>
<div draggable={draggable}>
<LazyImage
className={
isFocused
? `focused ${$isNodeSelection(selection) ? 'draggable' : ''}`
: null
}
src={src}
altText={altText}
imageRef={imageRef}
/>
</div>
</>
</Suspense>
);
}

Wyświetl plik

@ -0,0 +1,173 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { $applyNodeReplacement, DecoratorNode } from 'lexical';
import * as React from 'react';
import { Suspense } from 'react';
import type {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
LexicalNode,
NodeKey,
SerializedLexicalNode,
Spread,
} from 'lexical';
const ImageComponent = React.lazy(() => import('./image-component'));
interface ImagePayload {
altText?: string
key?: NodeKey
src: string
}
function convertImageElement(domNode: Node): null | DOMConversionOutput {
if (domNode instanceof HTMLImageElement) {
const { alt: altText, src } = domNode;
const node = $createImageNode({ altText, src });
return { node };
}
return null;
}
type SerializedImageNode = Spread<
{
altText: string
src: string
},
SerializedLexicalNode
>;
class ImageNode extends DecoratorNode<JSX.Element> {
__src: string;
__altText: string;
static getType(): string {
return 'image';
}
static clone(node: ImageNode): ImageNode {
return new ImageNode(
node.__src,
node.__altText,
node.__key,
);
}
static importJSON(serializedNode: SerializedImageNode): ImageNode {
const { altText, src } =
serializedNode;
const node = $createImageNode({
altText,
src,
});
return node;
}
exportDOM(): DOMExportOutput {
const element = document.createElement('img');
element.setAttribute('src', this.__src);
element.setAttribute('alt', this.__altText);
return { element };
}
static importDOM(): DOMConversionMap | null {
return {
img: (node: Node) => ({
conversion: convertImageElement,
priority: 0,
}),
};
}
constructor(
src: string,
altText: string,
key?: NodeKey,
) {
super(key);
this.__src = src;
this.__altText = altText;
}
exportJSON(): SerializedImageNode {
return {
altText: this.getAltText(),
src: this.getSrc(),
type: 'image',
version: 1,
};
}
// View
createDOM(config: EditorConfig): HTMLElement {
const span = document.createElement('span');
const theme = config.theme;
const className = theme.image;
if (className !== undefined) {
span.className = className;
}
return span;
}
updateDOM(): false {
return false;
}
getSrc(): string {
return this.__src;
}
getAltText(): string {
return this.__altText;
}
decorate(): JSX.Element {
return (
<Suspense fallback={null}>
<ImageComponent
src={this.__src}
altText={this.__altText}
nodeKey={this.getKey()}
/>
</Suspense>
);
}
}
function $createImageNode({
altText = '',
src,
key,
}: ImagePayload): ImageNode {
return $applyNodeReplacement(
new ImageNode(
src,
altText,
key,
),
);
}
const $isImageNode = (
node: LexicalNode | null | undefined,
): node is ImageNode => node instanceof ImageNode;
export {
type ImagePayload,
type SerializedImageNode,
ImageNode,
$createImageNode,
$isImageNode,
};

Wyświetl plik

@ -23,7 +23,7 @@ import * as React from 'react';
import { createPortal } from 'react-dom';
import { useIntl } from 'react-intl';
import { uploadFiles } from 'soapbox/actions/compose';
import { uploadFile } from 'soapbox/actions/compose';
import { useAppDispatch, useInstance } from 'soapbox/hooks';
import { onlyImages } from '../../components/upload-button';
@ -49,7 +49,11 @@ const UploadButton: React.FC<IUploadButton> = ({ onSelectFile }) => {
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (e.target.files?.length) {
// @ts-ignore
dispatch(uploadFiles([e.target.files.item(0)] as any, intl));
dispatch(uploadFile(
e.target.files.item(0) as File,
intl,
({ url }) => onSelectFile(url),
));
}
};