mirror of
				https://github.com/usememos/memos.git
				synced 2025-10-31 16:59:30 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			181 lines
		
	
	
	
		
			7.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			181 lines
		
	
	
	
		
			7.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { Button } from "@usememos/mui";
 | |
| import { ArrowUpLeftFromCircleIcon, MessageCircleIcon } from "lucide-react";
 | |
| import { ClientError } from "nice-grpc-web";
 | |
| import { useEffect, useState } from "react";
 | |
| import { toast } from "react-hot-toast";
 | |
| import { Link, useLocation, useParams } from "react-router-dom";
 | |
| import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
 | |
| import MemoEditor from "@/components/MemoEditor";
 | |
| import MemoView from "@/components/MemoView";
 | |
| import MobileHeader from "@/components/MobileHeader";
 | |
| import useCurrentUser from "@/hooks/useCurrentUser";
 | |
| import useNavigateTo from "@/hooks/useNavigateTo";
 | |
| import useResponsiveWidth from "@/hooks/useResponsiveWidth";
 | |
| import { memoNamePrefix } from "@/store/common";
 | |
| import { useMemoStore } from "@/store/v1";
 | |
| import { workspaceStore } from "@/store/v2";
 | |
| import { Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service";
 | |
| import { cn } from "@/utils";
 | |
| import { useTranslate } from "@/utils/i18n";
 | |
| 
 | |
| const MemoDetail = () => {
 | |
|   const t = useTranslate();
 | |
|   const { md } = useResponsiveWidth();
 | |
|   const params = useParams();
 | |
|   const navigateTo = useNavigateTo();
 | |
|   const { state: locationState } = useLocation();
 | |
|   const currentUser = useCurrentUser();
 | |
|   const memoStore = useMemoStore();
 | |
|   const uid = params.uid;
 | |
|   const memoName = `${memoNamePrefix}${uid}`;
 | |
|   const memo = memoStore.getMemoByName(memoName);
 | |
|   const workspaceMemoRelatedSetting = workspaceStore.state.memoRelatedSetting;
 | |
|   const [parentMemo, setParentMemo] = useState<Memo | undefined>(undefined);
 | |
|   const [showCommentEditor, setShowCommentEditor] = useState(false);
 | |
|   const commentRelations =
 | |
|     memo?.relations.filter((relation) => relation.relatedMemo?.name === memo.name && relation.type === MemoRelation_Type.COMMENT) || [];
 | |
|   const comments = commentRelations.map((relation) => memoStore.getMemoByName(relation.memo!.name)).filter((memo) => memo) as any as Memo[];
 | |
|   const showCreateCommentButton = workspaceMemoRelatedSetting.enableComment && currentUser && !showCommentEditor;
 | |
| 
 | |
|   // Prepare memo.
 | |
|   useEffect(() => {
 | |
|     if (memoName) {
 | |
|       memoStore.getOrFetchMemoByName(memoName).catch((error: ClientError) => {
 | |
|         toast.error(error.details);
 | |
|         navigateTo("/403");
 | |
|       });
 | |
|     } else {
 | |
|       navigateTo("/404");
 | |
|     }
 | |
|   }, [memoName]);
 | |
| 
 | |
|   // Prepare memo comments.
 | |
|   useEffect(() => {
 | |
|     if (!memo) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     (async () => {
 | |
|       if (memo.parent) {
 | |
|         memoStore.getOrFetchMemoByName(memo.parent).then((memo: Memo) => {
 | |
|           setParentMemo(memo);
 | |
|         });
 | |
|       } else {
 | |
|         setParentMemo(undefined);
 | |
|       }
 | |
|       await Promise.all(commentRelations.map((relation) => memoStore.getOrFetchMemoByName(relation.memo!.name)));
 | |
|     })();
 | |
|   }, [memo]);
 | |
| 
 | |
|   if (!memo) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const handleShowCommentEditor = () => {
 | |
|     setShowCommentEditor(true);
 | |
|   };
 | |
| 
 | |
|   const handleCommentCreated = async (memoCommentName: string) => {
 | |
|     await memoStore.getOrFetchMemoByName(memoCommentName);
 | |
|     await memoStore.getOrFetchMemoByName(memo.name, { skipCache: true });
 | |
|     setShowCommentEditor(false);
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
 | |
|       {!md && (
 | |
|         <MobileHeader>
 | |
|           <MemoDetailSidebarDrawer memo={memo} parentPage={locationState?.from} />
 | |
|         </MobileHeader>
 | |
|       )}
 | |
|       <div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
 | |
|         <div className={cn(md ? "w-[calc(100%-15rem)]" : "w-full")}>
 | |
|           {parentMemo && (
 | |
|             <div className="w-auto inline-block mb-2">
 | |
|               <Link
 | |
|                 className="px-3 py-1 border rounded-lg max-w-xs w-auto text-sm flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-400 dark:border-gray-500 hover:shadow hover:opacity-80"
 | |
|                 to={`/${parentMemo.name}`}
 | |
|                 state={locationState}
 | |
|                 viewTransition
 | |
|               >
 | |
|                 <ArrowUpLeftFromCircleIcon className="w-4 h-auto shrink-0 opacity-60 mr-2" />
 | |
|                 <span className="truncate">{parentMemo.content}</span>
 | |
|               </Link>
 | |
|             </div>
 | |
|           )}
 | |
|           <MemoView
 | |
|             key={`${memo.name}-${memo.displayTime}`}
 | |
|             className="shadow hover:shadow-md transition-all"
 | |
|             memo={memo}
 | |
|             compact={false}
 | |
|             parentPage={locationState?.from}
 | |
|             showCreator
 | |
|             showVisibility
 | |
|             showPinned
 | |
|             showNsfwContent
 | |
|           />
 | |
|           <div className="pt-8 pb-16 w-full">
 | |
|             <h2 id="comments" className="sr-only">
 | |
|               {t("memo.comment.self")}
 | |
|             </h2>
 | |
|             <div className="relative mx-auto flex-grow w-full min-h-full flex flex-col justify-start items-start gap-y-1">
 | |
|               {comments.length === 0 ? (
 | |
|                 showCreateCommentButton && (
 | |
|                   <div className="w-full flex flex-row justify-center items-center py-6">
 | |
|                     <Button variant="plain" color="primary" onClick={handleShowCommentEditor}>
 | |
|                       <span className="text-gray-500">{t("memo.comment.write-a-comment")}</span>
 | |
|                       <MessageCircleIcon className="ml-2 w-5 h-auto text-gray-500" />
 | |
|                     </Button>
 | |
|                   </div>
 | |
|                 )
 | |
|               ) : (
 | |
|                 <>
 | |
|                   <div className="w-full flex flex-row justify-between items-center h-8 pl-3 mb-2">
 | |
|                     <div className="flex flex-row justify-start items-center">
 | |
|                       <MessageCircleIcon className="w-5 h-auto text-gray-400 mr-1" />
 | |
|                       <span className="text-gray-400 text-sm">{t("memo.comment.self")}</span>
 | |
|                       <span className="text-gray-400 text-sm ml-1">({comments.length})</span>
 | |
|                     </div>
 | |
|                     {showCreateCommentButton && (
 | |
|                       <Button variant="plain" color="primary" className="text-gray-500" onClick={handleShowCommentEditor}>
 | |
|                         {t("memo.comment.write-a-comment")}
 | |
|                       </Button>
 | |
|                     )}
 | |
|                   </div>
 | |
|                   {comments.map((comment) => (
 | |
|                     <MemoView
 | |
|                       key={`${comment.name}-${comment.displayTime}`}
 | |
|                       memo={comment}
 | |
|                       parentPage={locationState?.from}
 | |
|                       showCreator
 | |
|                       compact
 | |
|                     />
 | |
|                   ))}
 | |
|                 </>
 | |
|               )}
 | |
|             </div>
 | |
|             {showCommentEditor && (
 | |
|               <div className="w-full">
 | |
|                 <MemoEditor
 | |
|                   cacheKey={`${memo.name}-${memo.updateTime}-comment`}
 | |
|                   placeholder={t("editor.add-your-comment-here")}
 | |
|                   parentMemoName={memo.name}
 | |
|                   autoFocus
 | |
|                   onConfirm={handleCommentCreated}
 | |
|                   onCancel={() => setShowCommentEditor(false)}
 | |
|                 />
 | |
|               </div>
 | |
|             )}
 | |
|           </div>
 | |
|         </div>
 | |
|         {md && (
 | |
|           <div className="sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full">
 | |
|             <MemoDetailSidebar className="py-6" memo={memo} parentPage={locationState?.from} />
 | |
|           </div>
 | |
|         )}
 | |
|       </div>
 | |
|     </section>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default MemoDetail;
 |