mirror of https://github.com/usememos/memos.git
feat: show inline comment preview in list view
Add a comment preview section below memo cards in list view, displaying up to 3 comment snippets with a "View all" link. Removes the old comment count icon from the memo header in favor of this richer inline display. Comment preview is hidden in memo detail view.
This commit is contained in:
parent
3e4c052f44
commit
3a5d3c8ff9
|
|
@ -1,12 +1,14 @@
|
|||
import { memo, useMemo, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { useUser } from "@/hooks/useUserQueries";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { State } from "@/types/proto/api/v1/common_pb";
|
||||
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { isSuperUser } from "@/utils/user";
|
||||
import MemoEditor from "../MemoEditor";
|
||||
import PreviewImageDialog from "../PreviewImageDialog";
|
||||
import { MemoBody, MemoHeader } from "./components";
|
||||
import { MemoBody, MemoCommentListView, MemoHeader } from "./components";
|
||||
import { MEMO_CARD_BASE_CLASSES } from "./constants";
|
||||
import { useImagePreview, useMemoActions, useMemoHandlers } from "./hooks";
|
||||
import { MemoViewContext } from "./MemoViewContext";
|
||||
|
|
@ -42,6 +44,13 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
|
|||
openPreview,
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`);
|
||||
const commentAmount = memoData.relations.filter(
|
||||
(r) => r.type === MemoRelation_Type.COMMENT && r.relatedMemo?.name === memoData.name,
|
||||
).length;
|
||||
const showCommentPreview = !isInMemoDetailPage && commentAmount > 0;
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
memo: memoData,
|
||||
|
|
@ -69,32 +78,47 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
const article = (
|
||||
<article
|
||||
className={cn(MEMO_CARD_BASE_CLASSES, showCommentPreview ? "mb-0 rounded-b-none" : "mb-2", className)}
|
||||
ref={cardRef}
|
||||
tabIndex={readonly ? -1 : 0}
|
||||
>
|
||||
<MemoHeader
|
||||
showCreator={props.showCreator}
|
||||
showVisibility={props.showVisibility}
|
||||
showPinned={props.showPinned}
|
||||
onEdit={openEditor}
|
||||
onGotoDetail={handleGotoMemoDetailPage}
|
||||
onUnpin={unpinMemo}
|
||||
/>
|
||||
|
||||
<MemoBody
|
||||
compact={props.compact}
|
||||
onContentClick={handleMemoContentClick}
|
||||
onContentDoubleClick={handleMemoContentDoubleClick}
|
||||
onToggleNsfwVisibility={toggleNsfwVisibility}
|
||||
/>
|
||||
|
||||
<PreviewImageDialog
|
||||
open={previewState.open}
|
||||
onOpenChange={setPreviewOpen}
|
||||
imgUrls={previewState.urls}
|
||||
initialIndex={previewState.index}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
|
||||
return (
|
||||
<MemoViewContext.Provider value={contextValue}>
|
||||
<article className={cn(MEMO_CARD_BASE_CLASSES, className)} ref={cardRef} tabIndex={readonly ? -1 : 0}>
|
||||
<MemoHeader
|
||||
showCreator={props.showCreator}
|
||||
showVisibility={props.showVisibility}
|
||||
showPinned={props.showPinned}
|
||||
onEdit={openEditor}
|
||||
onGotoDetail={handleGotoMemoDetailPage}
|
||||
onUnpin={unpinMemo}
|
||||
/>
|
||||
|
||||
<MemoBody
|
||||
compact={props.compact}
|
||||
onContentClick={handleMemoContentClick}
|
||||
onContentDoubleClick={handleMemoContentDoubleClick}
|
||||
onToggleNsfwVisibility={toggleNsfwVisibility}
|
||||
/>
|
||||
|
||||
<PreviewImageDialog
|
||||
open={previewState.open}
|
||||
onOpenChange={setPreviewOpen}
|
||||
imgUrls={previewState.urls}
|
||||
initialIndex={previewState.index}
|
||||
/>
|
||||
</article>
|
||||
{showCommentPreview ? (
|
||||
<div className="mb-2">
|
||||
{article}
|
||||
<MemoCommentListView />
|
||||
</div>
|
||||
) : (
|
||||
article
|
||||
)}
|
||||
</MemoViewContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMemoComments } from "@/hooks/useMemoQueries";
|
||||
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
|
||||
|
||||
const MemoCommentListView: React.FC = () => {
|
||||
const { memo } = useMemoViewContext();
|
||||
const { isInMemoDetailPage, commentAmount } = useMemoViewDerived();
|
||||
|
||||
const { data } = useMemoComments(memo.name, { enabled: !isInMemoDetailPage && commentAmount > 0 });
|
||||
const comments = data?.memos ?? [];
|
||||
|
||||
if (isInMemoDetailPage || commentAmount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayedComments = comments.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="border border-t-0 border-border rounded-b-xl px-4 pt-2 pb-3 flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-muted-foreground">Comments{commentAmount > 1 ? ` (${commentAmount})` : ""}</span>
|
||||
<Link
|
||||
to={`/${memo.name}#comments`}
|
||||
className="flex items-center gap-0.5 text-xs text-muted-foreground hover:text-foreground hover:underline underline-offset-2 transition-colors"
|
||||
>
|
||||
View all
|
||||
<ArrowUpRightIcon className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
{displayedComments.map((comment) => (
|
||||
<div key={comment.name} className="bg-muted/60 rounded-md px-2 py-1 text-xs text-muted-foreground truncate leading-relaxed">
|
||||
{comment.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoCommentListView;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||
import { BookmarkIcon, MessageCircleMoreIcon } from "lucide-react";
|
||||
import { BookmarkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
|
@ -20,8 +20,8 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
|
|||
const t = useTranslate();
|
||||
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
|
||||
|
||||
const { memo, creator, currentUser, parentPage, isArchived, readonly } = useMemoViewContext();
|
||||
const { isInMemoDetailPage, commentAmount, relativeTimeFormat } = useMemoViewDerived();
|
||||
const { memo, creator, currentUser, isArchived, readonly } = useMemoViewContext();
|
||||
const { relativeTimeFormat } = useMemoViewDerived();
|
||||
|
||||
const displayTime = isArchived ? (
|
||||
(memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language)
|
||||
|
|
@ -52,18 +52,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
|
|||
/>
|
||||
)}
|
||||
|
||||
{!isInMemoDetailPage && commentAmount > 0 && (
|
||||
<Link
|
||||
className={cn("flex flex-row justify-start items-center rounded-md px-1 hover:opacity-80 gap-0.5")}
|
||||
to={`/${memo.name}#comments`}
|
||||
viewTransition
|
||||
state={{ from: parentPage }}
|
||||
>
|
||||
<MessageCircleMoreIcon className="w-4 h-4 mx-auto text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">{commentAmount}</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { default as MemoBody } from "./MemoBody";
|
||||
export { default as MemoCommentListView } from "./MemoCommentListView";
|
||||
export { default as MemoHeader } from "./MemoHeader";
|
||||
|
|
|
|||
Loading…
Reference in New Issue