aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/pages/timeline/TimelinePostEdit.tsx
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-07-30 23:47:53 +0800
committercrupest <crupest@outlook.com>2023-07-30 23:47:53 +0800
commit538d6830a0022b49b99695095d85e567b0c86e71 (patch)
treea0c4d164b05d03f636d603b28f77ca881c16ef10 /FrontEnd/src/pages/timeline/TimelinePostEdit.tsx
parenta148f11c193d35ba489f887ed393aedf58a1c714 (diff)
downloadtimeline-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.tsx267
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;