chore: implement memo content views

pull/2323/head
Steven 2 years ago
parent fd395e5661
commit e40621eb0f

@ -1,4 +1,5 @@
import { Dropdown, IconButton, Menu, MenuButton } from "@mui/joy"; import { Dropdown, IconButton, Menu, MenuButton } from "@mui/joy";
import { useEffect } from "react";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import Icon from "./Icon"; import Icon from "./Icon";
@ -7,6 +8,10 @@ const FloatingNavButton = () => {
const t = useTranslate(); const t = useTranslate();
const navigateTo = useNavigateTo(); const navigateTo = useNavigateTo();
useEffect(() => {
handleScrollToTop();
}, []);
const handleScrollToTop = () => { const handleScrollToTop = () => {
document.body.querySelector("#root")?.scrollTo({ top: 0, behavior: "smooth" }); document.body.querySelector("#root")?.scrollTo({ top: 0, behavior: "smooth" });
}; };

@ -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 { memo, useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; 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 { getRelativeTimeString } from "@/helpers/datetime";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
@ -19,11 +19,13 @@ import MemoRelationListView from "./MemoRelationListView";
import MemoResourceListView from "./MemoResourceListView"; import MemoResourceListView from "./MemoResourceListView";
import showPreviewImageDialog from "./PreviewImageDialog"; import showPreviewImageDialog from "./PreviewImageDialog";
import UserAvatar from "./UserAvatar"; import UserAvatar from "./UserAvatar";
import VisibilityIcon from "./VisibilityIcon";
import "@/less/memo.less"; import "@/less/memo.less";
interface Props { interface Props {
memo: Memo; memo: Memo;
showVisibility?: boolean; showVisibility?: boolean;
showCommentEntry?: boolean;
lazyRendering?: 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>; 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>) => { const handleGotoMemoDetailPage = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.altKey) { if (event.altKey) {
showChangeMemoCreatedTsDialog(memo.id); showChangeMemoCreatedTsDialog(memo.id);
@ -274,6 +268,13 @@ const Memo: React.FC<Props> = (props: Props) => {
)} )}
</div> </div>
</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 <MemoContent
content={memo.content} content={memo.content}
onMemoContentClick={handleMemoContentClick} 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" /> <Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Tooltip title={"The visibility of memo"} placement="top"> <Tooltip title={"The visibility of memo"} placement="top">
<Select <span>
className="w-auto text-sm" <VisibilityIcon visibility={memo.visibility} />
variant="plain" </span>
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> </Tooltip>
</> </>
)} )}

