From 0833654d91dca0ff36cb6352528f6d00556352c0 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Wed, 13 Jan 2021 14:13:02 +0800
Subject: ...

---
 .../src/app/views/timeline-common/TimelineItem.tsx | 83 +++++-----------------
 1 file changed, 17 insertions(+), 66 deletions(-)

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

diff --git a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
index 233c81bd..74431402 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
@@ -2,44 +2,13 @@ import React from "react";
 import clsx from "clsx";
 import { Link } from "react-router-dom";
 import { useTranslation } from "react-i18next";
-import { Modal, Button } from "react-bootstrap";
 
 import { useAvatar } from "@/services/user";
 import { TimelinePostInfo } from "@/services/timeline";
 
 import BlobImage from "../common/BlobImage";
-
-const TimelinePostDeleteConfirmDialog: React.FC<{
-  onClose: () => void;
-  onConfirm: () => void;
-}> = ({ onClose, onConfirm }) => {
-  const { t } = useTranslation();
-
-  return (
-    <Modal onHide={onClose} show centered>
-      <Modal.Header>
-        <Modal.Title className="text-danger">
-          {t("timeline.post.deleteDialog.title")}
-        </Modal.Title>
-      </Modal.Header>
-      <Modal.Body>{t("timeline.post.deleteDialog.prompt")}</Modal.Body>
-      <Modal.Footer>
-        <Button variant="secondary" onClick={onClose}>
-          {t("operationDialog.cancel")}
-        </Button>
-        <Button
-          variant="danger"
-          onClick={() => {
-            onConfirm();
-            onClose();
-          }}
-        >
-          {t("operationDialog.confirm")}
-        </Button>
-      </Modal.Footer>
-    </Modal>
-  );
-};
+import TimelineLine from "./TimelineLine";
+import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog";
 
 export interface TimelineItemProps {
   post: TimelinePostInfo;
@@ -50,7 +19,6 @@ export interface TimelineItemProps {
     onDelete: () => void;
   };
   onClick?: () => void;
-  onResize?: () => void;
   className?: string;
   style?: React.CSSProperties;
 }
@@ -60,7 +28,7 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
 
   const current = props.current === true;
 
-  const { more, onResize } = props;
+  const { more } = props;
 
   const avatar = useAvatar(props.post.author.username);
 
