feat(memo): add image sharing in detail view

Keep the unpublished image-sharing flow scoped to memo detail pages.

- add a dedicated share-image preview and export pipeline
- measure the rendered memo card so preview and exported image stay aligned
- move the entry point into the detail sidebar and drawer only
This commit is contained in:
boojack 2026-04-06 18:30:01 +08:00
parent 2cbc70762b
commit 38fc22b754
15 changed files with 440 additions and 13 deletions

View File

@ -38,6 +38,7 @@
"dayjs": "^1.11.20",
"fuse.js": "^7.1.0",
"highlight.js": "^11.11.1",
"html-to-image": "^1.11.13",
"i18next": "^25.8.18",
"katex": "^0.16.38",
"leaflet": "^1.9.4",

View File

@ -83,6 +83,9 @@ importers:
highlight.js:
specifier: ^11.11.1
version: 11.11.1
html-to-image:
specifier: ^1.11.13
version: 1.11.13
i18next:
specifier: ^25.8.18
version: 25.8.18(typescript@6.0.2)
@ -2013,6 +2016,9 @@ packages:
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
html-to-image@1.11.13:
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@ -4688,6 +4694,8 @@ snapshots:
dependencies:
void-elements: 3.1.0
html-to-image@1.11.13: {}
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}

View File

