diff options
Diffstat (limited to 'FrontEnd/src')
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx | 143 |
1 files changed, 59 insertions, 84 deletions
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx index 09d066cc..05bd9ed6 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -1,7 +1,7 @@ import React from "react"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; -import { Button, Spinner, Row, Col, Form } from "react-bootstrap"; +import { Row, Col, Form } from "react-bootstrap"; import { UiLogicError } from "@/common"; @@ -15,50 +15,31 @@ import { import { pushAlert } from "@/services/alert"; import { base64 } from "@/http/common"; +import BlobImage from "../common/BlobImage"; +import LoadingButton from "../common/LoadingButton"; + interface TimelinePostEditImageProps { - onSelect: (blob: Blob | null) => void; + onSelect: (file: File | null) => void; } const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { const { onSelect } = props; + const { t } = useTranslation(); const [file, setFile] = React.useState<File | null>(null); - const [fileUrl, setFileUrl] = React.useState<string | null>(null); - const [error, setError] = React.useState<string | null>(null); + const [error, setError] = React.useState<boolean>(false); - React.useEffect(() => { - if (file != null) { - const url = URL.createObjectURL(file); - setFileUrl(url); - return () => { - URL.revokeObjectURL(url); - }; - } - }, [file]); - - const onInputChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback( - (e) => { - const files = e.target.files; - if (files == null || files.length === 0) { - setFile(null); - setFileUrl(null); - } else { - setFile(files[0]); - } + const onInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { + setError(false); + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); onSelect(null); - setError(null); - }, - [onSelect] - ); - - const onImgLoad = React.useCallback(() => { - onSelect(file); - }, [onSelect, file]); - - const onImgError = React.useCallback(() => { - setError("loadImageError"); - }, []); + } else { + setFile(files[0]); + } + }; return ( <> @@ -68,15 +49,20 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { accept="image/*" className="mx-3 my-1 d-inline-block" /> - {fileUrl && error == null && ( - <img - src={fileUrl} + {file != null && error == null && ( + <BlobImage + blob={file} className="timeline-post-edit-image" - onLoad={onImgLoad} - onError={onImgError} + onLoad={() => onSelect(file)} + onError={() => { + onSelect(null); + setError(true); + }} /> )} - {error != null && <div className="text-danger">{t(error)}</div>} + {error != null && ( + <div className="text-danger">{t("loadImageError")}</div> + )} </> ); }; @@ -93,10 +79,10 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { const { t } = useTranslation(); - const [state, setState] = React.useState<"input" | "process">("input"); + const [process, setProcess] = React.useState<boolean>(false); const [kind, setKind] = React.useState<"text" | "image">("text"); const [text, setText] = React.useState<string>(""); - const [imageBlob, setImageBlob] = React.useState<Blob | null>(null); + const [image, setImage] = React.useState<File | null>(null); const draftLocalStorageKey = `timeline.${timeline.name}.postDraft`; @@ -104,7 +90,9 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); }, [draftLocalStorageKey]); - const canSend = kind === "text" || (kind === "image" && imageBlob != null); + const canSend = + (kind === "text" && text.length !== 0) || + (kind === "image" && image != null); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef<HTMLDivElement>(null!); @@ -128,11 +116,11 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { const toggleKind = React.useCallback(() => { setKind((oldKind) => (oldKind === "text" ? "image" : "text")); - setImageBlob(null); + setImage(null); }, []); const onSend = async (): Promise<void> => { - setState("process"); + setProcess(true); let requestData: HttpTimelinePostPostRequestData; switch (kind) { @@ -143,14 +131,14 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { }; break; case "image": - if (imageBlob == null) { + if (image == null) { throw new UiLogicError( "Content type is image but image blob is null." ); } requestData = { - contentType: imageBlob.type, - data: await base64(imageBlob), + contentType: image.type, + data: await base64(image), }; break; default: @@ -167,7 +155,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { setText(""); window.localStorage.removeItem(draftLocalStorageKey); } - setState("input"); + setProcess(false); setKind("text"); onPosted(data); }, @@ -176,15 +164,11 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { type: "danger", message: "timeline.sendPostFailed", }); - setState("input"); + setProcess(false); } ); }; - const onImageSelect = React.useCallback((blob: Blob | null) => { - setImageBlob(blob); - }, []); - return ( <div ref={containerRef} @@ -197,7 +181,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { as="textarea" className="w-100 h-100 timeline-post-edit" value={text} - disabled={state === "process"} + disabled={process} onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => { const value = event.currentTarget.value; setText(value); @@ -205,37 +189,28 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { }} /> ) : ( - <TimelinePostEditImage onSelect={onImageSelect} /> + <TimelinePostEditImage onSelect={setImage} /> )} </Col> <Col xs="auto" className="align-self-end m-1"> - {(() => { - if (state === "input") { - return ( - <> - <div className="d-block text-center mt-1 mb-2"> - <i - onLoad={notifyHeightChange} - className={clsx( - kind === "text" ? "bi-image" : "bi-card-text", - "icon-button" - )} - onClick={toggleKind} - /> - </div> - <Button - variant="primary" - onClick={onSend} - disabled={!canSend} - > - {t("timeline.send")} - </Button> - </> - ); - } else { - return <Spinner variant="primary" animation="border" />; - } - })()} + <div className="d-block text-center mt-1 mb-2"> + <i + onLoad={notifyHeightChange} + className={clsx( + kind === "text" ? "bi-image" : "bi-card-text", + "icon-button" + )} + onClick={process ? undefined : toggleKind} + /> + </div> + <LoadingButton + variant="primary" + onClick={onSend} + disabled={!canSend} + loading={process} + > + {t("timeline.send")} + </LoadingButton> </Col> </Row> </div> |