diff --git a/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx b/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx index 776573185..d3e8dfa33 100644 --- a/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx +++ b/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx @@ -52,24 +52,33 @@ const InsertMenu = (props: InsertMenuProps) => { }); const location = useLocation(props.location); + const { + state: locationState, + locationInitialized, + handlePositionChange: handleLocationPositionChange, + getLocation, + reset: locationReset, + updateCoordinate, + setPlaceholder, + } = location; const [debouncedPosition, setDebouncedPosition] = useState(undefined); useDebounce( () => { - setDebouncedPosition(location.state.position); + setDebouncedPosition(locationState.position); }, 1000, - [location.state.position], + [locationState.position], ); const { data: displayName } = useReverseGeocoding(debouncedPosition?.lat, debouncedPosition?.lng); useEffect(() => { if (displayName) { - location.setPlaceholder(displayName); + setPlaceholder(displayName); } - }, [displayName]); + }, [displayName, setPlaceholder]); const isUploading = selectingFlag || isUploadingProp; @@ -79,11 +88,11 @@ const InsertMenu = (props: InsertMenuProps) => { const handleLocationClick = useCallback(() => { setLocationDialogOpen(true); - if (!initialLocation && !location.locationInitialized) { + if (!initialLocation && !locationInitialized) { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { - location.handlePositionChange(new LatLng(position.coords.latitude, position.coords.longitude)); + handleLocationPositionChange(new LatLng(position.coords.latitude, position.coords.longitude)); }, (error) => { console.error("Geolocation error:", error); @@ -91,27 +100,20 @@ const InsertMenu = (props: InsertMenuProps) => { ); } } - }, [initialLocation, location]); + }, [initialLocation, locationInitialized, handleLocationPositionChange]); const handleLocationConfirm = useCallback(() => { - const newLocation = location.getLocation(); + const newLocation = getLocation(); if (newLocation) { onLocationChange(newLocation); setLocationDialogOpen(false); } - }, [location, onLocationChange]); + }, [getLocation, onLocationChange]); const handleLocationCancel = useCallback(() => { - location.reset(); + locationReset(); setLocationDialogOpen(false); - }, [location]); - - const handlePositionChange = useCallback( - (position: LatLng) => { - location.handlePositionChange(position); - }, - [location], - ); + }, [locationReset]); const handleToggleFocusMode = useCallback(() => { onToggleFocusMode?.(); @@ -200,11 +202,10 @@ const InsertMenu = (props: InsertMenuProps) => { diff --git a/web/src/components/MemoEditor/components/LocationDialog.tsx b/web/src/components/MemoEditor/components/LocationDialog.tsx index 9d5376e50..8b670a64c 100644 --- a/web/src/components/MemoEditor/components/LocationDialog.tsx +++ b/web/src/components/MemoEditor/components/LocationDialog.tsx @@ -12,7 +12,6 @@ export const LocationDialog = ({ open, onOpenChange, state, - locationInitialized: _locationInitialized, onPositionChange, onUpdateCoordinate, onPlaceholderChange, diff --git a/web/src/components/MemoEditor/constants.ts b/web/src/components/MemoEditor/constants.ts index 477d1cc26..c135a3300 100644 --- a/web/src/components/MemoEditor/constants.ts +++ b/web/src/components/MemoEditor/constants.ts @@ -1,5 +1,3 @@ -export const LOCALSTORAGE_DEBOUNCE_DELAY = 500; - export const FOCUS_MODE_STYLES = { backdrop: "fixed inset-0 bg-black/20 backdrop-blur-sm z-40", container: { diff --git a/web/src/components/MemoEditor/hooks/useKeyboard.ts b/web/src/components/MemoEditor/hooks/useKeyboard.ts index 880806370..b09307e4d 100644 --- a/web/src/components/MemoEditor/hooks/useKeyboard.ts +++ b/web/src/components/MemoEditor/hooks/useKeyboard.ts @@ -1,11 +1,10 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import type { EditorRefActions } from "../Editor"; -interface UseKeyboardOptions { - onSave: () => void; -} +export const useKeyboard = (editorRef: React.RefObject, onSave: () => void) => { + const onSaveRef = useRef(onSave); + onSaveRef.current = onSave; -export const useKeyboard = (editorRef: React.RefObject, options: UseKeyboardOptions) => { useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!(event.metaKey || event.ctrlKey) || event.key !== "Enter") { @@ -24,10 +23,10 @@ export const useKeyboard = (editorRef: React.RefObject, } event.preventDefault(); - options.onSave(); + onSaveRef.current(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [editorRef, options]); + }, [editorRef]); }; diff --git a/web/src/components/MemoEditor/hooks/useLocation.ts b/web/src/components/MemoEditor/hooks/useLocation.ts index 334df849a..5243fd92d 100644 --- a/web/src/components/MemoEditor/hooks/useLocation.ts +++ b/web/src/components/MemoEditor/hooks/useLocation.ts @@ -1,11 +1,14 @@ import { create } from "@bufbuild/protobuf"; import { LatLng } from "leaflet"; -import { useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { Location, LocationSchema } from "@/types/proto/api/v1/memo_service_pb"; import { LocationState } from "../types/insert-menu"; export const useLocation = (initialLocation?: Location) => { const [locationInitialized, setLocationInitialized] = useState(false); + const locationInitializedRef = useRef(locationInitialized); + locationInitializedRef.current = locationInitialized; + const [state, setState] = useState({ placeholder: initialLocation?.placeholder || "", position: initialLocation ? new LatLng(initialLocation.latitude, initialLocation.longitude) : undefined, @@ -13,34 +16,48 @@ export const useLocation = (initialLocation?: Location) => { lngInput: initialLocation ? String(initialLocation.longitude) : "", }); - const updatePosition = (position?: LatLng) => { + // Ref to latest state so getLocation can be stable without closing over state. + const stateRef = useRef(state); + stateRef.current = state; + + const updatePosition = useCallback((position?: LatLng) => { setState((prev) => ({ ...prev, position, latInput: position ? String(position.lat) : "", lngInput: position ? String(position.lng) : "", })); - }; + }, []); - const handlePositionChange = (position: LatLng) => { - if (!locationInitialized) setLocationInitialized(true); - updatePosition(position); - }; + // Stable — reads locationInitialized via ref to avoid recreating on every change. + const handlePositionChange = useCallback( + (position: LatLng) => { + if (!locationInitializedRef.current) setLocationInitialized(true); + updatePosition(position); + }, + [updatePosition], + ); - const updateCoordinate = (type: "lat" | "lng", value: string) => { - setState((prev) => ({ ...prev, [type === "lat" ? "latInput" : "lngInput"]: value })); + // Stable — merges coordinate update into a single functional setState, avoiding closure over state.position. + const updateCoordinate = useCallback((type: "lat" | "lng", value: string) => { const num = parseFloat(value); const isValid = type === "lat" ? !isNaN(num) && num >= -90 && num <= 90 : !isNaN(num) && num >= -180 && num <= 180; - if (isValid && state.position) { - updatePosition(type === "lat" ? new LatLng(num, state.position.lng) : new LatLng(state.position.lat, num)); - } - }; + setState((prev) => { + const next = { ...prev, [type === "lat" ? "latInput" : "lngInput"]: value }; + if (isValid && prev.position) { + const newPos = type === "lat" ? new LatLng(num, prev.position.lng) : new LatLng(prev.position.lat, num); + return { ...next, position: newPos, latInput: String(newPos.lat), lngInput: String(newPos.lng) }; + } + return next; + }); + }, []); - const setPlaceholder = (placeholder: string) => { + // Stable reference — uses functional setState, no closure deps. + const setPlaceholder = useCallback((placeholder: string) => { setState((prev) => ({ ...prev, placeholder })); - }; + }, []); - const reset = () => { + const reset = useCallback(() => { setState({ placeholder: "", position: undefined, @@ -48,26 +65,23 @@ export const useLocation = (initialLocation?: Location) => { lngInput: "", }); setLocationInitialized(false); - }; + }, []); - const getLocation = (): Location | undefined => { - if (!state.position || !state.placeholder.trim()) { + // Stable — reads latest state via ref, no closure over state. + const getLocation = useCallback((): Location | undefined => { + const { position, placeholder } = stateRef.current; + if (!position || !placeholder.trim()) { return undefined; } return create(LocationSchema, { - latitude: state.position.lat, - longitude: state.position.lng, - placeholder: state.placeholder, + latitude: position.lat, + longitude: position.lng, + placeholder, }); - }; + }, []); - return { - state, - locationInitialized, - handlePositionChange, - updateCoordinate, - setPlaceholder, - reset, - getLocation, - }; + return useMemo( + () => ({ state, locationInitialized, handlePositionChange, updateCoordinate, setPlaceholder, reset, getLocation }), + [state, locationInitialized, handlePositionChange, updateCoordinate, setPlaceholder, reset, getLocation], + ); }; diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index fb75dfe07..bfbbc8775 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -57,7 +57,7 @@ const MemoEditorImpl: React.FC = ({ dispatch(actions.toggleFocusMode()); }; - useKeyboard(editorRef, { onSave: handleSave }); + useKeyboard(editorRef, handleSave); async function handleSave() { // Validate before saving @@ -145,7 +145,7 @@ const MemoEditorImpl: React.FC = ({ )} {/* Editor content grows to fill available space in focus mode */} - + {/* Metadata and toolbar grouped together at bottom */}
diff --git a/web/src/components/MemoEditor/services/memoService.ts b/web/src/components/MemoEditor/services/memoService.ts index db6338fc4..57fbad4e1 100644 --- a/web/src/components/MemoEditor/services/memoService.ts +++ b/web/src/components/MemoEditor/services/memoService.ts @@ -135,7 +135,6 @@ export const memoService = { ui: { isFocusMode: false, isLoading: { saving: false, uploading: false, loading: false }, - isDragging: false, isComposing: false, }, timestamps: { diff --git a/web/src/components/MemoEditor/state/actions.ts b/web/src/components/MemoEditor/state/actions.ts index 0c990c3e0..166a50489 100644 --- a/web/src/components/MemoEditor/state/actions.ts +++ b/web/src/components/MemoEditor/state/actions.ts @@ -62,11 +62,6 @@ export const editorActions = { payload: { key, value }, }), - setDragging: (value: boolean): EditorAction => ({ - type: "SET_DRAGGING", - payload: value, - }), - setComposing: (value: boolean): EditorAction => ({ type: "SET_COMPOSING", payload: value, diff --git a/web/src/components/MemoEditor/state/reducer.ts b/web/src/components/MemoEditor/state/reducer.ts index df0a0d3c3..d2add61d6 100644 --- a/web/src/components/MemoEditor/state/reducer.ts +++ b/web/src/components/MemoEditor/state/reducer.ts @@ -101,15 +101,6 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS }, }; - case "SET_DRAGGING": - return { - ...state, - ui: { - ...state.ui, - isDragging: action.payload, - }, - }; - case "SET_COMPOSING": return { ...state, diff --git a/web/src/components/MemoEditor/state/types.ts b/web/src/components/MemoEditor/state/types.ts index 07aa732e5..f10bba1e0 100644 --- a/web/src/components/MemoEditor/state/types.ts +++ b/web/src/components/MemoEditor/state/types.ts @@ -20,7 +20,6 @@ export interface EditorState { uploading: boolean; loading: boolean; }; - isDragging: boolean; isComposing: boolean; }; timestamps: { @@ -43,7 +42,6 @@ export type EditorAction = | { type: "CLEAR_LOCAL_FILES" } | { type: "TOGGLE_FOCUS_MODE" } | { type: "SET_LOADING"; payload: { key: LoadingKey; value: boolean } } - | { type: "SET_DRAGGING"; payload: boolean } | { type: "SET_COMPOSING"; payload: boolean } | { type: "SET_TIMESTAMPS"; payload: Partial } | { type: "RESET" }; @@ -63,7 +61,6 @@ export const initialState: EditorState = { uploading: false, loading: false, }, - isDragging: false, isComposing: false, }, timestamps: { diff --git a/web/src/components/MemoEditor/types/components.ts b/web/src/components/MemoEditor/types/components.ts index efa80bf36..b53d7530f 100644 --- a/web/src/components/MemoEditor/types/components.ts +++ b/web/src/components/MemoEditor/types/components.ts @@ -18,7 +18,6 @@ export interface MemoEditorProps { export interface EditorContentProps { placeholder?: string; - autoFocus?: boolean; } export interface EditorToolbarProps { @@ -57,7 +56,6 @@ export interface LocationDialogProps { open: boolean; onOpenChange: (open: boolean) => void; state: LocationState; - locationInitialized: boolean; onPositionChange: (position: LatLng) => void; onUpdateCoordinate: (type: "lat" | "lng", value: string) => void; onPlaceholderChange: (placeholder: string) => void; diff --git a/web/src/components/MemoEditor/types/context.ts b/web/src/components/MemoEditor/types/context.ts deleted file mode 100644 index 456a9fdee..000000000 --- a/web/src/components/MemoEditor/types/context.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createContext } from "react"; -import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; -import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; -import type { LocalFile } from "./attachment"; - -export interface MemoEditorContextValue { - attachmentList: Attachment[]; - relationList: MemoRelation[]; - setAttachmentList: (attachmentList: Attachment[]) => void; - setRelationList: (relationList: MemoRelation[]) => void; - memoName?: string; - addLocalFiles?: (files: LocalFile[]) => void; - removeLocalFile?: (previewUrl: string) => void; - localFiles?: LocalFile[]; -} - -const defaultContextValue: MemoEditorContextValue = { - attachmentList: [], - relationList: [], - setAttachmentList: () => {}, - setRelationList: () => {}, - addLocalFiles: () => {}, - removeLocalFile: () => {}, - localFiles: [], -}; - -export const MemoEditorContext = createContext(defaultContextValue); diff --git a/web/src/components/MemoEditor/types/index.ts b/web/src/components/MemoEditor/types/index.ts index 77485691a..758a21d90 100644 --- a/web/src/components/MemoEditor/types/index.ts +++ b/web/src/components/MemoEditor/types/index.ts @@ -15,5 +15,4 @@ export type { TagSuggestionsProps, VisibilitySelectorProps, } from "./components"; -export { MemoEditorContext, type MemoEditorContextValue } from "./context"; export type { LocationState } from "./insert-menu"; diff --git a/web/src/components/MemoPreview/MemoPreview.tsx b/web/src/components/MemoPreview/MemoPreview.tsx index e63f111d1..3c1272039 100644 --- a/web/src/components/MemoPreview/MemoPreview.tsx +++ b/web/src/components/MemoPreview/MemoPreview.tsx @@ -23,6 +23,9 @@ const STUB_CONTEXT: MemoViewContextValue = { readonly: true, showNSFWContent: false, nsfw: false, + openEditor: () => {}, + toggleNsfwVisibility: () => {}, + openPreview: () => {}, }; const AttachmentThumbnails = ({ attachments }: { attachments: Attachment[] }) => { diff --git a/web/src/components/MemoView/MemoView.tsx b/web/src/components/MemoView/MemoView.tsx index 2b834c9d0..d494b0f6b 100644 --- a/web/src/components/MemoView/MemoView.tsx +++ b/web/src/components/MemoView/MemoView.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useMemo, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useUser } from "@/hooks/useUserQueries"; @@ -9,7 +9,7 @@ import MemoEditor from "../MemoEditor"; import PreviewImageDialog from "../PreviewImageDialog"; import { MemoBody, MemoCommentListView, MemoHeader } from "./components"; import { MEMO_CARD_BASE_CLASSES } from "./constants"; -import { useImagePreview, useMemoActions, useMemoHandlers } from "./hooks"; +import { useImagePreview } from "./hooks"; import { computeCommentAmount, MemoViewContext } from "./MemoViewContext"; import type { MemoViewProps } from "./types"; @@ -27,21 +27,12 @@ const MemoView: React.FC = (props: MemoViewProps) => { // NSFW content management: always blur content tagged with NSFW (case-insensitive) const [showNSFWContent, setShowNSFWContent] = useState(false); const nsfw = memoData.tags?.some((tag) => tag.toUpperCase() === "NSFW") ?? false; - const toggleNsfwVisibility = () => setShowNSFWContent((prev) => !prev); + const toggleNsfwVisibility = useCallback(() => setShowNSFWContent((prev) => !prev), []); const { previewState, openPreview, setPreviewOpen } = useImagePreview(); - const { unpinMemo } = useMemoActions(memoData); - const closeEditor = () => setShowEditor(false); - const openEditor = () => setShowEditor(true); - - const { handleGotoMemoDetailPage, handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({ - memoName: memoData.name, - parentPage, - readonly, - openEditor, - openPreview, - }); + const openEditor = useCallback(() => setShowEditor(true), []); + const closeEditor = useCallback(() => setShowEditor(false), []); const location = useLocation(); const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`); @@ -57,8 +48,23 @@ const MemoView: React.FC = (props: MemoViewProps) => { readonly, showNSFWContent, nsfw, + openEditor, + toggleNsfwVisibility, + openPreview, }), - [memoData, creator, currentUser, parentPage, isArchived, readonly, showNSFWContent, nsfw], + [ + memoData, + creator, + currentUser, + parentPage, + isArchived, + readonly, + showNSFWContent, + nsfw, + openEditor, + toggleNsfwVisibility, + openPreview, + ], ); if (showEditor) { @@ -80,21 +86,9 @@ const MemoView: React.FC = (props: MemoViewProps) => { ref={cardRef} tabIndex={readonly ? -1 : 0} > - + - + void; + toggleNsfwVisibility: () => void; + openPreview: (urls: string | string[], index?: number) => void; } export const MemoViewContext = createContext(null); diff --git a/web/src/components/MemoView/components/MemoBody.tsx b/web/src/components/MemoView/components/MemoBody.tsx index 8847ddf0a..8d4506433 100644 --- a/web/src/components/MemoView/components/MemoBody.tsx +++ b/web/src/components/MemoView/components/MemoBody.tsx @@ -3,6 +3,7 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; import MemoContent from "../../MemoContent"; import { MemoReactionListView } from "../../MemoReactionListView"; +import { useMemoHandlers } from "../hooks"; import { useMemoViewContext } from "../MemoViewContext"; import type { MemoBodyProps } from "../types"; import { AttachmentList, LocationDisplay, RelationList } from "./metadata"; @@ -21,8 +22,10 @@ const NsfwOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => { ); }; -const MemoBody: React.FC = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => { - const { memo, parentPage, showNSFWContent, nsfw } = useMemoViewContext(); +const MemoBody: React.FC = ({ compact }) => { + const { memo, parentPage, showNSFWContent, nsfw, readonly, openEditor, openPreview, toggleNsfwVisibility } = useMemoViewContext(); + + const { handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({ readonly, openEditor, openPreview }); const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); @@ -37,8 +40,8 @@ const MemoBody: React.FC = ({ compact, onContentClick, onContentD @@ -47,7 +50,7 @@ const MemoBody: React.FC = ({ compact, onContentClick, onContentD
- {nsfw && !showNSFWContent && } + {nsfw && !showNSFWContent && } ); }; diff --git a/web/src/components/MemoView/components/MemoHeader.tsx b/web/src/components/MemoView/components/MemoHeader.tsx index 375e578d4..c91e308df 100644 --- a/web/src/components/MemoView/components/MemoHeader.tsx +++ b/web/src/components/MemoView/components/MemoHeader.tsx @@ -1,8 +1,9 @@ import { timestampDate } from "@bufbuild/protobuf/wkt"; import { BookmarkIcon } from "lucide-react"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { Link } from "react-router-dom"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import useNavigateTo from "@/hooks/useNavigateTo"; import i18n from "@/i18n"; import { cn } from "@/lib/utils"; import { Visibility } from "@/types/proto/api/v1/memo_service_pb"; @@ -13,16 +14,24 @@ import MemoActionMenu from "../../MemoActionMenu"; import { ReactionSelector } from "../../MemoReactionListView"; import UserAvatar from "../../UserAvatar"; import VisibilityIcon from "../../VisibilityIcon"; +import { useMemoActions } from "../hooks"; import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext"; import type { MemoHeaderProps } from "../types"; -const MemoHeader: React.FC = ({ showCreator, showVisibility, showPinned, onEdit, onGotoDetail, onUnpin }) => { +const MemoHeader: React.FC = ({ showCreator, showVisibility, showPinned }) => { const t = useTranslate(); const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false); - const { memo, creator, currentUser, isArchived, readonly } = useMemoViewContext(); + const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext(); const { relativeTimeFormat } = useMemoViewDerived(); + const navigateTo = useNavigateTo(); + const handleGotoMemoDetailPage = useCallback(() => { + navigateTo(`/${memo.name}`, { state: { from: parentPage } }); + }, [memo.name, parentPage, navigateTo]); + + const { unpinMemo } = useMemoActions(memo); + const displayTime = isArchived ? ( (memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language) ) : ( @@ -37,9 +46,9 @@ const MemoHeader: React.FC = ({ showCreator, showVisibility, sh
{showCreator && creator ? ( - + ) : ( - + )}
@@ -70,7 +79,7 @@ const MemoHeader: React.FC = ({ showCreator, showVisibility, sh - + @@ -80,7 +89,7 @@ const MemoHeader: React.FC = ({ showCreator, showVisibility, sh )} - +
); diff --git a/web/src/components/MemoView/components/metadata/AttachmentList.tsx b/web/src/components/MemoView/components/metadata/AttachmentList.tsx index 4f48224ff..c3305e568 100644 --- a/web/src/components/MemoView/components/metadata/AttachmentList.tsx +++ b/web/src/components/MemoView/components/metadata/AttachmentList.tsx @@ -1,9 +1,9 @@ import { FileAudioIcon, FileIcon, PaperclipIcon } from "lucide-react"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; import { formatFileSize, getFileTypeLabel } from "@/utils/format"; -import PreviewImageDialog from "../../../PreviewImageDialog"; +import { useMemoViewContext } from "../../MemoViewContext"; import AttachmentCard from "./AttachmentCard"; import SectionHeader from "./SectionHeader"; @@ -128,12 +128,7 @@ const DocsList = ({ attachments }: { attachments: Attachment[] }) => ( const Divider = () =>
; const AttachmentList = ({ attachments }: AttachmentListProps) => { - const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number; mimeType?: string }>({ - open: false, - urls: [], - index: 0, - mimeType: undefined, - }); + const { openPreview } = useMemoViewContext(); const { visual, audio, docs } = useMemo(() => separateAttachments(attachments), [attachments]); @@ -146,38 +141,28 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => { const handleImageClick = (imgUrl: string) => { const index = imageUrls.findIndex((url) => url === imgUrl); - const mimeType = imageAttachments[index]?.type; - setPreviewImage({ open: true, urls: imageUrls, index, mimeType }); + openPreview(imageUrls, index >= 0 ? index : 0); }; const sections = [visual.length > 0, audio.length > 0, docs.length > 0]; const sectionCount = sections.filter(Boolean).length; return ( - <> -
- +
+ -
- {visual.length > 0 && } +
+ {visual.length > 0 && } - {visual.length > 0 && sectionCount > 1 && } + {visual.length > 0 && sectionCount > 1 && } - {audio.length > 0 && } + {audio.length > 0 && } - {audio.length > 0 && docs.length > 0 && } + {audio.length > 0 && docs.length > 0 && } - {docs.length > 0 && } -
+ {docs.length > 0 && }
- - setPreviewImage((prev) => ({ ...prev, open }))} - imgUrls={previewImage.urls} - initialIndex={previewImage.index} - /> - +
); }; diff --git a/web/src/components/MemoView/hooks/useImagePreview.ts b/web/src/components/MemoView/hooks/useImagePreview.ts index 51755d003..7a0e975ae 100644 --- a/web/src/components/MemoView/hooks/useImagePreview.ts +++ b/web/src/components/MemoView/hooks/useImagePreview.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; export interface ImagePreviewState { open: boolean; @@ -8,16 +8,20 @@ export interface ImagePreviewState { export interface UseImagePreviewReturn { previewState: ImagePreviewState; - openPreview: (url: string) => void; + openPreview: (urls: string | string[], index?: number) => void; setPreviewOpen: (open: boolean) => void; } export const useImagePreview = (): UseImagePreviewReturn => { const [previewState, setPreviewState] = useState({ open: false, urls: [], index: 0 }); - return { - previewState, - openPreview: (url: string) => setPreviewState({ open: true, urls: [url], index: 0 }), - setPreviewOpen: (open: boolean) => setPreviewState((prev) => ({ ...prev, open })), - }; + const openPreview = useCallback((urls: string | string[], index = 0) => { + setPreviewState({ open: true, urls: Array.isArray(urls) ? urls : [urls], index }); + }, []); + + const setPreviewOpen = useCallback((open: boolean) => { + setPreviewState((prev) => ({ ...prev, open })); + }, []); + + return { previewState, openPreview, setPreviewOpen }; }; diff --git a/web/src/components/MemoView/hooks/useMemoHandlers.ts b/web/src/components/MemoView/hooks/useMemoHandlers.ts index 661caa65e..a57516124 100644 --- a/web/src/components/MemoView/hooks/useMemoHandlers.ts +++ b/web/src/components/MemoView/hooks/useMemoHandlers.ts @@ -1,24 +1,16 @@ import { useCallback } from "react"; import { useInstance } from "@/contexts/InstanceContext"; -import useNavigateTo from "@/hooks/useNavigateTo"; interface UseMemoHandlersOptions { - memoName: string; - parentPage: string; readonly: boolean; openEditor: () => void; - openPreview: (url: string) => void; + openPreview: (urls: string | string[], index?: number) => void; } export const useMemoHandlers = (options: UseMemoHandlersOptions) => { - const { memoName, parentPage, readonly, openEditor, openPreview } = options; - const navigateTo = useNavigateTo(); + const { readonly, openEditor, openPreview } = options; const { memoRelatedSetting } = useInstance(); - const handleGotoMemoDetailPage = useCallback(() => { - navigateTo(`/${memoName}`, { state: { from: parentPage } }); - }, [memoName, parentPage, navigateTo]); - const handleMemoContentClick = useCallback( (e: React.MouseEvent) => { const targetEl = e.target as HTMLElement; @@ -43,5 +35,5 @@ export const useMemoHandlers = (options: UseMemoHandlersOptions) => { [readonly, openEditor, memoRelatedSetting.enableDoubleClickEdit], ); - return { handleGotoMemoDetailPage, handleMemoContentClick, handleMemoContentDoubleClick }; + return { handleMemoContentClick, handleMemoContentDoubleClick }; }; diff --git a/web/src/components/MemoView/types.ts b/web/src/components/MemoView/types.ts index 45e505dc7..0025e12fb 100644 --- a/web/src/components/MemoView/types.ts +++ b/web/src/components/MemoView/types.ts @@ -14,14 +14,8 @@ export interface MemoHeaderProps { showCreator?: boolean; showVisibility?: boolean; showPinned?: boolean; - onEdit: () => void; - onGotoDetail: () => void; - onUnpin: () => void; } export interface MemoBodyProps { compact?: boolean; - onContentClick: (e: React.MouseEvent) => void; - onContentDoubleClick: (e: React.MouseEvent) => void; - onToggleNsfwVisibility: () => void; } diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 4ed48d364..6b761ab3d 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -47,12 +47,7 @@ const MemoDetail = () => { return; } - if (error.code === Code.PermissionDenied) { - navigateTo("/403", { replace: true }); - return; - } - - if (error.code === Code.NotFound) { + if (error.code === Code.PermissionDenied || error.code === Code.NotFound) { navigateTo("/404", { replace: true }); return; }