mirror of https://github.com/usememos/memos.git
chore: enhance MemoView component structure
This commit is contained in:
parent
7aa8262ef2
commit
6dcf7cc74c
|
|
@ -1,6 +1,7 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { memo, useMemo, useRef, useState } from "react";
|
import { memo, useMemo, useRef, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { Memo } from "@/types/proto/api/v1/memo_service";
|
||||||
import MemoEditor from "../MemoEditor";
|
import MemoEditor from "../MemoEditor";
|
||||||
import PreviewImageDialog from "../PreviewImageDialog";
|
import PreviewImageDialog from "../PreviewImageDialog";
|
||||||
import { MemoBody, MemoHeader } from "./components";
|
import { MemoBody, MemoHeader } from "./components";
|
||||||
|
|
@ -16,40 +17,31 @@ import {
|
||||||
useNsfwContent,
|
useNsfwContent,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
import { MemoViewContext } from "./MemoViewContext";
|
import { MemoViewContext } from "./MemoViewContext";
|
||||||
import type { MemoViewProps } from "./types";
|
|
||||||
|
|
||||||
/**
|
interface Props {
|
||||||
* MemoView component displays a single memo card with full functionality including:
|
memo: Memo;
|
||||||
* - Creator information and display time
|
compact?: boolean;
|
||||||
* - Memo content with markdown rendering
|
showCreator?: boolean;
|
||||||
* - Attachments and location
|
showVisibility?: boolean;
|
||||||
* - Reactions and comments
|
showPinned?: boolean;
|
||||||
* - Edit mode with inline editor
|
showNsfwContent?: boolean;
|
||||||
* - Keyboard shortcuts for quick actions
|
className?: string;
|
||||||
* - NSFW content blur protection
|
parentPage?: string;
|
||||||
*/
|
}
|
||||||
const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
|
|
||||||
|
const MemoView: React.FC<Props> = observer((props: Props) => {
|
||||||
const { memo: memoData, className } = props;
|
const { memo: memoData, className } = props;
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// State
|
|
||||||
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
|
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
|
||||||
|
|
||||||
// Custom hooks for data fetching
|
|
||||||
const creator = useMemoCreator(memoData.creator);
|
const creator = useMemoCreator(memoData.creator);
|
||||||
|
const { commentAmount, relativeTimeFormat, isArchived, readonly, isInMemoDetailPage, parentPage } = useMemoViewDerivedState(
|
||||||
// Custom hooks for derived state
|
memoData,
|
||||||
const { commentAmount, relativeTimeFormat, isArchived, readonly, isInMemoDetailPage, parentPage } = useMemoViewDerivedState({
|
props.parentPage,
|
||||||
memo: memoData,
|
);
|
||||||
parentPage: props.parentPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom hooks for UI state management
|
|
||||||
const { nsfw, showNSFWContent, toggleNsfwVisibility } = useNsfwContent(memoData, props.showNsfwContent);
|
const { nsfw, showNSFWContent, toggleNsfwVisibility } = useNsfwContent(memoData, props.showNsfwContent);
|
||||||
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
|
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
|
||||||
const { showEditor, openEditor, handleEditorConfirm, handleEditorCancel } = useMemoEditor();
|
const { showEditor, openEditor, handleEditorConfirm, handleEditorCancel } = useMemoEditor();
|
||||||
|
|
||||||
// Custom hooks for actions
|
|
||||||
const { archiveMemo, unpinMemo } = useMemoActions(memoData);
|
const { archiveMemo, unpinMemo } = useMemoActions(memoData);
|
||||||
const { handleGotoMemoDetailPage, handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({
|
const { handleGotoMemoDetailPage, handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({
|
||||||
memoName: memoData.name,
|
memoName: memoData.name,
|
||||||
|
|
@ -58,9 +50,7 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
|
||||||
openEditor,
|
openEditor,
|
||||||
openPreview,
|
openPreview,
|
||||||
});
|
});
|
||||||
|
useKeyboardShortcuts(cardRef, {
|
||||||
// Keyboard shortcuts
|
|
||||||
const { handleShortcutActivation } = useKeyboardShortcuts(cardRef, {
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
readonly,
|
readonly,
|
||||||
showEditor,
|
showEditor,
|
||||||
|
|
@ -69,8 +59,6 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
|
||||||
onArchive: archiveMemo,
|
onArchive: archiveMemo,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize context value to prevent unnecessary re-renders
|
|
||||||
// IMPORTANT: This must be before the early return to satisfy Rules of Hooks
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
memo: memoData,
|
memo: memoData,
|
||||||
|
|
@ -87,7 +75,6 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
|
||||||
[memoData, creator, isArchived, readonly, isInMemoDetailPage, parentPage, commentAmount, relativeTimeFormat, nsfw, showNSFWContent],
|
[memoData, creator, isArchived, readonly, isInMemoDetailPage, parentPage, commentAmount, relativeTimeFormat, nsfw, showNSFWContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render inline editor when editing
|
|
||||||
if (showEditor) {
|
if (showEditor) {
|
||||||
return (
|
return (
|
||||||
<MemoEditor
|
<MemoEditor
|
||||||
|
|
@ -101,16 +88,9 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render memo card
|
|
||||||
return (
|
return (
|
||||||
<MemoViewContext.Provider value={contextValue}>
|
<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, className)}
|
|
||||||
ref={cardRef}
|
|
||||||
tabIndex={readonly ? -1 : 0}
|
|
||||||
onFocus={() => handleShortcutActivation(true)}
|
|
||||||
onBlur={() => handleShortcutActivation(false)}
|
|
||||||
>
|
|
||||||
<MemoHeader
|
<MemoHeader
|
||||||
showCreator={props.showCreator}
|
showCreator={props.showCreator}
|
||||||
showVisibility={props.showVisibility}
|
showVisibility={props.showVisibility}
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,21 @@ import { createContext, useContext } from "react";
|
||||||
import type { Memo } from "@/types/proto/api/v1/memo_service";
|
import type { Memo } from "@/types/proto/api/v1/memo_service";
|
||||||
import type { User } from "@/types/proto/api/v1/user_service";
|
import type { User } from "@/types/proto/api/v1/user_service";
|
||||||
|
|
||||||
/**
|
|
||||||
* Context value for MemoView component tree
|
|
||||||
* Provides shared state and props to child components
|
|
||||||
*/
|
|
||||||
export interface MemoViewContextValue {
|
export interface MemoViewContextValue {
|
||||||
/** The memo data */
|
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
/** The memo creator user data */
|
|
||||||
creator: User | undefined;
|
creator: User | undefined;
|
||||||
/** Whether the memo is in archived state */
|
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
/** Whether the current user can only view (not edit) the memo */
|
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
/** Whether we're currently on the memo detail page */
|
|
||||||
isInMemoDetailPage: boolean;
|
isInMemoDetailPage: boolean;
|
||||||
/** Parent page path for navigation state */
|
|
||||||
parentPage: string;
|
parentPage: string;
|
||||||
/** Number of comments on this memo */
|
|
||||||
commentAmount: number;
|
commentAmount: number;
|
||||||
/** Time format to use (datetime for old memos, auto for recent) */
|
|
||||||
relativeTimeFormat: "datetime" | "auto";
|
relativeTimeFormat: "datetime" | "auto";
|
||||||
/** Whether this memo contains NSFW content */
|
|
||||||
nsfw: boolean;
|
nsfw: boolean;
|
||||||
/** Whether to show NSFW content without blur */
|
|
||||||
showNSFWContent: boolean;
|
showNSFWContent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Context for sharing MemoView state across child components
|
|
||||||
* This eliminates prop drilling for commonly used values
|
|
||||||
*/
|
|
||||||
export const MemoViewContext = createContext<MemoViewContextValue | null>(null);
|
export const MemoViewContext = createContext<MemoViewContextValue | null>(null);
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to access MemoView context
|
|
||||||
* @throws Error if used outside of MemoViewContext.Provider
|
|
||||||
*/
|
|
||||||
export const useMemoViewContext = (): MemoViewContextValue => {
|
export const useMemoViewContext = (): MemoViewContextValue => {
|
||||||
const context = useContext(MemoViewContext);
|
const context = useContext(MemoViewContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,15 @@ import MemoContent from "../../MemoContent";
|
||||||
import { MemoReactionListView } from "../../MemoReactionListView";
|
import { MemoReactionListView } from "../../MemoReactionListView";
|
||||||
import { AttachmentList, LocationDisplay, RelationList } from "../../memo-metadata";
|
import { AttachmentList, LocationDisplay, RelationList } from "../../memo-metadata";
|
||||||
import { useMemoViewContext } from "../MemoViewContext";
|
import { useMemoViewContext } from "../MemoViewContext";
|
||||||
import type { MemoBodyProps } from "../types";
|
|
||||||
|
|
||||||
/**
|
interface Props {
|
||||||
* MemoBody component displays the main content of a memo including:
|
compact?: boolean;
|
||||||
* - Memo content (markdown)
|
onContentClick: (e: React.MouseEvent) => void;
|
||||||
* - Location display
|
onContentDoubleClick: (e: React.MouseEvent) => void;
|
||||||
* - Attachments
|
onToggleNsfwVisibility: () => void;
|
||||||
* - Related memos
|
}
|
||||||
* - Reactions
|
|
||||||
* - NSFW content overlay
|
const MemoBody: React.FC<Props> = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => {
|
||||||
*/
|
|
||||||
const MemoBody: React.FC<MemoBodyProps> = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => {
|
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
|
|
||||||
// Get shared state from context
|
// Get shared state from context
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,20 @@ import { ReactionSelector } from "../../reactions";
|
||||||
import UserAvatar from "../../UserAvatar";
|
import UserAvatar from "../../UserAvatar";
|
||||||
import VisibilityIcon from "../../VisibilityIcon";
|
import VisibilityIcon from "../../VisibilityIcon";
|
||||||
import { useMemoViewContext } from "../MemoViewContext";
|
import { useMemoViewContext } from "../MemoViewContext";
|
||||||
import type { MemoHeaderProps } from "../types";
|
|
||||||
|
|
||||||
/**
|
interface Props {
|
||||||
* MemoHeader component displays the top section of a memo card including:
|
showCreator?: boolean;
|
||||||
* - Creator info (avatar, name) when showCreator is true
|
showVisibility?: boolean;
|
||||||
* - Display time (relative or absolute)
|
showPinned?: boolean;
|
||||||
* - Reaction selector
|
onEdit: () => void;
|
||||||
* - Comment count link
|
onGotoDetail: () => void;
|
||||||
* - Visibility icon
|
onUnpin: () => void;
|
||||||
* - Pin indicator
|
onToggleNsfwVisibility?: () => void;
|
||||||
* - NSFW hide button
|
reactionSelectorOpen: boolean;
|
||||||
* - Action menu
|
onReactionSelectorOpenChange: (open: boolean) => void;
|
||||||
*/
|
}
|
||||||
const MemoHeader: React.FC<MemoHeaderProps> = ({
|
|
||||||
|
const MemoHeader: React.FC<Props> = ({
|
||||||
showCreator,
|
showCreator,
|
||||||
showVisibility,
|
showVisibility,
|
||||||
showPinned,
|
showPinned,
|
||||||
|
|
@ -130,9 +130,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Creator display with avatar and name
|
|
||||||
*/
|
|
||||||
interface CreatorDisplayProps {
|
interface CreatorDisplayProps {
|
||||||
creator: User;
|
creator: User;
|
||||||
displayTime: React.ReactNode;
|
displayTime: React.ReactNode;
|
||||||
|
|
@ -163,9 +160,6 @@ const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, o
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple time display without creator info
|
|
||||||
*/
|
|
||||||
interface TimeDisplayProps {
|
interface TimeDisplayProps {
|
||||||
displayTime: React.ReactNode;
|
displayTime: React.ReactNode;
|
||||||
onGotoDetail: () => void;
|
onGotoDetail: () => void;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,8 @@
|
||||||
/**
|
|
||||||
* Constants for MemoView component
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** CSS class for memo card styling */
|
|
||||||
export const MEMO_CARD_BASE_CLASSES =
|
export const MEMO_CARD_BASE_CLASSES =
|
||||||
"relative group flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 text-card-foreground rounded-lg border border-border transition-colors";
|
"relative group flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 text-card-foreground rounded-lg border border-border transition-colors";
|
||||||
|
|
||||||
/** Keyboard shortcut keys */
|
export const KEYBOARD_SHORTCUTS = { EDIT: "e", ARCHIVE: "a" } as const;
|
||||||
export const KEYBOARD_SHORTCUTS = {
|
|
||||||
EDIT: "e",
|
|
||||||
ARCHIVE: "a",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Text input element types for keyboard shortcut filtering */
|
|
||||||
export const TEXT_INPUT_TYPES = ["text", "search", "email", "password", "url", "tel", "number"] as const;
|
export const TEXT_INPUT_TYPES = ["text", "search", "email", "password", "url", "tel", "number"] as const;
|
||||||
|
|
||||||
/** Time threshold for relative time format (24 hours in milliseconds) */
|
|
||||||
export const RELATIVE_TIME_THRESHOLD_MS = 1000 * 60 * 60 * 24;
|
export const RELATIVE_TIME_THRESHOLD_MS = 1000 * 60 * 60 * 24;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
export type { UseMemoEditorReturn } from "./useMemoEditor";
|
|
||||||
export { useMemoEditor } from "./useMemoEditor";
|
export { useMemoEditor } from "./useMemoEditor";
|
||||||
export type { UseMemoHandlersOptions, UseMemoHandlersReturn } from "./useMemoHandlers";
|
|
||||||
export { useMemoHandlers } from "./useMemoHandlers";
|
export { useMemoHandlers } from "./useMemoHandlers";
|
||||||
export type { UseMemoViewDerivedStateOptions, UseMemoViewDerivedStateReturn } from "./useMemoViewDerivedState";
|
|
||||||
export { useMemoViewDerivedState } from "./useMemoViewDerivedState";
|
export { useMemoViewDerivedState } from "./useMemoViewDerivedState";
|
||||||
export { useImagePreview, useKeyboardShortcuts, useMemoActions, useMemoCreator, useNsfwContent } from "./useMemoViewState";
|
export { useImagePreview, useKeyboardShortcuts, useMemoActions, useMemoCreator, useNsfwContent } from "./useMemoViewState";
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,16 @@
|
||||||
import { useCallback, useState } from "react";
|
import { useState } from "react";
|
||||||
import { userStore } from "@/store";
|
import { userStore } from "@/store";
|
||||||
|
|
||||||
export interface UseMemoEditorReturn {
|
export const useMemoEditor = () => {
|
||||||
showEditor: boolean;
|
|
||||||
openEditor: () => void;
|
|
||||||
handleEditorConfirm: () => void;
|
|
||||||
handleEditorCancel: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for managing memo editor state and actions
|
|
||||||
* Encapsulates all editor-related state and handlers
|
|
||||||
*/
|
|
||||||
export const useMemoEditor = (): UseMemoEditorReturn => {
|
|
||||||
const [showEditor, setShowEditor] = useState(false);
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
|
|
||||||
const openEditor = useCallback(() => {
|
|
||||||
setShowEditor(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleEditorConfirm = useCallback(() => {
|
|
||||||
setShowEditor(false);
|
|
||||||
userStore.setStatsStateId();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleEditorCancel = useCallback(() => {
|
|
||||||
setShowEditor(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showEditor,
|
showEditor,
|
||||||
openEditor,
|
openEditor: () => setShowEditor(true),
|
||||||
handleEditorConfirm,
|
handleEditorConfirm: () => {
|
||||||
handleEditorCancel,
|
setShowEditor(false);
|
||||||
|
userStore.setStatsStateId();
|
||||||
|
},
|
||||||
|
handleEditorCancel: () => setShowEditor(false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,32 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||||
import { instanceStore } from "@/store";
|
import { instanceStore } from "@/store";
|
||||||
import type { UseImagePreviewReturn } from "../types";
|
|
||||||
|
|
||||||
export interface UseMemoHandlersOptions {
|
interface UseMemoHandlersOptions {
|
||||||
memoName: string;
|
memoName: string;
|
||||||
parentPage: string;
|
parentPage: string;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
openEditor: () => void;
|
openEditor: () => void;
|
||||||
openPreview: UseImagePreviewReturn["openPreview"];
|
openPreview: (url: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseMemoHandlersReturn {
|
export const useMemoHandlers = (options: UseMemoHandlersOptions) => {
|
||||||
handleGotoMemoDetailPage: () => void;
|
|
||||||
handleMemoContentClick: (e: React.MouseEvent) => void;
|
|
||||||
handleMemoContentDoubleClick: (e: React.MouseEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for managing memo event handlers
|
|
||||||
* Centralizes all click and interaction handlers
|
|
||||||
*/
|
|
||||||
export const useMemoHandlers = (options: UseMemoHandlersOptions): UseMemoHandlersReturn => {
|
|
||||||
const { memoName, parentPage, readonly, openEditor, openPreview } = options;
|
const { memoName, parentPage, readonly, openEditor, openPreview } = options;
|
||||||
const navigateTo = useNavigateTo();
|
const navigateTo = useNavigateTo();
|
||||||
|
|
||||||
|
// These useCallbacks are necessary since they have real dependencies
|
||||||
const handleGotoMemoDetailPage = useCallback(() => {
|
const handleGotoMemoDetailPage = useCallback(() => {
|
||||||
navigateTo(`/${memoName}`, {
|
navigateTo(`/${memoName}`, { state: { from: parentPage } });
|
||||||
state: { from: parentPage },
|
|
||||||
});
|
|
||||||
}, [memoName, parentPage, navigateTo]);
|
}, [memoName, parentPage, navigateTo]);
|
||||||
|
|
||||||
const handleMemoContentClick = useCallback(
|
const handleMemoContentClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
const targetEl = e.target as HTMLElement;
|
const targetEl = e.target as HTMLElement;
|
||||||
|
|
||||||
if (targetEl.tagName === "IMG") {
|
if (targetEl.tagName === "IMG") {
|
||||||
// Check if the image is inside a link
|
|
||||||
const linkElement = targetEl.closest("a");
|
const linkElement = targetEl.closest("a");
|
||||||
if (linkElement) {
|
if (linkElement) return; // If image is inside a link, don't show preview
|
||||||
// If image is inside a link, only navigate to the link (don't show preview)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imgUrl = targetEl.getAttribute("src");
|
const imgUrl = targetEl.getAttribute("src");
|
||||||
if (imgUrl) {
|
if (imgUrl) openPreview(imgUrl);
|
||||||
openPreview(imgUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[openPreview],
|
[openPreview],
|
||||||
|
|
@ -55,9 +35,7 @@ export const useMemoHandlers = (options: UseMemoHandlersOptions): UseMemoHandler
|
||||||
const handleMemoContentDoubleClick = useCallback(
|
const handleMemoContentDoubleClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
|
if (instanceStore.state.memoRelatedSetting.enableDoubleClickEdit) {
|
||||||
const instanceMemoRelatedSetting = instanceStore.state.memoRelatedSetting;
|
|
||||||
if (instanceMemoRelatedSetting.enableDoubleClickEdit) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openEditor();
|
openEditor();
|
||||||
}
|
}
|
||||||
|
|
@ -65,9 +43,5 @@ export const useMemoHandlers = (options: UseMemoHandlersOptions): UseMemoHandler
|
||||||
[readonly, openEditor],
|
[readonly, openEditor],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return { handleGotoMemoDetailPage, handleMemoContentClick, handleMemoContentDoubleClick };
|
||||||
handleGotoMemoDetailPage,
|
|
||||||
handleMemoContentClick,
|
|
||||||
handleMemoContentDoubleClick,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useMemo } from "react";
|
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { State } from "@/types/proto/api/v1/common";
|
import { State } from "@/types/proto/api/v1/common";
|
||||||
|
|
@ -7,55 +6,21 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service";
|
||||||
import { isSuperUser } from "@/utils/user";
|
import { isSuperUser } from "@/utils/user";
|
||||||
import { RELATIVE_TIME_THRESHOLD_MS } from "../constants";
|
import { RELATIVE_TIME_THRESHOLD_MS } from "../constants";
|
||||||
|
|
||||||
export interface UseMemoViewDerivedStateOptions {
|
export const useMemoViewDerivedState = (memo: Memo, parentPageProp?: string) => {
|
||||||
memo: Memo;
|
|
||||||
parentPage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseMemoViewDerivedStateReturn {
|
|
||||||
commentAmount: number;
|
|
||||||
relativeTimeFormat: "datetime" | "auto";
|
|
||||||
isArchived: boolean;
|
|
||||||
readonly: boolean;
|
|
||||||
isInMemoDetailPage: boolean;
|
|
||||||
parentPage: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for computing derived state from memo data
|
|
||||||
* Centralizes all computed values to avoid repetition and improve readability
|
|
||||||
*/
|
|
||||||
export const useMemoViewDerivedState = (options: UseMemoViewDerivedStateOptions): UseMemoViewDerivedStateReturn => {
|
|
||||||
const { memo, parentPage: parentPageProp } = options;
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
|
|
||||||
// Compute all derived state
|
const commentAmount = memo.relations.filter(
|
||||||
const commentAmount = useMemo(
|
(relation) => relation.type === MemoRelation_Type.COMMENT && relation.relatedMemo?.name === memo.name,
|
||||||
() =>
|
).length;
|
||||||
memo.relations.filter((relation) => relation.type === MemoRelation_Type.COMMENT && relation.relatedMemo?.name === memo.name).length,
|
|
||||||
[memo.relations, memo.name],
|
|
||||||
);
|
|
||||||
|
|
||||||
const relativeTimeFormat: "datetime" | "auto" = useMemo(
|
const relativeTimeFormat: "datetime" | "auto" =
|
||||||
() => (memo.displayTime && Date.now() - memo.displayTime.getTime() > RELATIVE_TIME_THRESHOLD_MS ? "datetime" : "auto"),
|
memo.displayTime && Date.now() - memo.displayTime.getTime() > RELATIVE_TIME_THRESHOLD_MS ? "datetime" : "auto";
|
||||||
[memo.displayTime],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isArchived = useMemo(() => memo.state === State.ARCHIVED, [memo.state]);
|
const isArchived = memo.state === State.ARCHIVED;
|
||||||
|
const readonly = memo.creator !== user?.name && !isSuperUser(user);
|
||||||
|
const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`);
|
||||||
|
const parentPage = parentPageProp || location.pathname;
|
||||||
|
|
||||||
const readonly = useMemo(() => memo.creator !== user?.name && !isSuperUser(user), [memo.creator, user]);
|
return { commentAmount, relativeTimeFormat, isArchived, readonly, isInMemoDetailPage, parentPage };
|
||||||
|
|
||||||
const isInMemoDetailPage = useMemo(() => location.pathname.startsWith(`/${memo.name}`), [location.pathname, memo.name]);
|
|
||||||
|
|
||||||
const parentPage = useMemo(() => parentPageProp || location.pathname, [parentPageProp, location.pathname]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
commentAmount,
|
|
||||||
relativeTimeFormat,
|
|
||||||
isArchived,
|
|
||||||
readonly,
|
|
||||||
isInMemoDetailPage,
|
|
||||||
parentPage,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,34 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { instanceStore, memoStore, userStore } from "@/store";
|
import { instanceStore, memoStore, userStore } from "@/store";
|
||||||
import { State } from "@/types/proto/api/v1/common";
|
import { State } from "@/types/proto/api/v1/common";
|
||||||
import type { Memo } from "@/types/proto/api/v1/memo_service";
|
import type { Memo } from "@/types/proto/api/v1/memo_service";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { KEYBOARD_SHORTCUTS, TEXT_INPUT_TYPES } from "../constants";
|
import { KEYBOARD_SHORTCUTS, TEXT_INPUT_TYPES } from "../constants";
|
||||||
import type {
|
|
||||||
ImagePreviewState,
|
|
||||||
UseImagePreviewReturn,
|
|
||||||
UseKeyboardShortcutsOptions,
|
|
||||||
UseMemoActionsReturn,
|
|
||||||
UseNsfwContentReturn,
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
/**
|
interface ImagePreviewState {
|
||||||
* Hook for handling memo actions (archive, unpin)
|
open: boolean;
|
||||||
*/
|
urls: string[];
|
||||||
export const useMemoActions = (memo: Memo): UseMemoActionsReturn => {
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseKeyboardShortcutsOptions {
|
||||||
|
enabled: boolean;
|
||||||
|
readonly: boolean;
|
||||||
|
showEditor: boolean;
|
||||||
|
isArchived: boolean;
|
||||||
|
onEdit: () => void;
|
||||||
|
onArchive: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMemoActions = (memo: Memo) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const isArchived = memo.state === State.ARCHIVED;
|
const isArchived = memo.state === State.ARCHIVED;
|
||||||
|
|
||||||
const archiveMemo = useCallback(async () => {
|
const archiveMemo = async () => {
|
||||||
if (isArchived) {
|
if (isArchived) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await memoStore.updateMemo(
|
await memoStore.updateMemo({ name: memo.name, state: State.ARCHIVED }, ["state"]);
|
||||||
{
|
|
||||||
name: memo.name,
|
|
||||||
state: State.ARCHIVED,
|
|
||||||
},
|
|
||||||
["state"],
|
|
||||||
);
|
|
||||||
toast.success(t("message.archived-successfully"));
|
toast.success(t("message.archived-successfully"));
|
||||||
userStore.setStatsStateId();
|
userStore.setStatsStateId();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|
@ -40,66 +36,37 @@ export const useMemoActions = (memo: Memo): UseMemoActionsReturn => {
|
||||||
const err = error as { details?: string };
|
const err = error as { details?: string };
|
||||||
toast.error(err?.details || "Failed to archive memo");
|
toast.error(err?.details || "Failed to archive memo");
|
||||||
}
|
}
|
||||||
}, [isArchived, memo.name, t]);
|
};
|
||||||
|
|
||||||
const unpinMemo = useCallback(async () => {
|
const unpinMemo = async () => {
|
||||||
if (!memo.pinned) {
|
if (!memo.pinned) return;
|
||||||
return;
|
await memoStore.updateMemo({ name: memo.name, pinned: false }, ["pinned"]);
|
||||||
}
|
};
|
||||||
|
|
||||||
await memoStore.updateMemo(
|
|
||||||
{
|
|
||||||
name: memo.name,
|
|
||||||
pinned: false,
|
|
||||||
},
|
|
||||||
["pinned"],
|
|
||||||
);
|
|
||||||
}, [memo.name, memo.pinned]);
|
|
||||||
|
|
||||||
return { archiveMemo, unpinMemo };
|
return { archiveMemo, unpinMemo };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const isTextInputElement = (element: HTMLElement | null): boolean => {
|
||||||
* Hook for handling keyboard shortcuts on the memo card
|
if (!element) return false;
|
||||||
*/
|
if (element.isContentEditable) return true;
|
||||||
export const useKeyboardShortcuts = (
|
if (element instanceof HTMLTextAreaElement) return true;
|
||||||
cardRef: React.RefObject<HTMLDivElement | null>,
|
if (element instanceof HTMLInputElement) {
|
||||||
options: UseKeyboardShortcutsOptions,
|
return TEXT_INPUT_TYPES.includes(element.type as (typeof TEXT_INPUT_TYPES)[number]);
|
||||||
): {
|
}
|
||||||
shortcutActive: boolean;
|
return false;
|
||||||
handleShortcutActivation: (active: boolean) => void;
|
};
|
||||||
} => {
|
|
||||||
|
export const useKeyboardShortcuts = (cardRef: React.RefObject<HTMLDivElement | null>, options: UseKeyboardShortcutsOptions) => {
|
||||||
const { enabled, readonly, showEditor, isArchived, onEdit, onArchive } = options;
|
const { enabled, readonly, showEditor, isArchived, onEdit, onArchive } = options;
|
||||||
const [shortcutActive, setShortcutActive] = useState(false);
|
|
||||||
|
|
||||||
const isTextInputElement = useCallback((element: HTMLElement | null): boolean => {
|
|
||||||
if (!element) return false;
|
|
||||||
if (element.isContentEditable) return true;
|
|
||||||
if (element instanceof HTMLTextAreaElement) return true;
|
|
||||||
|
|
||||||
if (element instanceof HTMLInputElement) {
|
|
||||||
return TEXT_INPUT_TYPES.includes(element.type as (typeof TEXT_INPUT_TYPES)[number]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled || readonly || showEditor || !cardRef.current) {
|
if (!enabled || readonly || showEditor || !cardRef.current) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cardEl = cardRef.current;
|
const cardEl = cardRef.current;
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
const target = event.target as HTMLElement | null;
|
const target = event.target as HTMLElement | null;
|
||||||
if (!cardEl.contains(target) || isTextInputElement(target)) {
|
if (!cardEl.contains(target) || isTextInputElement(target)) return;
|
||||||
return;
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
if (key === KEYBOARD_SHORTCUTS.EDIT) {
|
if (key === KEYBOARD_SHORTCUTS.EDIT) {
|
||||||
|
|
@ -113,29 +80,10 @@ export const useKeyboardShortcuts = (
|
||||||
|
|
||||||
cardEl.addEventListener("keydown", handleKeyDown);
|
cardEl.addEventListener("keydown", handleKeyDown);
|
||||||
return () => cardEl.removeEventListener("keydown", handleKeyDown);
|
return () => cardEl.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [enabled, readonly, showEditor, isArchived, onEdit, onArchive, cardRef, isTextInputElement]);
|
}, [enabled, readonly, showEditor, isArchived, onEdit, onArchive, cardRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showEditor || readonly) {
|
|
||||||
setShortcutActive(false);
|
|
||||||
}
|
|
||||||
}, [showEditor, readonly]);
|
|
||||||
|
|
||||||
const handleShortcutActivation = useCallback(
|
|
||||||
(active: boolean) => {
|
|
||||||
if (readonly) return;
|
|
||||||
setShortcutActive(active);
|
|
||||||
},
|
|
||||||
[readonly],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { shortcutActive, handleShortcutActivation };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const useNsfwContent = (memo: Memo, initialShowNsfw?: boolean) => {
|
||||||
* Hook for managing NSFW content visibility
|
|
||||||
*/
|
|
||||||
export const useNsfwContent = (memo: Memo, initialShowNsfw?: boolean): UseNsfwContentReturn => {
|
|
||||||
const [showNSFWContent, setShowNSFWContent] = useState(initialShowNsfw ?? false);
|
const [showNSFWContent, setShowNSFWContent] = useState(initialShowNsfw ?? false);
|
||||||
const instanceMemoRelatedSetting = instanceStore.state.memoRelatedSetting;
|
const instanceMemoRelatedSetting = instanceStore.state.memoRelatedSetting;
|
||||||
|
|
||||||
|
|
@ -143,50 +91,23 @@ export const useNsfwContent = (memo: Memo, initialShowNsfw?: boolean): UseNsfwCo
|
||||||
instanceMemoRelatedSetting.enableBlurNsfwContent &&
|
instanceMemoRelatedSetting.enableBlurNsfwContent &&
|
||||||
memo.tags?.some((tag) => instanceMemoRelatedSetting.nsfwTags.some((nsfwTag) => tag === nsfwTag || tag.startsWith(`${nsfwTag}/`)));
|
memo.tags?.some((tag) => instanceMemoRelatedSetting.nsfwTags.some((nsfwTag) => tag === nsfwTag || tag.startsWith(`${nsfwTag}/`)));
|
||||||
|
|
||||||
const toggleNsfwVisibility = useCallback(() => {
|
|
||||||
setShowNSFWContent((prev) => !prev);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nsfw: nsfw ?? false,
|
nsfw: nsfw ?? false,
|
||||||
showNSFWContent,
|
showNSFWContent,
|
||||||
toggleNsfwVisibility,
|
toggleNsfwVisibility: () => setShowNSFWContent((prev) => !prev),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const useImagePreview = () => {
|
||||||
* Hook for managing image preview dialog state
|
const [previewState, setPreviewState] = useState<ImagePreviewState>({ open: false, urls: [], index: 0 });
|
||||||
*/
|
|
||||||
export const useImagePreview = (): UseImagePreviewReturn => {
|
|
||||||
const [previewState, setPreviewState] = useState<ImagePreviewState>({
|
|
||||||
open: false,
|
|
||||||
urls: [],
|
|
||||||
index: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const openPreview = useCallback((url: string) => {
|
|
||||||
setPreviewState({ open: true, urls: [url], index: 0 });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closePreview = useCallback(() => {
|
|
||||||
setPreviewState((prev) => ({ ...prev, open: false }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setPreviewOpen = useCallback((open: boolean) => {
|
|
||||||
setPreviewState((prev) => ({ ...prev, open }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
previewState,
|
previewState,
|
||||||
openPreview,
|
openPreview: (url: string) => setPreviewState({ open: true, urls: [url], index: 0 }),
|
||||||
closePreview,
|
setPreviewOpen: (open: boolean) => setPreviewState((prev) => ({ ...prev, open })),
|
||||||
setPreviewOpen,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for fetching and managing memo creator data
|
|
||||||
*/
|
|
||||||
export const useMemoCreator = (creatorName: string) => {
|
export const useMemoCreator = (creatorName: string) => {
|
||||||
const [creator, setCreator] = useState(userStore.getUserByName(creatorName));
|
const [creator, setCreator] = useState(userStore.getUserByName(creatorName));
|
||||||
const fetchedRef = useRef(false);
|
const fetchedRef = useRef(false);
|
||||||
|
|
@ -194,7 +115,6 @@ export const useMemoCreator = (creatorName: string) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fetchedRef.current) return;
|
if (fetchedRef.current) return;
|
||||||
fetchedRef.current = true;
|
fetchedRef.current = true;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const user = await userStore.getOrFetchUserByName(creatorName);
|
const user = await userStore.getOrFetchUserByName(creatorName);
|
||||||
setCreator(user);
|
setCreator(user);
|
||||||
|
|
|
||||||
|
|
@ -10,33 +10,4 @@
|
||||||
|
|
||||||
export { MemoBody, MemoHeader } from "./components";
|
export { MemoBody, MemoHeader } from "./components";
|
||||||
export * from "./constants";
|
export * from "./constants";
|
||||||
export type {
|
|
||||||
UseMemoEditorReturn,
|
|
||||||
UseMemoHandlersOptions,
|
|
||||||
UseMemoHandlersReturn,
|
|
||||||
UseMemoViewDerivedStateOptions,
|
|
||||||
UseMemoViewDerivedStateReturn,
|
|
||||||
} from "./hooks";
|
|
||||||
export {
|
|
||||||
useImagePreview,
|
|
||||||
useKeyboardShortcuts,
|
|
||||||
useMemoActions,
|
|
||||||
useMemoCreator,
|
|
||||||
useMemoEditor,
|
|
||||||
useMemoHandlers,
|
|
||||||
useMemoViewDerivedState,
|
|
||||||
useNsfwContent,
|
|
||||||
} from "./hooks";
|
|
||||||
export { default, default as MemoView } from "./MemoView";
|
export { default, default as MemoView } from "./MemoView";
|
||||||
export type { MemoViewContextValue } from "./MemoViewContext";
|
|
||||||
export { MemoViewContext, useMemoViewContext } from "./MemoViewContext";
|
|
||||||
export type {
|
|
||||||
ImagePreviewState,
|
|
||||||
MemoBodyProps,
|
|
||||||
MemoHeaderProps,
|
|
||||||
MemoViewProps,
|
|
||||||
UseImagePreviewReturn,
|
|
||||||
UseKeyboardShortcutsOptions,
|
|
||||||
UseMemoActionsReturn,
|
|
||||||
UseNsfwContentReturn,
|
|
||||||
} from "./types";
|
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
import type { Memo } from "@/types/proto/api/v1/memo_service";
|
|
||||||
import type { User } from "@/types/proto/api/v1/user_service";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the MemoView component
|
|
||||||
*/
|
|
||||||
export interface MemoViewProps {
|
|
||||||
/** The memo data to display */
|
|
||||||
memo: Memo;
|
|
||||||
/** Enable compact mode with truncated content */
|
|
||||||
compact?: boolean;
|
|
||||||
/** Show creator avatar and name */
|
|
||||||
showCreator?: boolean;
|
|
||||||
/** Show visibility icon */
|
|
||||||
showVisibility?: boolean;
|
|
||||||
/** Show pinned indicator */
|
|
||||||
showPinned?: boolean;
|
|
||||||
/** Show NSFW content without blur */
|
|
||||||
showNsfwContent?: boolean;
|
|
||||||
/** Additional CSS classes */
|
|
||||||
className?: string;
|
|
||||||
/** Parent page path for navigation state */
|
|
||||||
parentPage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the MemoHeader component
|
|
||||||
* Note: Most data props now come from MemoViewContext
|
|
||||||
*/
|
|
||||||
export interface MemoHeaderProps {
|
|
||||||
// Display options
|
|
||||||
showCreator?: boolean;
|
|
||||||
showVisibility?: boolean;
|
|
||||||
showPinned?: boolean;
|
|
||||||
// Callbacks
|
|
||||||
onEdit: () => void;
|
|
||||||
onGotoDetail: () => void;
|
|
||||||
onUnpin: () => void;
|
|
||||||
onToggleNsfwVisibility?: () => void;
|
|
||||||
// Reaction state
|
|
||||||
reactionSelectorOpen: boolean;
|
|
||||||
onReactionSelectorOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the MemoBody component
|
|
||||||
* Note: Most data props now come from MemoViewContext
|
|
||||||
*/
|
|
||||||
export interface MemoBodyProps {
|
|
||||||
// Display options
|
|
||||||
compact?: boolean;
|
|
||||||
// Callbacks
|
|
||||||
onContentClick: (e: React.MouseEvent) => void;
|
|
||||||
onContentDoubleClick: (e: React.MouseEvent) => void;
|
|
||||||
onToggleNsfwVisibility: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State for image preview dialog
|
|
||||||
*/
|
|
||||||
export interface ImagePreviewState {
|
|
||||||
open: boolean;
|
|
||||||
urls: string[];
|
|
||||||
index: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return type for useMemoActions hook
|
|
||||||
*/
|
|
||||||
export interface UseMemoActionsReturn {
|
|
||||||
archiveMemo: () => Promise<void>;
|
|
||||||
unpinMemo: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return type for useKeyboardShortcuts hook
|
|
||||||
*/
|
|
||||||
export interface UseKeyboardShortcutsOptions {
|
|
||||||
enabled: boolean;
|
|
||||||
readonly: boolean;
|
|
||||||
showEditor: boolean;
|
|
||||||
isArchived: boolean;
|
|
||||||
onEdit: () => void;
|
|
||||||
onArchive: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return type for useNsfwContent hook
|
|
||||||
*/
|
|
||||||
export interface UseNsfwContentReturn {
|
|
||||||
nsfw: boolean;
|
|
||||||
showNSFWContent: boolean;
|
|
||||||
toggleNsfwVisibility: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return type for useImagePreview hook
|
|
||||||
*/
|
|
||||||
export interface UseImagePreviewReturn {
|
|
||||||
previewState: ImagePreviewState;
|
|
||||||
openPreview: (url: string) => void;
|
|
||||||
closePreview: () => void;
|
|
||||||
setPreviewOpen: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue