From 05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- .../app/views/timeline-common/TimelinePostEdit.tsx | 241 +++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx (limited to 'FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx') diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx new file mode 100644 index 00000000..dfa2f879 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -0,0 +1,241 @@ +import React from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import Svg from "react-inlinesvg"; +import { Button, Spinner, Row, Col, Form } from "react-bootstrap"; +import textIcon from "bootstrap-icons/icons/card-text.svg"; +import imageIcon from "bootstrap-icons/icons/image.svg"; + +import { UiLogicError } from "@/common"; + +import { pushAlert } from "@/services/alert"; +import { TimelineCreatePostRequest } from "@/services/timeline"; + +interface TimelinePostEditImageProps { + onSelect: (blob: Blob | null) => void; +} + +const TimelinePostEditImage: React.FC = (props) => { + const { onSelect } = props; + const { t } = useTranslation(); + + const [file, setFile] = React.useState(null); + const [fileUrl, setFileUrl] = React.useState(null); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + if (file != null) { + const url = URL.createObjectURL(file); + setFileUrl(url); + return () => { + URL.revokeObjectURL(url); + }; + } + }, [file]); + + const onInputChange: React.ChangeEventHandler = React.useCallback( + (e) => { + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + setFileUrl(null); + } else { + setFile(files[0]); + } + onSelect(null); + setError(null); + }, + [onSelect] + ); + + const onImgLoad = React.useCallback(() => { + onSelect(file); + }, [onSelect, file]); + + const onImgError = React.useCallback(() => { + setError("loadImageError"); + }, []); + + return ( + <> + + {fileUrl && error == null && ( + + )} + {error != null &&
{t(error)}
} + + ); +}; + +export type TimelinePostSendCallback = ( + content: TimelineCreatePostRequest +) => Promise; + +export interface TimelinePostEditProps { + className?: string; + onPost: TimelinePostSendCallback; + onHeightChange?: (height: number) => void; + timelineUniqueId: string; +} + +const TimelinePostEdit: React.FC = (props) => { + const { onPost } = props; + + const { t } = useTranslation(); + + const [state, setState] = React.useState<"input" | "process">("input"); + const [kind, setKind] = React.useState<"text" | "image">("text"); + const [text, setText] = React.useState(""); + const [imageBlob, setImageBlob] = React.useState(null); + + const draftLocalStorageKey = `timeline.${props.timelineUniqueId}.postDraft`; + + React.useEffect(() => { + setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); + }, [draftLocalStorageKey]); + + const canSend = kind === "text" || (kind === "image" && imageBlob != null); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const containerRef = React.useRef(null!); + + const notifyHeightChange = (): void => { + if (props.onHeightChange) { + props.onHeightChange(containerRef.current.clientHeight); + } + }; + + React.useEffect(() => { + if (props.onHeightChange) { + props.onHeightChange(containerRef.current.clientHeight); + } + return () => { + if (props.onHeightChange) { + props.onHeightChange(0); + } + }; + }); + + const toggleKind = React.useCallback(() => { + setKind((oldKind) => (oldKind === "text" ? "image" : "text")); + setImageBlob(null); + }, []); + + const onSend = React.useCallback(() => { + setState("process"); + + const req: TimelineCreatePostRequest = (() => { + switch (kind) { + case "text": + return { + content: { + type: "text", + text: text, + }, + } as TimelineCreatePostRequest; + case "image": + if (imageBlob == null) { + throw new UiLogicError( + "Content type is image but image blob is null." + ); + } + return { + content: { + type: "image", + data: imageBlob, + }, + } as TimelineCreatePostRequest; + default: + throw new UiLogicError("Unknown content type."); + } + })(); + + onPost(req).then( + (_) => { + if (kind === "text") { + setText(""); + window.localStorage.removeItem(draftLocalStorageKey); + } + setState("input"); + setKind("text"); + }, + (_) => { + pushAlert({ + type: "danger", + message: t("timeline.sendPostFailed"), + }); + setState("input"); + } + ); + }, [onPost, kind, text, imageBlob, t, draftLocalStorageKey]); + + const onImageSelect = React.useCallback((blob: Blob | null) => { + setImageBlob(blob); + }, []); + + return ( +
+ + + {kind === "text" ? ( + ) => { + const value = event.currentTarget.value; + setText(value); + window.localStorage.setItem(draftLocalStorageKey, value); + }} + /> + ) : ( + + )} + + + {(() => { + if (state === "input") { + return ( + <> +
+ +
+ + + ); + } else { + return ; + } + })()} + +
+
+ ); +}; + +export default TimelinePostEdit; -- cgit v1.2.3