@ -0,0 +1,114 @@
import { DownloadIcon, ImageIcon, Loader2Icon, Share2Icon } from "lucide-react";
import { useCallback, useMemo, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useTranslate } from "@/utils/i18n";
import { useMemoViewContext } from "../MemoView/MemoViewContext";
import MemoShareImagePreview from "./MemoShareImagePreview";
import { buildMemoShareImageFileName, createMemoShareImageBlob, getMemoShareDialogWidth, getMemoSharePreviewWidth } from "./memoShareImage";
interface MemoShareImageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
const MemoShareImageDialog = ({ open, onOpenChange }: MemoShareImageDialogProps) => {
const t = useTranslate();
const { memo, cardWidth } = useMemoViewContext();
const previewRef = useRef<HTMLDivElement>(null);
const [isRendering, setIsRendering] = useState(false);
const previewWidth = useMemo(() => getMemoSharePreviewWidth(cardWidth), [cardWidth]);
const dialogWidth = useMemo(() => getMemoShareDialogWidth(previewWidth), [previewWidth]);
const createShareBlob = useCallback(async () => {
const preview = previewRef.current;
if (!preview) {
throw new Error("Preview is not ready");
}
return createMemoShareImageBlob(preview);
}, []);
const handleDownload = useCallback(async () => {
setIsRendering(true);
try {
const blob = await createShareBlob();
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = buildMemoShareImageFileName(memo.name);
anchor.click();
URL.revokeObjectURL(url);
toast.success(t("memo.share.image-downloaded"));
} catch {
toast.error(t("memo.share.image-download-failed"));
} finally {
setIsRendering(false);
}
}, [createShareBlob, memo.name, t]);
const handleNativeShare = useCallback(async () => {
if (typeof navigator.share !== "function") {
return;
}
setIsRendering(true);
try {
const blob = await createShareBlob();
const file = new File([blob], buildMemoShareImageFileName(memo.name), { type: "image/png" });
if (typeof navigator.canShare === "function" && !navigator.canShare({ files: [file] })) {
toast.error(t("memo.share.image-share-failed"));
return;
}
await navigator.share({
files: [file],
title: memo.content.slice(0, 60),
});
} catch (error) {
if (!(error instanceof DOMException && error.name === "AbortError")) {
toast.error(t("memo.share.image-share-failed"));
}
} finally {
setIsRendering(false);
}
}, [createShareBlob, memo.content, memo.name, t]);
const supportsNativeShare =
typeof navigator !== "undefined" && typeof navigator.share === "function" && typeof navigator.canShare === "function";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent size="full" className="md:w-auto md:max-w-none" style={{ width: `${dialogWidth}px` }}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ImageIcon className="h-4 w-4" />
{t("memo.share.image-title")}
</DialogTitle>
<DialogDescription>{t("memo.share.image-description", { width: previewWidth })}</DialogDescription>
</DialogHeader>
<div className="overflow-auto p-1 sm:p-2">
<MemoShareImagePreview ref={previewRef} width={previewWidth} />
</div>
<DialogFooter>
{supportsNativeShare && (
<Button variant="outline" onClick={handleNativeShare} disabled={isRendering}>
{isRendering ? <Loader2Icon className="mr-2 h-4 w-4 animate-spin" /> : <Share2Icon className="mr-2 h-4 w-4" />}
{t("memo.share.image-share")}
</Button>
)}
<Button onClick={handleDownload} disabled={isRendering}>
{isRendering ? <Loader2Icon className="mr-2 h-4 w-4 animate-spin" /> : <DownloadIcon className="mr-2 h-4 w-4" />}
{t("memo.share.image-download")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default MemoShareImageDialog;

View File

@ -0,0 +1,113 @@
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { forwardRef, useMemo } from "react";
import MemoContent from "@/components/MemoContent";
import { separateAttachments } from "@/components/MemoMetadata/Attachment/attachmentHelpers";
import UserAvatar from "@/components/UserAvatar";
import i18n from "@/i18n";
import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n";
import { buildAttachmentVisualItems, countLogicalAttachmentItems } from "@/utils/media-item";
import { useMemoViewContext } from "../MemoView/MemoViewContext";
import { getMemoSharePreviewAvatarUrl } from "./memoShareImage";
const MemoShareImagePreview = forwardRef<HTMLDivElement, { width: number }>(({ width }, ref) => {
const t = useTranslate();
const { memo, creator, blurred, showBlurredContent } = useMemoViewContext();
const displayName = creator?.displayName || creator?.username || t("common.memo");
const avatarUrl = getMemoSharePreviewAvatarUrl(creator?.avatarUrl);
const displayTime = memo.displayTime ? timestampDate(memo.displayTime) : memo.createTime ? timestampDate(memo.createTime) : undefined;
const formattedDisplayTime = displayTime?.toLocaleString(i18n.language, {
dateStyle: "medium",
timeStyle: "short",
});
const { attachmentCount, nonVisualAttachmentCount, visualItems } = useMemo(() => {
const attachmentGroups = separateAttachments(memo.attachments);
const previewVisualItems = buildAttachmentVisualItems(attachmentGroups.visual);
const totalAttachmentCount = countLogicalAttachmentItems(memo.attachments);
return {
attachmentCount: totalAttachmentCount,
nonVisualAttachmentCount: totalAttachmentCount - previewVisualItems.length,
visualItems: previewVisualItems,
};
}, [memo.attachments]);
return (
<div
ref={ref}
className="relative overflow-hidden rounded-[24px] border border-border/50 bg-linear-to-br from-background via-muted/15 to-background p-2.5 sm:p-3"
style={{ width }}
>
<div className="pointer-events-none absolute -top-16 right-0 h-32 w-32 rounded-full bg-sky-500/8 blur-3xl" />
<div className="pointer-events-none absolute -bottom-20 -left-10 h-36 w-36 rounded-full bg-amber-400/8 blur-3xl" />
<div className="relative overflow-hidden rounded-[20px] border border-border/60 bg-background/98 p-4 shadow-sm shadow-foreground/5 sm:p-5">
<div className="flex items-start gap-3">
<div className="flex min-w-0 items-center gap-2.5">
<UserAvatar avatarUrl={avatarUrl} className="h-9 w-9 rounded-2xl" />
<div className="min-w-0">
<div className="truncate text-[13px] font-semibold text-foreground">{displayName}</div>
{formattedDisplayTime && <div className="truncate text-xs text-muted-foreground">{formattedDisplayTime}</div>}
</div>
</div>
</div>
<div className="mt-4">
<div className={cn("pointer-events-none", blurred && !showBlurredContent && "blur-lg")}>
<MemoContent content={memo.content} compact={false} contentClassName="text-[14px] leading-6.5 sm:text-[15px]" />
</div>
</div>
{visualItems.length > 0 && (
<div className={cn("mt-4 grid gap-1.5", visualItems.length === 1 ? "grid-cols-1" : "grid-cols-2")}>
{visualItems.slice(0, 4).map((item, index) => (
<div
key={item.id}
className={cn(
"relative overflow-hidden rounded-[18px] border border-border/70 bg-muted/40",
visualItems.length === 1 ? "aspect-[4/3]" : "aspect-square",
visualItems.length === 3 && index === 0 && "col-span-2 aspect-[2.2/1]",
)}
>
<img src={item.posterUrl} alt={item.filename} className="h-full w-full object-cover" loading="eager" decoding="async" />
{index === 3 && visualItems.length > 4 && (
<div className="absolute inset-0 flex items-center justify-center bg-foreground/35 text-lg font-semibold text-background">
+{visualItems.length - 4}
</div>
)}
</div>
))}
</div>
)}
{(memo.tags.length > 0 || nonVisualAttachmentCount > 0) && (
<div className="mt-4 flex flex-wrap items-center gap-1.5">
{memo.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="inline-flex rounded-full border border-border/70 bg-muted/55 px-2 py-0.5 text-[11px] text-muted-foreground"
>
#{tag}
</span>
))}
{memo.tags.length > 3 && (
<span className="inline-flex rounded-full border border-border/70 bg-muted/55 px-2 py-0.5 text-[11px] text-muted-foreground">
+{memo.tags.length - 3}
</span>
)}
{nonVisualAttachmentCount > 0 && (
<span className="inline-flex rounded-full border border-border/70 bg-muted/55 px-2 py-0.5 text-[11px] text-muted-foreground">
{attachmentCount} {t("common.attachments").toLowerCase()}
</span>
)}
</div>
)}
</div>
</div>
);
});
MemoShareImagePreview.displayName = "MemoShareImagePreview";
export default MemoShareImagePreview;

