mirror of
https://github.com/usememos/memos.git
synced 2024-11-28 14:23:15 +03:00
chore: implement memo content views
This commit is contained in:
parent
fd395e5661
commit
e40621eb0f
@ -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" });
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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: [],
|
||||
|
@ -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">
|
||||
|
@ -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");
|
||||
|
26
web/src/components/VisibilityIcon.tsx
Normal file
26
web/src/components/VisibilityIcon.tsx
Normal 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;
|
@ -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 />
|
||||
|
1
web/src/types/modules/memo.d.ts
vendored
1
web/src/types/modules/memo.d.ts
vendored
@ -18,6 +18,7 @@ interface Memo {
|
||||
creatorName: string;
|
||||
resourceList: any[];
|
||||
relationList: MemoRelation[];
|
||||
parent?: Memo;
|
||||
}
|
||||
|
||||
interface MemoCreate {
|
||||
|
2
web/src/types/modules/memoRelation.d.ts
vendored
2
web/src/types/modules/memoRelation.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
type MemoRelationType = "REFERENCE" | "ADDITIONAL";
|
||||
type MemoRelationType = "REFERENCE" | "COMMENT";
|
||||
|
||||
interface MemoRelation {
|
||||
memoId: MemoId;
|
||||
|
Loading…
Reference in New Issue
Block a user