From 93a90bd8904ffda3686c8308657f69b1782d6e24 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Mon, 15 Feb 2021 01:08:05 +0800
Subject: refactor: Refactor timeline card.

---
 .../views/timeline-common/TimelinePageTemplate.tsx | 213 +++++++++++++--------
 1 file changed, 133 insertions(+), 80 deletions(-)

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

diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx
index 92eb0887..3087c20e 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx
@@ -1,33 +1,39 @@
 import React from "react";
-
-import { UiLogicError } from "@/common";
+import { useTranslation } from "react-i18next";
+import { Spinner } from "react-bootstrap";
 
 import { HttpNetworkError, HttpNotFoundError } from "@/http/common";
-import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
-
-import { TimelineMemberDialog } from "./TimelineMember";
-import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
-import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI";
-
-export interface TimelinePageTemplateProps<TManageItem> {
-  name: string;
-  onManage: (item: TManageItem) => void;
-  UiComponent: React.ComponentType<
-    Omit<TimelinePageTemplateUIProps<TManageItem>, "CardComponent">
-  >;
+import {
+  getHttpTimelineClient,
+  HttpTimelineInfo,
+  HttpTimelinePostInfo,
+} from "@/http/timeline";
+
+import { getAlertHost } from "@/services/alert";
+
+import Timeline from "./Timeline";
+import TimelinePostEdit from "./TimelinePostEdit";
+
+export interface TimelinePageCardProps {
+  timeline: HttpTimelineInfo;
+  collapse: boolean;
+  toggleCollapse: () => void;
+  className?: string;
+  onReload: () => void;
+}
+
+export interface TimelinePageTemplateProps {
+  timelineName: string;
   notFoundI18nKey: string;
   reloadKey: number;
   onReload: () => void;
+  CardComponent: React.ComponentType<TimelinePageCardProps>;
 }
 
-export default function TimelinePageTemplate<TManageItem>(
-  props: TimelinePageTemplateProps<TManageItem>
-): React.ReactElement | null {
-  const { name, reloadKey, onReload } = props;
+const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {
+  const { timelineName, reloadKey, onReload, CardComponent } = props;
 
-  const [dialog, setDialog] = React.useState<null | "property" | "member">(
-    null
-  );
+  const { t } = useTranslation();
 
   const [timeline, setTimeline] = React.useState<
     HttpTimelineInfo | "loading" | "offline" | "notexist" | "error"
@@ -38,7 +44,7 @@ export default function TimelinePageTemplate<TManageItem>(
 
     let subscribe = true;
     void getHttpTimelineClient()
-      .getTimeline(name)
+      .getTimeline(timelineName)
       .then(
         (data) => {
           if (subscribe) {
@@ -61,70 +67,117 @@ export default function TimelinePageTemplate<TManageItem>(
     return () => {
       subscribe = false;
     };
-  }, [name, reloadKey]);
+  }, [timelineName, reloadKey]);
 
-  let dialogElement: React.ReactElement | undefined;
-  const closeDialog = (): void => setDialog(null);
+  const scrollToBottom = React.useCallback(() => {
+    window.scrollTo(0, document.body.scrollHeight);
+  }, []);
 
-  if (dialog === "property") {
-    if (typeof timeline !== "object") {
-      throw new UiLogicError(
-        "Timeline is null but attempt to open change property dialog."
-      );
+  const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0);
+
+  const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0);
+
+  const [newPosts, setNewPosts] = React.useState<HttpTimelinePostInfo[]>([]);
+
+  const reloadTimeline = (): void => {
+    setTimelineReloadKey((old) => old + 1);
+    setNewPosts([]);
+  };
+
+  const onPostEditHeightChange = React.useCallback((height: number): void => {
+    setBottomSpaceHeight(height);
+    if (height === 0) {
+      const alertHost = getAlertHost();
+      if (alertHost != null) {
+        alertHost.style.removeProperty("margin-bottom");
+      }
+    } else {
+      const alertHost = getAlertHost();
+      if (alertHost != null) {
+        alertHost.style.marginBottom = `${height}px`;
+      }
     }
+  }, []);
+
+  const cardCollapseLocalStorageKey = `timeline.${timelineName}.cardCollapse`;
 
-    dialogElement = (
-      <TimelinePropertyChangeDialog
-        open
-        close={closeDialog}
-        timeline={timeline}
-        onChange={onReload}
-      />
+  const [cardCollapse, setCardCollapse] = React.useState<boolean>(true);
+  React.useEffect(() => {
+    const savedCollapse =
+      window.localStorage.getItem(cardCollapseLocalStorageKey) === "true";
+    setCardCollapse(savedCollapse);
+  }, [cardCollapseLocalStorageKey]);
+
+  const toggleCardCollapse = (): void => {
+    const newState = !cardCollapse;
+    setCardCollapse(newState);
+    window.localStorage.setItem(
+      cardCollapseLocalStorageKey,
+      newState.toString()
     );
-  } else if (dialog === "member") {
-    if (typeof timeline !== "object") {
-      throw new UiLogicError(
-        "Timeline is null but attempt to open change property dialog."
-      );
-    }
+  };
+
+  let body: React.ReactElement;
 
-    dialogElement = (
-      <TimelineMemberDialog
-        open
-        onClose={closeDialog}
-        timeline={timeline}
-        onChange={onReload}
-      />
+  if (timeline == "loading") {
+    body = (
+      <div className="full-viewport-center-child">
+        <Spinner variant="primary" animation="grow" />
+      </div>
+    );
+  } else if (timeline === "offline") {
+    // TODO: i18n
+    body = <p className="text-danger">Offline!</p>;
+  } else if (timeline === "notexist") {
+    body = <p className="text-danger">{t(props.notFoundI18nKey)}</p>;
+  } else if (timeline === "error") {
+    // TODO: i18n
+    body = <p className="text-danger">Error!</p>;
+  } else {
+    body = (
+      <>
+        <CardComponent
+          className="timeline-template-card"
+          timeline={timeline}
+          collapse={cardCollapse}
+          toggleCollapse={toggleCardCollapse}
+          onReload={onReload}
+        />
+        <div
+          className="timeline-container"
+          style={{
+            minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`,
+          }}
+        >
+          <Timeline
+            top={40}
+            timelineName={timeline.name}
+            reloadKey={timelineReloadKey}
+            onReload={reloadTimeline}
+            additionalPosts={newPosts}
+            onLoad={scrollToBottom}
+          />
+        </div>
+        {timeline.postable ? (
+          <>
+            <div
+              style={{ height: bottomSpaceHeight }}
+              className="flex-fix-length"
+            />
+            <TimelinePostEdit
+              className="fixed-bottom"
+              timeline={timeline}
+              onHeightChange={onPostEditHeightChange}
+              onPosted={(newPost) => {
+                setNewPosts((old) => [...old, newPost]);
+              }}
+            />
+          </>
+        ) : null}
+      </>
     );
   }
+  return body;
+};
 
-  const { UiComponent } = props;
-
-  return (
-    <>
-      <UiComponent
-        timeline={
-          typeof timeline === "object"
-            ? {
-                ...timeline,
-                operations: {
-                  onManage: timeline.manageable
-                    ? (item) => {
-                        if (item === "property") {
-                          setDialog(item);
-                        } else {
-                          props.onManage(item);
-                        }
-                      }
-                    : undefined,
-                  onMember: () => setDialog("member"),
-                },
-              }
-            : timeline
-        }
-        notExistMessageI18nKey={props.notFoundI18nKey}
-      />
-      {dialogElement}
-    </>
-  );
-}
+export default TimelinePageTemplate;
-- 
cgit v1.2.3