View File

@ -0,0 +1,110 @@
import { toBlob } from "html-to-image";
const WINDOW_HORIZONTAL_MARGIN = 32;
export const MEMO_SHARE_IMAGE_CONFIG = {
dialogExtraWidth: 80,
maxWidth: 520,
minWidth: 260,
previewScale: 0.9,
viewportMargin: 48,
} as const;
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
const isExportableImageUrl = (value?: string) => {
if (!value) {
return false;
}
if (value.startsWith("/") || value.startsWith("data:") || value.startsWith("blob:")) {
return true;
}
try {
return new URL(value, window.location.origin).origin === window.location.origin;
} catch {
return false;
}
};
const waitForPreviewAssets = async (node: HTMLElement) => {
try {
await document.fonts?.ready;
} catch {
// Ignore font loading failures and continue with the best available render.
}
const images = Array.from(node.querySelectorAll("img"));
await Promise.all(
images.map(
(image) =>
new Promise<void>((resolve) => {
if (image.complete) {
resolve();
return;
}
image.addEventListener("load", () => resolve(), { once: true });
image.addEventListener("error", () => resolve(), { once: true });
}),
),
);
};
export const buildMemoShareImageFileName = (memoName: string) => {
const suffix = memoName.split("/").pop() ?? "memo";
return `memo-${suffix}.png`;
};
export const getMemoSharePreviewWidth = (cardWidth: number) => {
const viewportWidth =
typeof window === "undefined" ? MEMO_SHARE_IMAGE_CONFIG.maxWidth : window.innerWidth - MEMO_SHARE_IMAGE_CONFIG.viewportMargin;
const baseWidth = cardWidth || viewportWidth;
return clamp(
Math.round(baseWidth * MEMO_SHARE_IMAGE_CONFIG.previewScale),
MEMO_SHARE_IMAGE_CONFIG.minWidth,
MEMO_SHARE_IMAGE_CONFIG.maxWidth,
);
};
export const getMemoShareDialogWidth = (previewWidth: number) => {
const viewportWidth =
typeof window === "undefined" ? previewWidth + MEMO_SHARE_IMAGE_CONFIG.dialogExtraWidth : window.innerWidth - WINDOW_HORIZONTAL_MARGIN;
return Math.min(previewWidth + MEMO_SHARE_IMAGE_CONFIG.dialogExtraWidth, viewportWidth);
};
export const getMemoSharePreviewAvatarUrl = (avatarUrl?: string) => (isExportableImageUrl(avatarUrl) ? avatarUrl : undefined);
export const createMemoShareImageBlob = async (node: HTMLElement) => {
await waitForPreviewAssets(node);
const rect = node.getBoundingClientRect();
const width = Math.ceil(rect.width || node.offsetWidth || node.clientWidth);
const height = Math.ceil(rect.height || node.offsetHeight || node.clientHeight);
const blob = await toBlob(node, {
cacheBust: true,
height,
pixelRatio: Math.max(2, Math.min(window.devicePixelRatio || 1, 3)),
width,
filter: (currentNode) => {
if (!(currentNode instanceof HTMLElement)) {
return true;
}
if (currentNode instanceof HTMLImageElement) {
return isExportableImageUrl(currentNode.currentSrc || currentNode.src);
}
return !(currentNode instanceof HTMLVideoElement);
},
});
if (!blob) {
throw new Error("Failed to render image");
}
return blob;
};