@ -98,7 +98,6 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef<
setContent: (text: string) => { setContent: (text: string) => {
if (editorRef.current) { if (editorRef.current) {
editorRef.current.value = text; editorRef.current.value = text;
editorRef.current.focus();
handleContentChangeCallback(editorRef.current.value); handleContentChangeCallback(editorRef.current.value);
updateEditorHeight(); updateEditorHeight();
} }

@ -17,18 +17,19 @@ const RelationListView = (props: Props) => {
const [formatedMemoRelationList, setFormatedMemoRelationList] = useState<FormatedMemoRelation[]>([]); const [formatedMemoRelationList, setFormatedMemoRelationList] = useState<FormatedMemoRelation[]>([]);
useEffect(() => { useEffect(() => {
const fetchRelatedMemoList = async () => { (async () => {
const requests = relationList.map(async (relation) => { const requests = relationList
const relatedMemo = await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId); .filter((relation) => relation.type === "REFERENCE")
return { .map(async (relation) => {
...relation, const relatedMemo = await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId);
relatedMemo, return {
}; ...relation,
}); relatedMemo,
};
});
const list = await Promise.all(requests); const list = await Promise.all(requests);
setFormatedMemoRelationList(list); setFormatedMemoRelationList(list);
}; })();
fetchRelatedMemoList();
}, [relationList]); }, [relationList]);
const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => { const handleDeleteRelation = async (memoRelation: FormatedMemoRelation) => {

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

@ -133,7 +133,7 @@ const MemoList: React.FC = () => {
return ( return (
<div className="memo-list-container"> <div className="memo-list-container">
{sortedMemos.map((memo) => ( {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 ? ( {isFetching ? (
<div className="status-text-container fetching-tip"> <div className="status-text-container fetching-tip">

@ -9,16 +9,15 @@ interface Props {
const MemoRelationListView = (props: Props) => { const MemoRelationListView = (props: Props) => {
const memoCacheStore = useMemoCacheStore(); const memoCacheStore = useMemoCacheStore();
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]); const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
const relationList = props.relationList;
useEffect(() => { 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))); const memoList = await Promise.all(relationList.map((relation) => memoCacheStore.getOrFetchMemoById(relation.relatedMemoId)));
setRelatedMemoList(memoList); setRelatedMemoList(memoList);
}; })();
}, [props.relationList]);
fetchRelatedMemoList();
}, [relationList]);
const handleGotoMemoDetail = (memo: Memo) => { const handleGotoMemoDetail = (memo: Memo) => {
window.open(`/m/${memo.id}`, "_blank"); window.open(`/m/${memo.id}`, "_blank");

@ -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;

@ -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 copy from "copy-to-clipboard";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import FloatingNavButton from "@/components/FloatingNavButton"; import FloatingNavButton from "@/components/FloatingNavButton";
import Icon from "@/components/Icon"; import Icon from "@/components/Icon";
import Memo from "@/components/Memo";
import MemoContent from "@/components/MemoContent"; import MemoContent from "@/components/MemoContent";
import MemoEditor from "@/components/MemoEditor";
import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog"; import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog";
import MemoRelationListView from "@/components/MemoRelationListView"; import MemoRelationListView from "@/components/MemoRelationListView";
import MemoResourceListView from "@/components/MemoResourceListView"; import MemoResourceListView from "@/components/MemoResourceListView";
import showShareMemoDialog from "@/components/ShareMemoDialog"; import showShareMemoDialog from "@/components/ShareMemoDialog";
import UserAvatar from "@/components/UserAvatar"; 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 { getDateTimeString } from "@/helpers/datetime";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
@ -29,11 +33,13 @@ const MemoDetail = () => {
const userV1Store = useUserV1Store(); const userV1Store = useUserV1Store();
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
const [comments, setComments] = useState<Memo[]>([]);
const { systemStatus } = globalStore.state; const { systemStatus } = globalStore.state;
const memoId = Number(params.memoId); const memoId = Number(params.memoId);
const memo = memoStore.state.memos.find((memo) => memo.id === memoId); const memo = memoStore.state.memos.find((memo) => memo.id === memoId);
const allowEdit = memo?.creatorUsername === currentUser?.username; const allowEdit = memo?.creatorUsername === currentUser?.username;
// Prepare memo.
useEffect(() => { useEffect(() => {
if (memoId && !isNaN(memoId)) { if (memoId && !isNaN(memoId)) {
memoStore memoStore
@ -51,6 +57,28 @@ const MemoDetail = () => {
} }
}, [memoId]); }, [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) { if (!memo) {
return null; return null;
} }
@ -76,21 +104,20 @@ const MemoDetail = () => {
return ( return (
<> <>
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-white dark:bg-zinc-800"> <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 min-h-full mx-auto flex flex-col justify-start items-center pb-6"> <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"> <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} /> <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> <p className="text-3xl text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
</div> </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"> <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> <span className="text-gray-400 select-none">{getDateTimeString(memo.displayTs)}</span>
</div> </div>
<MemoContent content={memo.content} /> <MemoContent content={memo.content} />
<MemoResourceListView resourceList={memo.resourceList} /> <MemoResourceListView resourceList={memo.resourceList} />
<MemoRelationListView relationList={memo.relationList} /> <MemoRelationListView relationList={memo.relationList} />
<Divider className="!my-6" /> <div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
<div className="w-full 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"> <div className="flex flex-row justify-start items-center">
<Tooltip title={"The identifier of memo"} placement="top"> <Tooltip title={"The identifier of memo"} placement="top">
<span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span> <span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span>
@ -103,24 +130,23 @@ const MemoDetail = () => {
{allowEdit && ( {allowEdit && (
<> <>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" /> <Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Tooltip title={"The visibility of memo"} placement="top"> <Select
<Select className="w-auto text-sm"
className="w-auto text-sm" variant="plain"
variant="plain" value={memo.visibility}
value={memo.visibility} startDecorator={<VisibilityIcon visibility={memo.visibility} />}
onChange={(_, visibility) => { onChange={(_, visibility) => {
if (visibility) { if (visibility) {
handleMemoVisibilityOptionChanged(visibility); handleMemoVisibilityOptionChanged(visibility);
} }
}} }}
> >
{VISIBILITY_SELECTOR_ITEMS.map((item) => ( {VISIBILITY_SELECTOR_ITEMS.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap"> <Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text} {item.text}
</Option> </Option>
))} ))}
</Select> </Select>
</Tooltip>
</> </>
)} )}
</div> </div>
@ -146,6 +172,31 @@ const MemoDetail = () => {
</div> </div>
</div> </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> </section>
<FloatingNavButton /> <FloatingNavButton />

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

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

Loading…
Cancel
Save