@@ -68,31 +36,25 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
 
   return (
     <div
-      className={clsx(
-        "timeline-item position-relative",
-        current && "current",
-        props.className
-      )}
+      className={clsx("timeline-item", current && "current", props.className)}
       onClick={props.onClick}
       style={props.style}
     >
-      <div className="timeline-line-area-container">
-        <div className="timeline-line-area">
-          <div className="timeline-line-segment start"></div>
-          <div className="timeline-line-node-container">
-            <div className="timeline-line-node"></div>
-          </div>
-          <div className="timeline-line-segment end"></div>
-          {current && <div className="timeline-line-segment current-end" />}
-        </div>
-      </div>
+      <TimelineLine center="node" current={current} />
       <div className="timeline-item-card">
-        <div>
+        <div className="timeline-item-header">
           <span className="mr-2">
-            <small className="text-secondary white-space-no-wrap mr-2">
-              {props.post.time.toLocaleString(i18n.languages)}
-            </small>
-            <small className="text-dark">{props.post.author.nickname}</small>
+            <span>
+              <Link to={"/users/" + props.post.author.username}>
+                <BlobImage blob={avatar} className="timeline-avatar mr-1" />
+              </Link>
+              <small className="text-dark mr-2">
+                {props.post.author.nickname}
+              </small>
+              <small className="text-secondary white-space-no-wrap">
+                {props.post.time.toLocaleString(i18n.languages)}
+              </small>
+            </span>
           </span>
           {more != null ? (
             <i
@@ -105,16 +67,6 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
           ) : null}
         </div>
         <div className="timeline-content">
-          <Link
-            className="float-left m-2"
-            to={"/users/" + props.post.author.username}
-          >
-            <BlobImage
-              onLoad={onResize}
-              blob={avatar}
-              className="avatar rounded"
-            />
-          </Link>
           {(() => {
             const { content } = props.post;
             if (content.type === "text") {
@@ -122,7 +74,6 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
             } else {
               return (
                 <BlobImage
-                  onLoad={onResize}
                   blob={content.data}
                   className="timeline-content-image"
                 />
-- 
cgit v1.2.3


From 4a633a0f83a3a7cf18feead821fcdff2453de926 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Wed, 13 Jan 2021 17:06:43 +0800
Subject: ...

---
 .../src/app/views/timeline-common/Timeline.tsx     | 84 ++++++++++++++++------
 .../app/views/timeline-common/TimelineDateItem.tsx | 19 +++++
 .../src/app/views/timeline-common/TimelineItem.tsx | 23 +++---
 .../app/views/timeline-common/timeline-common.sass | 18 ++++-
 4 files changed, 105 insertions(+), 39 deletions(-)
 create mode 100644 FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx

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

diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx
index 5c8f9195..ab658b89 100644
--- a/FrontEnd/src/app/views/timeline-common/Timeline.tsx
+++ b/FrontEnd/src/app/views/timeline-common/Timeline.tsx
@@ -5,6 +5,15 @@ import { TimelinePostInfo } from "@/services/timeline";
 
 import TimelineItem from "./TimelineItem";
 import TimelineTop from "./TimelineTop";
+import TimelineDateItem from "./TimelineDateItem";
+
+function dateEqual(left: Date, right: Date): boolean {
+  return (
+    left.getDate() == right.getDate() &&
+    left.getMonth() == right.getMonth() &&
+    left.getFullYear() == right.getFullYear()
+  );
+}
 
 export interface TimelinePostInfoEx extends TimelinePostInfo {
   onDelete?: () => void;
@@ -24,6 +33,31 @@ const Timeline: React.FC<TimelineProps> = (props) => {
 
   const [showMoreIndex, setShowMoreIndex] = React.useState<number>(-1);
 
+  const groupedPosts = React.useMemo<
+    { date: Date; posts: (TimelinePostInfoEx & { index: number })[] }[]
+  >(() => {
+    const result: {
+      date: Date;
+      posts: (TimelinePostInfoEx & { index: number })[];
+    }[] = [];
+    let index = 0;
+    for (const post of posts) {
+      const { time } = post;
+      if (result.length === 0) {
+        result.push({ date: time, posts: [{ ...post, index }] });
+      } else {
+        const lastGroup = result[result.length - 1];
+        if (dateEqual(lastGroup.date, time)) {
+          lastGroup.posts.push({ ...post, index });
+        } else {
+          result.push({ date: time, posts: [{ ...post, index }] });
+        }
+      }
+      index++;
+    }
+    return result;
+  }, [posts]);
+
   return (
     <div
       ref={props.containerRef}
@@ -31,29 +65,33 @@ const Timeline: React.FC<TimelineProps> = (props) => {
       className={clsx("timeline", props.className)}
     >
       <TimelineTop height="56px" />
-      {(() => {
-        const length = posts.length;
-        return posts.map((post, index) => {
-          return (
-            <TimelineItem
-              post={post}
-              key={post.id}
-              current={length - 1 === index}
-              more={
-                post.onDelete != null
-                  ? {
-                      isOpen: showMoreIndex === index,
-                      toggle: () =>
-                        setShowMoreIndex((old) => (old === index ? -1 : index)),
-                      onDelete: post.onDelete,
-                    }
-                  : undefined
-              }
-              onClick={() => setShowMoreIndex(-1)}
-            />
-          );
-        });
-      })()}
+      {groupedPosts.map((group) => {
+        return (
+          <>
+            <TimelineDateItem date={group.date} />
+            {group.posts.map((post) => (
+              <TimelineItem
+                post={post}
+                key={post.id}
+                current={posts.length - 1 === post.index}
+                more={
+                  post.onDelete != null
+                    ? {
+                        isOpen: showMoreIndex === post.index,
+                        toggle: () =>
+                          setShowMoreIndex((old) =>
+                            old === post.index ? -1 : post.index
+                          ),
+                        onDelete: post.onDelete,
+                      }
+                    : undefined
+                }
+                onClick={() => setShowMoreIndex(-1)}
+              />
+            ))}
+          </>
+        );
+      })}
     </div>
   );
 };
diff --git a/FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx
new file mode 100644
index 00000000..bcc1530f
--- /dev/null
+++ b/FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import TimelineLine from "./TimelineLine";
+
+export interface TimelineDateItemProps {
+  date: Date;
+}
+
+const TimelineDateItem: React.FC<TimelineDateItemProps> = ({ date }) => {
+  return (
+    <div className="timeline-date-item">
+      <TimelineLine center={null} />
+      <div className="timeline-date-item-badge">
+        {date.toLocaleDateString()}
+      </div>
+    </div>
+  );
+};
+
+export default TimelineDateItem;
diff --git a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
index 74431402..c096f890 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
@@ -1,7 +1,6 @@
 import React from "react";
 import clsx from "clsx";
 import { Link } from "react-router-dom";
-import { useTranslation } from "react-i18next";
 
 import { useAvatar } from "@/services/user";
 import { TimelinePostInfo } from "@/services/timeline";
@@ -24,8 +23,6 @@ export interface TimelineItemProps {
 }
 
 const TimelineItem: React.FC<TimelineItemProps> = (props) => {
-  const { i18n } = useTranslation();
-
   const current = props.current === true;
 
   const { more } = props;
@@ -42,6 +39,15 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
     >
       <TimelineLine center="node" current={current} />
       <div className="timeline-item-card">
+        {more != null ? (
+          <i
+            className="bi-chevron-down text-info icon-button float-right"
+            onClick={(e) => {
+              more.toggle();
+              e.stopPropagation();
+            }}
+          />
+        ) : null}
         <div className="timeline-item-header">
           <span className="mr-2">
             <span>
@@ -52,19 +58,10 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
                 {props.post.author.nickname}
               </small>
               <small className="text-secondary white-space-no-wrap">
-                {props.post.time.toLocaleString(i18n.languages)}
+                {props.post.time.toLocaleTimeString()}
               </small>
             </span>
           </span>
-          {more != null ? (
-            <i
-              className="bi-chevron-down text-info icon-button"
-              onClick={(e) => {
-                more.toggle();
-                e.stopPropagation();
-              }}
-            />
-          ) : null}
         </div>
         <div className="timeline-content">
           {(() => {
diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass
index 5af3a9ef..ebaf96b5 100644
--- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass
+++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass
@@ -49,7 +49,7 @@ $timeline-line-color-current: #36c2e6
     background: $timeline-line-color
 
     &.start
-      height: 1.4em
+      height: 1.8em
       flex: 0 0 auto
 
     &.end
@@ -102,7 +102,7 @@ $timeline-line-color-current: #36c2e6
 .timeline-item-card
   @extend .cru-card
   position: relative
-  padding: 0.5em 2em 0.5em 4em
+  padding: 0.3em 0.5em 1em 4em
   transition: background 0.5s, padding-left 0.5s
 
   @include media-breakpoint-down(sm)
@@ -114,7 +114,7 @@ $timeline-line-color-current: #36c2e6
 .timeline-item-header
   display: flex
   align-items: center
-  @extend .mb-2
+  @extend .my-2
 
 .timeline-avatar
   border-radius: 50%
@@ -133,6 +133,18 @@ $timeline-line-color-current: #36c2e6
   max-width: 60%
   max-height: 200px
 
+.timeline-date-item
+  position: relative
+  padding: 0.3em 0 0.3em 4em
+
+.timeline-date-item-badge
+  display: inline-block
+  padding: 0.1em 0.4em
+  border-radius: 0.4em
+  background: #7c7c7c
+  color: white
+  font-size: 0.8em
+
 .timeline-post-edit-image
   max-width: 100px
   max-height: 100px
-- 
cgit v1.2.3

