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 } from "@/helpers/consts"; import { getRelativeTimeString, getTimeStampByDate } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; import useNavigateTo from "@/hooks/useNavigateTo"; import { useFilterStore } from "@/store/module"; import { useUserV1Store, extractUsernameFromName, useMemoV1Store } from "@/store/v1"; import { RowStatus } from "@/types/proto/api/v2/common"; import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service"; import { Memo, Visibility } from "@/types/proto/api/v2/memo_service"; import { Resource } from "@/types/proto/api/v2/resource_service"; import { useTranslate } from "@/utils/i18n"; import { convertVisibilityToString } from "@/utils/memo"; import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; import { showCommonDialog } from "./Dialog/CommonDialog"; import Icon from "./Icon"; import MemoContent from "./MemoContent"; import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog"; import MemoRelationListView from "./MemoRelationListView"; import MemoResourceListView from "./MemoResourceListView"; import showPreviewImageDialog from "./PreviewImageDialog"; import showShareMemoDialog from "./ShareMemoDialog"; import UserAvatar from "./UserAvatar"; import VisibilityIcon from "./VisibilityIcon"; import "@/less/memo.less"; interface Props { memo: Memo; showCreator?: boolean; showParent?: boolean; showVisibility?: boolean; showPinnedStyle?: boolean; lazyRendering?: boolean; } const MemoView: React.FC = (props: Props) => { const { memo, lazyRendering } = props; const t = useTranslate(); const navigateTo = useNavigateTo(); const { i18n } = useTranslation(); const filterStore = useFilterStore(); const memoStore = useMemoV1Store(); const userV1Store = useUserV1Store(); const user = useCurrentUser(); const [shouldRender, setShouldRender] = useState(lazyRendering ? false : true); const [displayTime, setDisplayTime] = useState(getRelativeTimeString(getTimeStampByDate(memo.displayTime))); const [creator, setCreator] = useState(userV1Store.getUserByUsername(extractUsernameFromName(memo.creator))); const [parentMemo, setParentMemo] = useState(undefined); const [resources, setResources] = useState([]); const [memoRelations, setMemoRelations] = useState([]); const memoContainerRef = useRef(null); const referenceRelations = memoRelations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); const readonly = memo.creator !== user?.name; // Prepare memo creator. useEffect(() => { if (creator) return; (async () => { const user = await userV1Store.getOrFetchUserByUsername(extractUsernameFromName(memo.creator)); setCreator(user); })(); }, [memo.creator]); // Update display time string. useEffect(() => { let intervalFlag: any = -1; if (Date.now() - getTimeStampByDate(memo.displayTime) < 1000 * 60 * 60 * 24) { intervalFlag = setInterval(() => { setDisplayTime(getRelativeTimeString(getTimeStampByDate(memo.displayTime))); }, 1000 * 1); } return () => { clearInterval(intervalFlag); }; }, [i18n.language]); // Lazy rendering. useEffect(() => { if (shouldRender) return; if (!memoContainerRef.current) return; const observer = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) return; observer.disconnect(); setShouldRender(true); }); observer.observe(memoContainerRef.current); return () => observer.disconnect(); }, [lazyRendering, filterStore.state]); useEffect(() => { if (!shouldRender) { return; } memoStore.fetchMemoResources(memo.id).then((resources: Resource[]) => { setResources(resources); }); memoStore.fetchMemoRelations(memo.id).then((relations: MemoRelation[]) => { setMemoRelations(relations); const parentMemoId = relations.find( (relation) => relation.memoId === memo.id && relation.type === MemoRelation_Type.COMMENT )?.relatedMemoId; if (parentMemoId) { memoStore.getOrFetchMemoById(parentMemoId).then((memo: Memo) => { setParentMemo(memo); }); } }); }, [shouldRender]); if (!shouldRender) { // Render a placeholder to occupy the space. return
; } const handleGotoMemoDetailPage = (event: React.MouseEvent) => { if (event.altKey) { showChangeMemoCreatedTsDialog(memo.id); } else { navigateTo(`/m/${memo.id}`); } }; const handleTogglePinMemoBtnClick = async () => { try { if (memo.pinned) { await memoStore.updateMemo( { id: memo.id, pinned: false, }, ["pinned"] ); } else { await memoStore.updateMemo( { id: memo.id, pinned: true, }, ["pinned"] ); } } catch (error) { // do nth } }; const handleEditMemoClick = () => { showMemoEditorDialog({ memoId: memo.id, }); }; const handleMarkMemoClick = () => { showMemoEditorDialog({ relationList: [ { memoId: UNKNOWN_ID, relatedMemoId: memo.id, type: MemoRelation_Type.REFERENCE, }, ], }); }; const handleArchiveMemoClick = async () => { try { await memoStore.updateMemo( { id: memo.id, rowStatus: RowStatus.ARCHIVED, }, ["row_status"] ); } catch (error: any) { console.error(error); toast.error(error.response.data.message); } }; const handleDeleteMemoClick = async () => { showCommonDialog({ title: t("memo.delete-memo"), content: t("memo.delete-confirm"), style: "danger", dialogName: "delete-memo-dialog", onConfirm: async () => { await memoStore.deleteMemo(memo.id); }, }); }; const handleMemoContentClick = async (e: React.MouseEvent) => { const targetEl = e.target as HTMLElement; if (targetEl.classList.contains("tag-container")) { const tagName = targetEl.innerText.slice(1); const currTagQuery = filterStore.getState().tag; if (currTagQuery === tagName) { filterStore.setTagFilter(undefined); } else { filterStore.setTagFilter(tagName); } } else if (targetEl.tagName === "IMG") { const imgUrl = targetEl.getAttribute("src"); if (imgUrl) { showPreviewImageDialog([imgUrl], 0); } } }; return (
{props.showCreator && creator && ( <> {creator.nickname || extractUsernameFromName(creator.name)} )} {displayTime} {props.showPinnedStyle && memo.pinned && ( <> )}
#{memo.id} {props.showVisibility && memo.visibility !== Visibility.PRIVATE && ( <> )}
{!readonly && ( <>
{!parentMemo && ( {memo.pinned ? : } {memo.pinned ? t("common.unpin") : t("common.pin")} )} {t("common.edit")} {!parentMemo && ( {t("common.mark")} )} showShareMemoDialog(memo)}> {t("common.share")} {t("common.archive")} {t("common.delete")}
)}
{props.showParent && parentMemo && (
#{parentMemo.id} {parentMemo.content}
)}
); }; export default memo(MemoView);