feat(memo-preview): support comment metadata in previews (#5768)

Co-authored-by: memoclaw <265580040+memoclaw@users.noreply.github.com>
This commit is contained in:
memoclaw 2026-03-23 21:51:30 +08:00 committed by GitHub
parent 22519b57a0
commit e176b28c80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 16 deletions

View File

@ -4,7 +4,6 @@ import { MemoPreview } from "@/components/MemoPreview";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { VisuallyHidden } from "@/components/ui/visually-hidden";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
@ -75,12 +74,9 @@ export const LinkMemoDialog = ({
<div className="w-full flex flex-col gap-1">
<div className="flex items-center gap-1.5 text-sm text-muted-foreground select-none">
{alreadyLinked && <LinkIcon className="w-3 h-3 shrink-0" />}
<span className="text-xs font-mono px-1 py-0.5 rounded border border-border bg-muted/40 shrink-0">
{extractMemoIdFromName(memo.name).slice(0, 6)}
</span>
<span>{memo.displayTime && timestampDate(memo.displayTime).toLocaleString()}</span>
</div>
<MemoPreview content={memo.content} attachments={memo.attachments} />
<MemoPreview name={memo.name} content={memo.content} attachments={memo.attachments} showMemoId />
</div>
</div>
);

View File

@ -1,8 +1,10 @@
import { create } from "@bufbuild/protobuf";
import { FileIcon } from "lucide-react";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb";
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
import MemoContent from "../MemoContent";
import { MemoViewContext, type MemoViewContextValue } from "../MemoView/MemoViewContext";
@ -10,8 +12,13 @@ import { MemoViewContext, type MemoViewContextValue } from "../MemoView/MemoView
interface MemoPreviewProps {
content: string;
attachments: Attachment[];
name?: string;
compact?: boolean;
className?: string;
creator?: User;
showCreator?: boolean;
showMemoId?: boolean;
truncate?: boolean;
}
const STUB_CONTEXT: MemoViewContextValue = {
@ -57,18 +64,76 @@ const AttachmentThumbnails = ({ attachments }: { attachments: Attachment[] }) =>
);
};
const MemoPreview = ({ content, attachments, compact = true, className }: MemoPreviewProps) => {
const PreviewMeta = ({
creator,
showCreator,
memoName,
showMemoId,
}: {
creator?: User;
showCreator?: boolean;
memoName?: string;
showMemoId?: boolean;
}) => {
const creatorName = creator?.displayName || creator?.username;
const memoId = showMemoId && memoName ? extractMemoIdFromName(memoName).slice(0, 6) : undefined;
if (!creatorName && !memoId) {
return null;
}
return (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground leading-none shrink-0">
{showMemoId && memoId && (
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 shrink-0">{memoId}</span>
)}
{showCreator && creatorName && <span className="font-medium text-foreground/80 truncate">{creatorName}</span>}
</div>
);
};
const MemoPreview = ({
content,
attachments,
name,
compact = true,
className,
creator,
showCreator = false,
showMemoId = false,
truncate = false,
}: MemoPreviewProps) => {
const hasContent = content.trim().length > 0;
const hasAttachments = attachments.length > 0;
const showMeta = showCreator || showMemoId;
if (!hasContent && !hasAttachments) {
return null;
}
const meta = <PreviewMeta creator={creator} showCreator={showCreator} memoName={name} showMemoId={showMemoId} />;
const contentNode = truncate ? (
hasContent ? (
<div className="text-sm text-muted-foreground truncate min-w-0">{content}</div>
) : hasAttachments ? null : (
<div className="text-sm text-muted-foreground truncate min-w-0">No content</div>
)
) : (
hasContent && <MemoContent content={content} compact={compact} />
);
return (
<MemoViewContext.Provider value={STUB_CONTEXT}>
<div className={cn("flex flex-col gap-1 pointer-events-none", className)}>
{hasContent && <MemoContent content={content} compact={compact} />}
<div
className={cn(
"pointer-events-none",
truncate ? "flex items-center gap-1.5 min-w-0 leading-tight" : "flex flex-col gap-1",
className,
)}
>
{showMeta && meta}
{showMeta && truncate && hasContent && <div className="text-muted-foreground/50 shrink-0">·</div>}
{contentNode}
{hasAttachments && <AttachmentThumbnails attachments={attachments} />}
</div>
</MemoViewContext.Provider>

View File

@ -1,9 +1,10 @@
import { ArrowUpRightIcon } from "lucide-react";
import { Link } from "react-router-dom";
import { MemoPreview } from "@/components/MemoPreview";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { useMemoComments } from "@/hooks/useMemoQueries";
import { useUsersByNames } from "@/hooks/useUserQueries";
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
import MemoSnippetLink from "./MemoSnippetLink";
const MemoCommentListView: React.FC = () => {
const { memo } = useMemoViewContext();
@ -11,13 +12,13 @@ const MemoCommentListView: React.FC = () => {
const { data } = useMemoComments(memo.name, { enabled: !isInMemoDetailPage && commentAmount > 0 });
const comments = data?.memos ?? [];
const displayedComments = comments.slice(0, 3);
const { data: commentCreators } = useUsersByNames(displayedComments.map((comment) => comment.creator));
if (isInMemoDetailPage || commentAmount === 0) {
return null;
}
const displayedComments = comments.slice(0, 3);
return (
<div className="border border-t-0 border-border rounded-b-lg px-4 pt-2 pb-3 flex flex-col gap-1">
<div className="flex items-center justify-between mb-1">
@ -32,14 +33,22 @@ const MemoCommentListView: React.FC = () => {
</div>
{displayedComments.map((comment) => {
const uid = extractMemoIdFromName(comment.name);
const creator = commentCreators?.get(comment.creator);
return (
<MemoSnippetLink
<Link
key={comment.name}
name={comment.name}
snippet={comment.snippet || comment.content}
to={`/${memo.name}#${uid}`}
className="bg-muted/40 rounded-md"
/>
viewTransition
className="rounded-md bg-muted/40 px-2 py-1 transition-colors hover:bg-muted/60"
>
<MemoPreview
content={comment.snippet || comment.content}
attachments={comment.attachments}
creator={creator}
showCreator
truncate
/>
</Link>
);
})}
</div>