From e40621eb0f320cf262c5ddf0c1c3dc612b793f4c Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 1 Oct 2023 22:14:25 +0800 Subject: [PATCH] chore: implement memo content views --- web/src/components/FloatingNavButton.tsx | 5 + web/src/components/Memo.tsx | 40 +++---- .../components/MemoEditor/Editor/index.tsx | 1 - .../MemoEditor/RelationListView.tsx | 21 ++-- web/src/components/MemoEditor/index.tsx | 1 - web/src/components/MemoList.tsx | 2 +- web/src/components/MemoRelationListView.tsx | 11 +- web/src/components/VisibilityIcon.tsx | 26 +++++ web/src/pages/MemoDetail.tsx | 101 +++++++++++++----- web/src/types/modules/memo.d.ts | 1 + web/src/types/modules/memoRelation.d.ts | 2 +- 11 files changed, 140 insertions(+), 71 deletions(-) create mode 100644 web/src/components/VisibilityIcon.tsx diff --git a/web/src/components/FloatingNavButton.tsx b/web/src/components/FloatingNavButton.tsx index dce2596d..056ba54f 100644 --- a/web/src/components/FloatingNavButton.tsx +++ b/web/src/components/FloatingNavButton.tsx @@ -1,4 +1,5 @@ import { Dropdown, IconButton, Menu, MenuButton } from "@mui/joy"; +import { useEffect } from "react"; import useNavigateTo from "@/hooks/useNavigateTo"; import { useTranslate } from "@/utils/i18n"; import Icon from "./Icon"; @@ -7,6 +8,10 @@ const FloatingNavButton = () => { const t = useTranslate(); const navigateTo = useNavigateTo(); + useEffect(() => { + handleScrollToTop(); + }, []); + const handleScrollToTop = () => { document.body.querySelector("#root")?.scrollTo({ top: 0, behavior: "smooth" }); }; diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 028d32cd..303fe3fa 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -1,9 +1,9 @@ -import { Divider, Select, Tooltip, Option } from "@mui/joy"; +import { Divider, Tooltip } from "@mui/joy"; import { memo, useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; +import { UNKNOWN_ID } from "@/helpers/consts"; import { getRelativeTimeString } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; import useNavigateTo from "@/hooks/useNavigateTo"; @@ -19,11 +19,13 @@ import MemoRelationListView from "./MemoRelationListView"; import MemoResourceListView from "./MemoResourceListView"; import showPreviewImageDialog from "./PreviewImageDialog"; import UserAvatar from "./UserAvatar"; +import VisibilityIcon from "./VisibilityIcon"; import "@/less/memo.less"; interface Props { memo: Memo; showVisibility?: boolean; + showCommentEntry?: boolean; lazyRendering?: boolean; } @@ -90,14 +92,6 @@ const Memo: React.FC = (props: Props) => { return
; } - const handleMemoVisibilityOptionChanged = async (value: string) => { - const visibilityValue = value as Visibility; - await memoStore.patchMemo({ - id: memo.id, - visibility: visibilityValue, - }); - }; - const handleGotoMemoDetailPage = (event: React.MouseEvent) => { if (event.altKey) { showChangeMemoCreatedTsDialog(memo.id); @@ -274,6 +268,13 @@ const Memo: React.FC = (props: Props) => { )} + {memo.parent && props.showCommentEntry && ( +
+ + This is a comment of #{memo.parent.id} + +
+ )} = (props: Props) => { <> - + + + )} diff --git a/web/src/components/MemoEditor/Editor/index.tsx b/web/src/components/MemoEditor/Editor/index.tsx index ea92bef0..4f4fb8b3 100644 --- a/web/src/components/MemoEditor/Editor/index.tsx +++ b/web/src/components/MemoEditor/Editor/index.tsx @@ -98,7 +98,6 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef< setContent: (text: string) => { if (editorRef.current) { editorRef.current.value = text; - editorRef.current.focus(); handleContentChangeCallback(editorRef.current.value); updateEditorHeight(); } diff --git a/web/src/components/MemoEditor/RelationListView.tsx b/web/src/components/MemoEditor/RelationListView.tsx index 87b9272b..87383bb7 100644 --- a/web/src/components/MemoEditor/RelationListView.tsx +++ b/web/src/components/MemoEditor/RelationListView.tsx @@ -17,18 +17,19 @@ const RelationListView = (props: Props) => { const [formatedMemoRelationList, setFormatedMemoRelationList] = useState([]); useEffect(() => { - const fetchRelatedMemoList = async () => { - const requests = relationList.map(async (relation) => { - const relatedMemo = await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId); - return { - ...relation, - relatedMemo, - }; - }); + (async () => { + const requests = relationList + .filter((relation) => relation.type === "REFERENCE") + .map(async (relation) => { + const relatedMemo = await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId); + return { + ...relation, + relatedMemo, + }; + }); const list = await Promise.all(requests); setFormatedMemoRelationList(list); - }; - fetchRelatedMemoList(); + })(); }, [relationList]); const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => { diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 3f08e77f..f4439376 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -49,7 +49,6 @@ const MemoEditor = (props: Props) => { const memoStore = useMemoStore(); const tagStore = useTagStore(); const resourceStore = useResourceStore(); - const [state, setState] = useState({ memoVisibility: "PRIVATE", resourceList: [], diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index cdffb5a4..e48ab522 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -133,7 +133,7 @@ const MemoList: React.FC = () => { return (
{sortedMemos.map((memo) => ( - + ))} {isFetching ? (
diff --git a/web/src/components/MemoRelationListView.tsx b/web/src/components/MemoRelationListView.tsx index 12438277..7b7684a2 100644 --- a/web/src/components/MemoRelationListView.tsx +++ b/web/src/components/MemoRelationListView.tsx @@ -9,16 +9,15 @@ interface Props { const MemoRelationListView = (props: Props) => { const memoCacheStore = useMemoCacheStore(); const [relatedMemoList, setRelatedMemoList] = useState([]); - const relationList = props.relationList; useEffect(() => { - const fetchRelatedMemoList = async () => { + (async () => { + // Only show reference relations. + const relationList = props.relationList.filter((relation) => relation.type === "REFERENCE"); const memoList = await Promise.all(relationList.map((relation) => memoCacheStore.getOrFetchMemoById(relation.relatedMemoId))); setRelatedMemoList(memoList); - }; - - fetchRelatedMemoList(); - }, [relationList]); + })(); + }, [props.relationList]); const handleGotoMemoDetail = (memo: Memo) => { window.open(`/m/${memo.id}`, "_blank"); diff --git a/web/src/components/VisibilityIcon.tsx b/web/src/components/VisibilityIcon.tsx new file mode 100644 index 00000000..5dc5a12b --- /dev/null +++ b/web/src/components/VisibilityIcon.tsx @@ -0,0 +1,26 @@ +import classNames from "classnames"; +import Icon from "./Icon"; + +interface Props { + visibility: Visibility; +} + +const VisibilityIcon = (props: Props) => { + const { visibility } = props; + + let VIcon = null; + if (visibility === "PRIVATE") { + VIcon = Icon.Lock; + } else if (visibility === "PROTECTED") { + VIcon = Icon.Users; + } else if (visibility === "PUBLIC") { + VIcon = Icon.Globe2; + } + if (!VIcon) { + return null; + } + + return ; +}; + +export default VisibilityIcon; diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 8cfa35f7..fe5ef0a2 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -1,17 +1,21 @@ -import { Divider, Select, Tooltip, Option, IconButton } from "@mui/joy"; +import { Select, Tooltip, Option, IconButton, Divider } from "@mui/joy"; import copy from "copy-to-clipboard"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { Link, useParams } from "react-router-dom"; import FloatingNavButton from "@/components/FloatingNavButton"; import Icon from "@/components/Icon"; +import Memo from "@/components/Memo"; import MemoContent from "@/components/MemoContent"; +import MemoEditor from "@/components/MemoEditor"; import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog"; import MemoRelationListView from "@/components/MemoRelationListView"; import MemoResourceListView from "@/components/MemoResourceListView"; import showShareMemoDialog from "@/components/ShareMemoDialog"; import UserAvatar from "@/components/UserAvatar"; -import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; +import VisibilityIcon from "@/components/VisibilityIcon"; +import { memoServiceClient } from "@/grpcweb"; +import { UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; import { getDateTimeString } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; import useNavigateTo from "@/hooks/useNavigateTo"; @@ -29,11 +33,13 @@ const MemoDetail = () => { const userV1Store = useUserV1Store(); const currentUser = useCurrentUser(); const [user, setUser] = useState(); + const [comments, setComments] = useState([]); const { systemStatus } = globalStore.state; const memoId = Number(params.memoId); const memo = memoStore.state.memos.find((memo) => memo.id === memoId); const allowEdit = memo?.creatorUsername === currentUser?.username; + // Prepare memo. useEffect(() => { if (memoId && !isNaN(memoId)) { memoStore @@ -51,6 +57,28 @@ const MemoDetail = () => { } }, [memoId]); + // Prepare memo comments. + useEffect(() => { + if (!memo) { + return; + } + + fetchMemoComments(); + }, [memo]); + + const fetchMemoComments = async () => { + if (!memo) { + return; + } + + const { memos } = await memoServiceClient.listMemoComments({ + id: memo.id, + }); + const requests = memos.map((memo) => memoStore.fetchMemoById(memo.id)); + const composedMemos = await Promise.all(requests); + setComments(composedMemos); + }; + if (!memo) { return null; } @@ -76,21 +104,20 @@ const MemoDetail = () => { return ( <> -
-
+
+

{systemStatus.customizedProfile.name}

-
+
{getDateTimeString(memo.displayTs)}
- -
+
#{memo.id} @@ -103,24 +130,23 @@ const MemoDetail = () => { {allowEdit && ( <> - - - + )}
@@ -146,6 +172,31 @@ const MemoDetail = () => {
+
+
+ {comments.map((comment) => ( + + ))} + {comments.length === 0 && ( +
+ +

No comments

+
+ )} + {/* Only show comment editor when user login */} + {currentUser && ( + <> + {comments.length === 0 && } + fetchMemoComments()} + /> + + )} +
+
diff --git a/web/src/types/modules/memo.d.ts b/web/src/types/modules/memo.d.ts index ddd5d6c7..4d6aa834 100644 --- a/web/src/types/modules/memo.d.ts +++ b/web/src/types/modules/memo.d.ts @@ -18,6 +18,7 @@ interface Memo { creatorName: string; resourceList: any[]; relationList: MemoRelation[]; + parent?: Memo; } interface MemoCreate { diff --git a/web/src/types/modules/memoRelation.d.ts b/web/src/types/modules/memoRelation.d.ts index 040c7c28..dddd3731 100644 --- a/web/src/types/modules/memoRelation.d.ts +++ b/web/src/types/modules/memoRelation.d.ts @@ -1,4 +1,4 @@ -type MemoRelationType = "REFERENCE" | "ADDITIONAL"; +type MemoRelationType = "REFERENCE" | "COMMENT"; interface MemoRelation { memoId: MemoId;