View File

@ -1,7 +1,7 @@
import { create } from "@bufbuild/protobuf";
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { isEqual } from "lodash-es";
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon, type LucideIcon, Share2Icon } from "lucide-react";
import { CheckCircleIcon, Code2Icon, HashIcon, ImageIcon, LinkIcon, type LucideIcon, Share2Icon } from "lucide-react";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import useCurrentUser from "@/hooks/useCurrentUser";
@ -16,6 +16,7 @@ import MemoSharePanel from "./MemoSharePanel";
interface Props {
memo: Memo;
className?: string;
onShareImageOpen?: () => void;
}
interface PropertyBadge {
@ -39,7 +40,7 @@ const PROPERTY_BADGE_CLASSES =
const TAG_BADGE_CLASSES =
"inline-flex items-center gap-1 px-1 rounded-md border border-border/60 bg-muted/60 text-sm text-muted-foreground hover:bg-muted hover:text-foreground/80 transition-colors cursor-pointer";
const MemoDetailSidebar = ({ memo, className }: Props) => {
const MemoDetailSidebar = ({ memo, className, onShareImageOpen }: Props) => {
const t = useTranslate();
const currentUser = useCurrentUser();
const [sharePanelOpen, setSharePanelOpen] = useState(false);
@ -64,12 +65,22 @@ const MemoDetailSidebar = ({ memo, className }: Props) => {
</SidebarSection>
)}
{canManageShares && (
{(canManageShares || onShareImageOpen) && (
<SidebarSection label={t("memo.share.section-label")}>
<Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}>
<Share2Icon className="w-4 h-4" />
{t("memo.share.open-panel")}
</Button>
<div className="flex flex-col gap-2">
{onShareImageOpen && (
<Button variant="outline" className="w-full justify-start gap-2" onClick={onShareImageOpen}>
<ImageIcon className="w-4 h-4" />
{t("memo.share.open-image")}
</Button>
)}
{canManageShares && (
<Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}>
<Share2Icon className="w-4 h-4" />
{t("memo.share.open-panel")}
</Button>
)}
</div>
</SidebarSection>
)}

View File

@ -8,9 +8,10 @@ import MemoDetailSidebar from "./MemoDetailSidebar";
interface Props {
memo: Memo;
onShareImageOpen?: () => void;
}
const MemoDetailSidebarDrawer = ({ memo }: Props) => {
const MemoDetailSidebarDrawer = ({ memo, onShareImageOpen }: Props) => {
const location = useLocation();
const [open, setOpen] = useState(false);
@ -26,7 +27,7 @@ const MemoDetailSidebarDrawer = ({ memo }: Props) => {
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-full sm:w-80 px-4 bg-background">
<MemoDetailSidebar className="py-4" memo={memo} />
<MemoDetailSidebar className="py-4" memo={memo} onShareImageOpen={onShareImageOpen} />
</SheetContent>
</Sheet>
);

View File

@ -27,6 +27,7 @@ const STUB_CONTEXT: MemoViewContextValue = {
creator: undefined,
currentUser: undefined,
parentPage: "/",
cardWidth: 0,
isArchived: false,
readonly: true,
showBlurredContent: false,

View File

@ -1,4 +1,4 @@
import { memo, useCallback, useMemo, useRef, useState } from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { useInstance } from "@/contexts/InstanceContext";
import useCurrentUser from "@/hooks/useCurrentUser";
@ -7,6 +7,7 @@ import { findTagMetadata } from "@/lib/tag";
import { cn } from "@/lib/utils";
import { State } from "@/types/proto/api/v1/common_pb";
import { isSuperUser } from "@/utils/user";
import MemoShareImageDialog from "../MemoActionMenu/MemoShareImageDialog";
import MemoEditor from "../MemoEditor";
import PreviewImageDialog from "../PreviewImageDialog";
import { MemoBody, MemoCommentListView, MemoHeader } from "./components";
@ -19,6 +20,7 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const { memo: memoData, className, parentPage: parentPageProp, compact, showCreator, showVisibility, showPinned } = props;
const cardRef = useRef<HTMLDivElement>(null);
const [showEditor, setShowEditor] = useState(false);
const [cardWidth, setCardWidth] = useState(0);
const currentUser = useCurrentUser();
const { tagsSetting } = useInstance();
@ -41,12 +43,40 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`) || location.pathname.startsWith("/memos/shares/");
const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0;
useEffect(() => {
const card = cardRef.current;
if (!card) {
return;
}
const updateWidth = (nextWidth?: number) => {
const width = Math.round(nextWidth ?? card.getBoundingClientRect().width);
setCardWidth((prev) => (prev === width ? prev : width));
};
updateWidth();
if (typeof ResizeObserver === "undefined") {
const handleResize = () => updateWidth();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}
const resizeObserver = new ResizeObserver((entries) => {
updateWidth(entries[0]?.contentRect.width);
});
resizeObserver.observe(card);
return () => resizeObserver.disconnect();
}, []);
const contextValue = useMemo(
() => ({
memo: memoData,
creator,
currentUser,
parentPage,
cardWidth,
isArchived,
readonly,
showBlurredContent,
@ -60,6 +90,7 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
creator,
currentUser,
parentPage,
cardWidth,
isArchived,
readonly,
showBlurredContent,
@ -100,6 +131,10 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
items={previewState.items}
initialIndex={previewState.index}
/>
{props.onShareImageDialogOpenChange && (
<MemoShareImageDialog open={Boolean(props.shareImageDialogOpen)} onOpenChange={props.onShareImageDialogOpenChange} />
)}
</article>
);

View File

@ -12,6 +12,7 @@ export interface MemoViewContextValue {
creator: User | undefined;
currentUser: User | undefined;
parentPage: string;
cardWidth: number;
isArchived: boolean;
readonly: boolean;
showBlurredContent: boolean;

View File

@ -8,6 +8,8 @@ export interface MemoViewProps {
showPinned?: boolean;
className?: string;
parentPage?: string;
shareImageDialogOpen?: boolean;
onShareImageDialogOpenChange?: (open: boolean) => void;
}
export interface MemoHeaderProps {

View File

@ -249,10 +249,19 @@
"expiry-7-days": "7 days",
"expiry-label": "Expires",
"expiry-never": "Never",
"image-description": "Preview rendered at your current device width ({{width}}px).",
"image-download": "Download PNG",
"image-download-failed": "Failed to export image",
"image-downloaded": "Image saved",
"image-footer": "Shared from Memos",
"image-share": "Share image",
"image-share-failed": "Failed to share image",
"image-title": "Share as image",
"expires-on": "Expires {{date}}",
"invalid-link": "This link is invalid or has expired.",
"never-expires": "Never expires",
"no-links": "No share links yet. Create one below.",
"open-image": "Share as image",
"open-panel": "Manage share links",
"revoke": "Revoke",
"revoke-failed": "Failed to revoke link",

View File

@ -221,10 +221,19 @@
"expiry-7-days": "7天",
"expiry-label": "到期时间",
"expiry-never": "永不",
"image-description": "预览会按你当前设备上的展示宽度({{width}}px渲染。",
"image-download": "下载 PNG",
"image-download-failed": "导出图片失败",
"image-downloaded": "图片已保存",
"image-footer": "由 Memos 生成分享图",
"image-share": "分享图片",
"image-share-failed": "分享图片失败",
"image-title": "分享成图片",
"expires-on": "{{date}} 到期",
"invalid-link": "该链接无效或已过期。",
"never-expires": "永不过期",
"no-links": "还没有分享链接。在下面创建一个。",
"open-image": "分享成图片",
"open-panel": "管理分享链接",
"revoke": "撤销",
"revoke-failed": "撤销链接失败",

View File

@ -221,10 +221,19 @@
"expiry-7-days": "7天",
"expiry-label": "到期時間",
"expiry-never": "永不",
"image-description": "預覽會依照你目前裝置上的顯示寬度({{width}}px渲染。",
"image-download": "下載 PNG",
"image-download-failed": "匯出圖片失敗",
"image-downloaded": "圖片已儲存",
"image-footer": "由 Memos 產生分享圖",
"image-share": "分享圖片",
"image-share-failed": "分享圖片失敗",
"image-title": "分享成圖片",
"expires-on": "{{date}} 到期",
"invalid-link": "該連結無效或已過期。",
"never-expires": "永不過期",
"no-links": "還沒有分享連結,請在下方建立。",
"open-image": "分享成圖片",
"open-panel": "管理分享連結",
"revoke": "撤銷",
"revoke-failed": "撤銷連結失敗",

View File

@ -1,6 +1,6 @@
import { Code, ConnectError } from "@connectrpc/connect";
import { ArrowUpLeftFromCircleIcon } from "lucide-react";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Link, Navigate, useLocation, useParams } from "react-router-dom";
import MemoCommentSection from "@/components/MemoCommentSection";
import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
@ -16,6 +16,7 @@ import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
const MemoDetail = () => {
const md = useMediaQuery("md");
const [shareImageDialogOpen, setShareImageDialogOpen] = useState(false);
const params = useParams();
const location = useLocation();
const { state: locationState, hash } = location;
@ -77,7 +78,7 @@ const MemoDetail = () => {
<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={displayMemo} />
<MemoDetailSidebarDrawer memo={displayMemo} onShareImageOpen={() => setShareImageDialogOpen(true)} />
</MobileHeader>
)}
<div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
@ -100,15 +101,17 @@ const MemoDetail = () => {
memo={displayMemo}
compact={false}
parentPage={locationState?.from}
shareImageDialogOpen={shareImageDialogOpen}
showCreator
showVisibility
showPinned
onShareImageDialogOpenChange={setShareImageDialogOpen}
/>
<MemoCommentSection memo={displayMemo} comments={comments} parentPage={locationState?.from} />
</div>
{md && (
<div className="sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full">
<MemoDetailSidebar className="py-6" memo={displayMemo} />
<MemoDetailSidebar className="py-6" memo={displayMemo} onShareImageOpen={() => setShareImageDialogOpen(true)} />
</div>
)}
</div>