From 28d3fe8bb0a60402ed09ab3c208b577d0607324c Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sun, 7 Mar 2021 16:48:51 +0800
Subject: ...

---
 .../app/views/timeline-common/TimelinePostEdit.tsx | 135 +++++++++++++++------
 1 file changed, 98 insertions(+), 37 deletions(-)

(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
index 5bc5b166..efe5a6ff 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
@@ -18,12 +18,38 @@ import { base64 } from "@/http/common";
 import BlobImage from "../common/BlobImage";
 import LoadingButton from "../common/LoadingButton";
 
+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 (
+    <Form.Control
+      as="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 } = props;
+  const { onSelect, disabled } = props;
 
   const { t } = useTranslation();
 
@@ -47,6 +73,7 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => {
         label={t("chooseImage")}
         onChange={onInputChange}
         accept="image/*"
+        disabled={disabled}
         className="mx-3 my-1 d-inline-block"
       />
       {file != null && !error && (
@@ -78,15 +105,24 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
   const { t } = useTranslation();
 
   const [process, setProcess] = React.useState<boolean>(false);
-  const [kind, setKind] = React.useState<"text" | "image">("text");
+
+  type PostKind = "text" | "markdown" | "image";
+
+  const [kind, setKind] = React.useState<PostKind>("text");
+
   const [text, setText] = React.useState<string>("");
+  const [markdown, setMarkdown] = React.useState<string>("");
   const [image, setImage] = React.useState<File | null>(null);
 
-  const draftLocalStorageKey = `timeline.${timeline.name}.postDraft`;
+  const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`;
+  const draftMarkdownLocalStorageKey = `timeline.${timeline.name}.postDraft.markdown`;
 
   React.useEffect(() => {
-    setText(window.localStorage.getItem(draftLocalStorageKey) ?? "");
-  }, [draftLocalStorageKey]);
+    setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? "");
+    setMarkdown(
+      window.localStorage.getItem(draftMarkdownLocalStorageKey) ?? ""
+    );
+  }, [draftTextLocalStorageKey, draftMarkdownLocalStorageKey]);
 
   const canSend =
     (kind === "text" && text.length !== 0) ||
@@ -102,9 +138,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
   };
 
   React.useEffect(() => {
-    if (onHeightChange) {
-      onHeightChange(containerRef.current.clientHeight);
-    }
+    notifyHeightChange();
     return () => {
       if (onHeightChange) {
         onHeightChange(0);
@@ -112,11 +146,6 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
     };
   });
 
-  const toggleKind = React.useCallback(() => {
-    setKind((oldKind) => (oldKind === "text" ? "image" : "text"));
-    setImage(null);
-  }, []);
-
   const onSend = async (): Promise<void> => {
     setProcess(true);
 
@@ -128,6 +157,12 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
           data: await base64(new Blob([text])),
         };
         break;
+      case "markdown":
+        requestData = {
+          contentType: "text/markdown",
+          data: await base64(new Blob([markdown])),
+        };
+        break;
       case "image":
         if (image == null) {
           throw new UiLogicError(
@@ -151,7 +186,10 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
         (data) => {
           if (kind === "text") {
             setText("");
-            window.localStorage.removeItem(draftLocalStorageKey);
+            window.localStorage.removeItem(draftTextLocalStorageKey);
+          } else if (kind === "markdown") {
+            setMarkdown("");
+            window.localStorage.removeItem(draftMarkdownLocalStorageKey);
           }
           setProcess(false);
           setKind("text");
@@ -174,32 +212,55 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
     >
       <Row>
         <Col className="px-1 py-1">
-          {kind === "text" ? (
-            <Form.Control
-              as="textarea"
-              className="w-100 h-100 timeline-post-edit"
-              value={text}
-              disabled={process}
-              onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
-                const value = event.currentTarget.value;
-                setText(value);
-                window.localStorage.setItem(draftLocalStorageKey, value);
-              }}
-            />
-          ) : (
-            <TimelinePostEditImage onSelect={setImage} />
-          )}
+          {(() => {
+            if (kind === "text") {
+              return (
+                <TimelinePostEditText
+                  className="w-100 h-100 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} />
+              );
+            } else if (kind === "markdown") {
+              return (
+                <TimelinePostEditText
+                  className="w-100 h-100 timeline-post-edit"
+                  text={markdown}
+                  disabled={process}
+                  onChange={(t) => {
+                    setMarkdown(t);
+                    window.localStorage.setItem(
+                      draftMarkdownLocalStorageKey,
+                      t
+                    );
+                  }}
+                />
+              );
+            }
+          })()}
         </Col>
         <Col xs="auto" className="align-self-end m-1">
           <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}
-            />
+            <Form.Control
+              as="select"
+              value={kind}
+              onChange={(event) => {
+                const { value } = event.currentTarget;
+                setKind(value as PostKind);
+              }}
+            >
+              <option value="text">text</option>
+              <option value="image">image</option>
+              <option value="markdown">markdown</option>
+            </Form.Control>
           </div>
           <LoadingButton
             variant="primary"
-- 
cgit v1.2.3


From 8c03762cbcb7ddef93b099e79a5def43d7ecb611 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sun, 7 Mar 2021 16:50:05 +0800
Subject: ...

---
 FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(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
index efe5a6ff..9d742514 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
@@ -126,7 +126,8 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
 
   const canSend =
     (kind === "text" && text.length !== 0) ||
-    (kind === "image" && image != null);
+    (kind === "image" && image != null) ||
+    (kind === "markdown" && markdown.length !== 0);
 
   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   const containerRef = React.useRef<HTMLDivElement>(null!);
-- 
cgit v1.2.3


From fa3b2bad71eae374d639073077030af9c5a908ff Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sun, 7 Mar 2021 22:12:05 +0800
Subject: ...

---
 FrontEnd/src/app/locales/en/translation.json       |  5 +++
 FrontEnd/src/app/locales/zh/translation.json       |  5 +++
 FrontEnd/src/app/views/common/Menu.tsx             |  5 ++-
 FrontEnd/src/app/views/common/common.sass          |  3 ++
 .../app/views/timeline-common/TimelinePostEdit.tsx | 44 +++++++++++++++-------
 5 files changed, 48 insertions(+), 14 deletions(-)

(limited to 'FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx')

diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json
index 63b2a1be..4002ee4f 100644
--- a/FrontEnd/src/app/locales/en/translation.json
+++ b/FrontEnd/src/app/locales/en/translation.json
@@ -99,6 +99,11 @@
       "notMatch": "Name does not match."
     },
     "post": {
+      "type": {
+        "text": "Plain Text",
+        "markdown": "Markdown",
+        "image": "Image"
+      },
       "deleteDialog": {
         "title": "Confirm Delete",
         "prompt": "Are you sure to delete the post? This operation is not recoverable."
diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json
index 296966c4..3f966d7c 100644
--- a/FrontEnd/src/app/locales/zh/translation.json
+++ b/FrontEnd/src/app/locales/zh/translation.json
@@ -99,6 +99,11 @@
       "notMatch": "名字不匹配"
     },
     "post": {
+      "type": {
+        "text": "纯文本",
+        "markdown": "Markdown",
+        "image": "图片"
+      },
       "deleteDialog": {
         "title": "确认删除",
         "prompt": "确定删除这个消息？这个操作不可撤销。"
diff --git a/FrontEnd/src/app/views/common/Menu.tsx b/FrontEnd/src/app/views/common/Menu.tsx
index c2110c9c..54650f22 100644
--- a/FrontEnd/src/app/views/common/Menu.tsx
+++ b/FrontEnd/src/app/views/common/Menu.tsx
@@ -12,6 +12,7 @@ export type MenuItem =
   | {
       type: "button";
       text: I18nText;
+      iconClassName?: string;
       color?: BootstrapThemeColor;
       onClick: () => void;
     };
@@ -44,6 +45,9 @@ const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => {
                 onItemClicked?.();
               }}
             >
+              {item.iconClassName != null ? (
+                <i className={clsx(item.iconClassName, "cru-menu-item-icon")} />
+              ) : null}
               {convertI18nText(item.text, t)}
             </div>
           );
@@ -67,7 +71,6 @@ export const PopupMenu: React.FC<PopupMenuProps> = ({ items, children }) => {
   return (
     <OverlayTrigger
       trigger="click"
-      placement="bottom"
       rootClose
       overlay={
         <Popover id="menu-popover">
diff --git a/FrontEnd/src/app/views/common/common.sass b/FrontEnd/src/app/views/common/common.sass
index 819408a0..0a30d995 100644
--- a/FrontEnd/src/app/views/common/common.sass
+++ b/FrontEnd/src/app/views/common/common.sass
@@ -87,5 +87,8 @@
         color: white
         background-color: $value
 
+.cru-menu-item-icon
+  margin-right: 1em
+
 .cru-menu-divider
   border-top: 1px solid $gray-200
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
index 9d742514..6c428b74 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
@@ -17,6 +17,7 @@ import { base64 } from "@/http/common";
 
 import BlobImage from "../common/BlobImage";
 import LoadingButton from "../common/LoadingButton";
+import { PopupMenu } from "../common/Menu";
 
 interface TimelinePostEditTextProps {
   text: string;
@@ -67,6 +68,12 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => {
     }
   };
 
+  React.useEffect(() => {
+    return () => {
+      onSelect(null);
+    };
+  }, [onSelect]);
+
   return (
     <>
       <Form.File
@@ -92,6 +99,14 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => {
   );
 };
 
+type PostKind = "text" | "markdown" | "image";
+
+const postKindIconClassNameMap: Record<PostKind, string> = {
+  text: "bi-fonts",
+  markdown: "bi-markdown",
+  image: "bi-image",
+};
+
 export interface TimelinePostEditProps {
   className?: string;
   timeline: HttpTimelineInfo;
@@ -106,8 +121,6 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
 
   const [process, setProcess] = React.useState<boolean>(false);
 
-  type PostKind = "text" | "markdown" | "image";
-
   const [kind, setKind] = React.useState<PostKind>("text");
 
   const [text, setText] = React.useState<string>("");
@@ -250,18 +263,23 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
         </Col>
         <Col xs="auto" className="align-self-end m-1">
           <div className="d-block text-center mt-1 mb-2">
-            <Form.Control
-              as="select"
-              value={kind}
-              onChange={(event) => {
-                const { value } = event.currentTarget;
-                setKind(value as PostKind);
-              }}
+            <PopupMenu
+              items={(["text", "image", "markdown"] as const).map((kind) => ({
+                type: "button",
+                text: `timeline.post.type.${kind}`,
+                iconClassName: postKindIconClassNameMap[kind],
+                onClick: () => {
+                  setKind(kind);
+                },
+              }))}
             >
-              <option value="text">text</option>
-              <option value="image">image</option>
-              <option value="markdown">markdown</option>
-            </Form.Control>
+              <i
+                className={clsx(
+                  postKindIconClassNameMap[kind],
+                  "icon-button large"
+                )}
+              />
+            </PopupMenu>
           </div>
           <LoadingButton
             variant="primary"
-- 
cgit v1.2.3

