chore: implement memo content views

This commit is contained in:
Steven 2023-10-01 22:14:25 +08:00
parent fd395e5661
commit e40621eb0f
11 changed files with 140 additions and 71 deletions

View File

@ -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" });
};

View File

@ -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: Props) => {
return <div className={`memo-wrapper min-h-[128px] ${"memos-" + memo.id}`} ref={memoContainerRef}></div>;
}
const handleMemoVisibilityOptionChanged = async (value: string) => {
const visibilityValue = value as Visibility;
await memoStore.patchMemo({
id: memo.id,
visibility: visibilityValue,
});
};
const handleGotoMemoDetailPage = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.altKey) {
showChangeMemoCreatedTsDialog(memo.id);
@ -274,6 +268,13 @@ const Memo: React.FC<Props> = (props: Props) => {
)}
</div>
</div>
{memo.parent && props.showCommentEntry && (
<div>
<Link to={`/m/${memo.parent.id}`}>
<span className="text-xs text-gray-400 opacity-80 dark:text-gray-500">This is a comment of #{memo.parent.id}</span>
</Link>
</div>
)}
<MemoContent
content={memo.content}
onMemoContentClick={handleMemoContentClick}
@ -299,22 +300,9 @@ const Memo: React.FC<Props> = (props: Props) => {
<>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Tooltip title={"The visibility of memo"} placement="top">
<Select
className="w-auto text-sm"
variant="plain"
value={memo.visibility}
onChange={(_, visibility) => {
if (visibility) {
handleMemoVisibilityOptionChanged(visibility);
}
}}
>
{VISIBILITY_SELECTOR_ITEMS.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
<span>
<VisibilityIcon visibility={memo.visibility} />
</span>
</Tooltip>
</>
)}

View File

@ -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();
}

View File

@ -17,18 +17,19 @@ const RelationListView = (props: Props) => {
const [formatedMemoRelationList, setFormatedMemoRelationList] = useState<FormatedMemoRelation[]>([]);
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) => {

View File

@ -49,7 +49,6 @@ const MemoEditor = (props: Props) => {
const memoStore = useMemoStore();
const tagStore = useTagStore();
const resourceStore = useResourceStore();
const [state, setState] = useState<State>({
memoVisibility: "PRIVATE",
resourceList: [],

View File

@ -133,7 +133,7 @@ const MemoList: React.FC = () => {
return (
<div className="memo-list-container">
{sortedMemos.map((memo) => (
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility />
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility showCommentEntry />
))}
{isFetching ? (
<div className="status-text-container fetching-tip">

View File

@ -9,16 +9,15 @@ interface Props {
const MemoRelationListView = (props: Props) => {
const memoCacheStore = useMemoCacheStore();
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
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");

View File

@ -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 <VIcon className={classNames("w-4 h-auto text-gray-400")} />;
};
export default VisibilityIcon;

View File

@ -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<User>();
const [comments, setComments] = useState<Memo[]>([]);
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 (
<>
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-white dark:bg-zinc-800">
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-6">
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<div className="relative w-full h-auto mx-auto flex flex-col justify-start items-center bg-white dark:bg-zinc-900">
<div className="w-full flex flex-col justify-start items-center py-8">
<UserAvatar className="!w-20 h-auto mb-2 drop-shadow" avatarUrl={systemStatus.customizedProfile.logoUrl} />
<p className="text-3xl text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
</div>
<div className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<div className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 pb-6">
<div className="w-full mb-4 flex flex-row justify-start items-center mr-1">
<span className="text-gray-400 select-none">{getDateTimeString(memo.displayTs)}</span>
</div>
<MemoContent content={memo.content} />
<MemoResourceListView resourceList={memo.resourceList} />
<MemoRelationListView relationList={memo.relationList} />
<Divider className="!my-6" />
<div className="w-full flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
<div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
<div className="flex flex-row justify-start items-center">
<Tooltip title={"The identifier of memo"} placement="top">
<span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span>
@ -103,24 +130,23 @@ const MemoDetail = () => {
{allowEdit && (
<>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Tooltip title={"The visibility of memo"} placement="top">
<Select
className="w-auto text-sm"
variant="plain"
value={memo.visibility}
onChange={(_, visibility) => {
if (visibility) {
handleMemoVisibilityOptionChanged(visibility);
}
}}
>
{VISIBILITY_SELECTOR_ITEMS.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</Tooltip>
<Select
className="w-auto text-sm"
variant="plain"
value={memo.visibility}
startDecorator={<VisibilityIcon visibility={memo.visibility} />}
onChange={(_, visibility) => {
if (visibility) {
handleMemoVisibilityOptionChanged(visibility);
}
}}
>
{VISIBILITY_SELECTOR_ITEMS.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</>
)}
</div>
@ -146,6 +172,31 @@ const MemoDetail = () => {
</div>
</div>
</div>
<div className="py-6 w-full border-t dark:border-t-zinc-700">
<div className="relative mx-auto flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 gap-y-1">
{comments.map((comment) => (
<Memo key={comment.id} memo={comment} />
))}
{comments.length === 0 && (
<div className="w-full flex flex-col justify-center items-center py-6">
<Icon.MessageCircle strokeWidth={1} className="w-8 h-auto text-gray-400" />
<p className="text-gray-400 italic text-sm">No comments</p>
</div>
)}
{/* Only show comment editor when user login */}
{currentUser && (
<>
{comments.length === 0 && <Divider className="!my-4" />}
<MemoEditor
key={memo.id}
className="border-none"
relationList={[{ memoId: UNKNOWN_ID, relatedMemoId: memo.id, type: "COMMENT" }]}
onConfirm={() => fetchMemoComments()}
/>
</>
)}
</div>
</div>
</section>
<FloatingNavButton />

View File

@ -18,6 +18,7 @@ interface Memo {
creatorName: string;
resourceList: any[];
relationList: MemoRelation[];
parent?: Memo;
}
interface MemoCreate {

View File

@ -1,4 +1,4 @@
type MemoRelationType = "REFERENCE" | "ADDITIONAL";
type MemoRelationType = "REFERENCE" | "COMMENT";
interface MemoRelation {
memoId: MemoId;