diff options
author | crupest <crupest@outlook.com> | 2023-07-30 23:47:53 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2023-07-30 23:47:53 +0800 |
commit | 538d6830a0022b49b99695095d85e567b0c86e71 (patch) | |
tree | a0c4d164b05d03f636d603b28f77ca881c16ef10 /FrontEnd/src/pages/timeline/TimelinePostEdit.tsx | |
parent | a148f11c193d35ba489f887ed393aedf58a1c714 (diff) | |
download | timeline-538d6830a0022b49b99695095d85e567b0c86e71.tar.gz timeline-538d6830a0022b49b99695095d85e567b0c86e71.tar.bz2 timeline-538d6830a0022b49b99695095d85e567b0c86e71.zip |
...
Diffstat (limited to 'FrontEnd/src/pages/timeline/TimelinePostEdit.tsx')
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelinePostEdit.tsx | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx b/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx new file mode 100644 index 00000000..c1fa0dd9 --- /dev/null +++ b/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx @@ -0,0 +1,267 @@ +import * as React from "react"; +import { useTranslation } from "react-i18next"; + +import { UiLogicError } from "@/common"; + +import { + getHttpTimelineClient, + HttpTimelineInfo, + HttpTimelinePostInfo, + HttpTimelinePostPostRequestData, +} from "@/http/timeline"; + +import { pushAlert } from "@/services/alert"; + +import base64 from "@/utilities/base64"; + +import BlobImage from "@/views/common/BlobImage"; +import LoadingButton from "@/views/common/button/LoadingButton"; +import PopupMenu from "@/views/common/menu/PopupMenu"; +import MarkdownPostEdit from "./MarkdownPostEdit"; +import TimelinePostEditCard from "./TimelinePostEditCard"; +import IconButton from "@/views/common/button/IconButton"; + +import "./TimelinePostEdit.css"; + +interface TimelinePostEditTextProps { + text: string; + disabled: boolean; + onChange: (text: string) => void; + className?: string; + style?: React.CSSProperties; +} + +const TimelinePostEditText: React.FC<TimelinePostEditTextProps> = (props) => { + const { text, disabled, onChange, className, style } = props; + + return ( + <textarea + value={text} + disabled={disabled} + onChange={(event) => { + onChange(event.target.value); + }} + className={className} + style={style} + /> + ); +}; + +interface TimelinePostEditImageProps { + onSelect: (file: File | null) => void; + disabled: boolean; +} + +const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { + const { onSelect, disabled } = props; + + const { t } = useTranslation(); + + const [file, setFile] = React.useState<File | null>(null); + const [error, setError] = React.useState<boolean>(false); + + const onInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { + setError(false); + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + onSelect(null); + } else { + setFile(files[0]); + } + }; + + React.useEffect(() => { + return () => { + onSelect(null); + }; + }, [onSelect]); + + return ( + <> + <input + type="file" + onChange={onInputChange} + accept="image/*" + disabled={disabled} + className="mx-3 my-1" + /> + {file != null && !error && ( + <BlobImage + blob={file} + className="timeline-post-edit-image" + onLoad={() => onSelect(file)} + onError={() => { + onSelect(null); + setError(true); + }} + /> + )} + {error ? <div className="text-danger">{t("loadImageError")}</div> : null} + </> + ); +}; + +type PostKind = "text" | "markdown" | "image"; + +const postKindIconMap: Record<PostKind, string> = { + text: "fonts", + markdown: "markdown", + image: "image", +}; + +export interface TimelinePostEditProps { + className?: string; + style?: React.CSSProperties; + timeline: HttpTimelineInfo; + onPosted: (newPost: HttpTimelinePostInfo) => void; +} + +const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { + const { timeline, style, className, onPosted } = props; + + const { t } = useTranslation(); + + const [process, setProcess] = React.useState<boolean>(false); + + const [kind, setKind] = React.useState<Exclude<PostKind, "markdown">>("text"); + const [showMarkdown, setShowMarkdown] = React.useState<boolean>(false); + + const [text, setText] = React.useState<string>(""); + const [image, setImage] = React.useState<File | null>(null); + + const draftTextLocalStorageKey = `timeline.${timeline.owner.username}.${timeline.nameV2}.postDraft.text`; + + React.useEffect(() => { + setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? ""); + }, [draftTextLocalStorageKey]); + + const canSend = + (kind === "text" && text.length !== 0) || + (kind === "image" && image != null); + + const onPostError = (): void => { + pushAlert({ + type: "danger", + message: "timeline.sendPostFailed", + }); + }; + + const onSend = async (): Promise<void> => { + setProcess(true); + + let requestData: HttpTimelinePostPostRequestData; + switch (kind) { + case "text": + requestData = { + contentType: "text/plain", + data: await base64(text), + }; + break; + case "image": + if (image == null) { + throw new UiLogicError( + "Content type is image but image blob is null.", + ); + } + requestData = { + contentType: image.type, + data: await base64(image), + }; + break; + default: + throw new UiLogicError("Unknown content type."); + } + + getHttpTimelineClient() + .postPost(timeline.owner.username, timeline.nameV2, { + dataList: [requestData], + }) + .then( + (data) => { + if (kind === "text") { + setText(""); + window.localStorage.removeItem(draftTextLocalStorageKey); + } + setProcess(false); + setKind("text"); + onPosted(data); + }, + () => { + setProcess(false); + onPostError(); + }, + ); + }; + + return ( + <TimelinePostEditCard className={className} style={style}> + {showMarkdown ? ( + <MarkdownPostEdit + className="cru-fill-parent" + onClose={() => setShowMarkdown(false)} + owner={timeline.owner.username} + timeline={timeline.nameV2} + onPosted={onPosted} + onPostError={onPostError} + /> + ) : ( + <div className="row"> + <div className="col px-1 py-1"> + {(() => { + if (kind === "text") { + return ( + <TimelinePostEditText + className="cru-fill-parent timeline-post-edit" + text={text} + disabled={process} + onChange={(t) => { + setText(t); + window.localStorage.setItem(draftTextLocalStorageKey, t); + }} + /> + ); + } else if (kind === "image") { + return ( + <TimelinePostEditImage + onSelect={setImage} + disabled={process} + /> + ); + } + })()} + </div> + <div className="col col-auto align-self-end m-1"> + <div className="d-block cru-text-center mt-1 mb-2"> + <PopupMenu + items={(["text", "image", "markdown"] as const).map((kind) => ({ + type: "button", + text: `timeline.post.type.${kind}`, + iconClassName: postKindIconMap[kind], + onClick: () => { + if (kind === "markdown") { + setShowMarkdown(true); + } else { + setKind(kind); + } + }, + }))} + > + <IconButton large icon={postKindIconMap[kind]} /> + </PopupMenu> + </div> + <LoadingButton + onClick={() => void onSend()} + disabled={!canSend} + loading={process} + > + {t("timeline.send")} + </LoadingButton> + </div> + </div> + )} + </TimelinePostEditCard> + ); +}; + +export default TimelinePostEdit; |