Commit 3a5d3c8f authored by Steven's avatar Steven

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.
parent 3e4c052f
import { memo, useMemo, useRef, useState } from "react"; import { memo, useMemo, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { useUser } from "@/hooks/useUserQueries"; import { useUser } from "@/hooks/useUserQueries";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { State } from "@/types/proto/api/v1/common_pb"; 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 { isSuperUser } from "@/utils/user";
import MemoEditor from "../MemoEditor"; import MemoEditor from "../MemoEditor";
import PreviewImageDialog from "../PreviewImageDialog"; import PreviewImageDialog from "../PreviewImageDialog";
import { MemoBody, MemoHeader } from "./components"; import { MemoBody, MemoCommentListView, MemoHeader } from "./components";
import { MEMO_CARD_BASE_CLASSES } from "./constants"; import { MEMO_CARD_BASE_CLASSES } from "./constants";
import { useImagePreview, useMemoActions, useMemoHandlers } from "./hooks"; import { useImagePreview, useMemoActions, useMemoHandlers } from "./hooks";
import { MemoViewContext } from "./MemoViewContext"; import { MemoViewContext } from "./MemoViewContext";
...@@ -42,6 +44,13 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => { ...@@ -42,6 +44,13 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
openPreview, 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( const contextValue = useMemo(
() => ({ () => ({
memo: memoData, memo: memoData,
...@@ -69,9 +78,12 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => { ...@@ -69,9 +78,12 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
); );
} }
return ( const article = (
<MemoViewContext.Provider value={contextValue}> <article
<article className={cn(MEMO_CARD_BASE_CLASSES, className)} ref={cardRef} tabIndex={readonly ? -1 : 0}> className={cn(MEMO_CARD_BASE_CLASSES, showCommentPreview ? "mb-0 rounded-b-none" : "mb-2", className)}
ref={cardRef}
tabIndex={readonly ? -1 : 0}
>
<MemoHeader <MemoHeader
showCreator={props.showCreator} showCreator={props.showCreator}
showVisibility={props.showVisibility} showVisibility={props.showVisibility}
...@@ -95,6 +107,18 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => { ...@@ -95,6 +107,18 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
initialIndex={previewState.index} initialIndex={previewState.index}
/> />
</article> </article>
);
return (
<MemoViewContext.Provider value={contextValue}>
{showCommentPreview ? (
<div className="mb-2">
{article}
<MemoCommentListView />
</div>
) : (
article
)}
</MemoViewContext.Provider> </MemoViewContext.Provider>
); );
}; };
......
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;
import { timestampDate } from "@bufbuild/protobuf/wkt"; import { timestampDate } from "@bufbuild/protobuf/wkt";
import { BookmarkIcon, MessageCircleMoreIcon } from "lucide-react"; import { BookmarkIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
...@@ -20,8 +20,8 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh ...@@ -20,8 +20,8 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
const t = useTranslate(); const t = useTranslate();
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false); const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const { memo, creator, currentUser, parentPage, isArchived, readonly } = useMemoViewContext(); const { memo, creator, currentUser, isArchived, readonly } = useMemoViewContext();
const { isInMemoDetailPage, commentAmount, relativeTimeFormat } = useMemoViewDerived(); const { relativeTimeFormat } = useMemoViewDerived();
const displayTime = isArchived ? ( const displayTime = isArchived ? (
(memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language) (memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language)
...@@ -52,18 +52,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh ...@@ -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 && ( {showVisibility && memo.visibility !== Visibility.PRIVATE && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
......
export { default as MemoBody } from "./MemoBody"; export { default as MemoBody } from "./MemoBody";
export { default as MemoCommentListView } from "./MemoCommentListView";
export { default as MemoHeader } from "./MemoHeader"; export { default as MemoHeader } from "./MemoHeader";
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment