Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
6dcf7cc7
Commit
6dcf7cc7
authored
Nov 30, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: enhance MemoView component structure
parent
7aa8262e
Changes
12
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
117 additions
and
477 deletions
+117
-477
MemoView.tsx
web/src/components/MemoView/MemoView.tsx
+19
-39
MemoViewContext.tsx
web/src/components/MemoView/MemoViewContext.tsx
+0
-22
MemoBody.tsx
web/src/components/MemoView/components/MemoBody.tsx
+8
-11
MemoHeader.tsx
web/src/components/MemoView/components/MemoHeader.tsx
+14
-20
constants.ts
web/src/components/MemoView/constants.ts
+1
-12
index.ts
web/src/components/MemoView/hooks/index.ts
+0
-3
useMemoEditor.ts
web/src/components/MemoView/hooks/useMemoEditor.ts
+8
-29
useMemoHandlers.ts
web/src/components/MemoView/hooks/useMemoHandlers.ts
+9
-35
useMemoViewDerivedState.ts
web/src/components/MemoView/hooks/useMemoViewDerivedState.ts
+11
-46
useMemoViewState.ts
web/src/components/MemoView/hooks/useMemoViewState.ts
+47
-127
index.ts
web/src/components/MemoView/index.ts
+0
-29
types.ts
web/src/components/MemoView/types.ts
+0
-104
No files found.
web/src/components/MemoView/MemoView.tsx
View file @
6dcf7cc7
import
{
observer
}
from
"mobx-react-lite"
;
import
{
memo
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
MemoEditor
from
"../MemoEditor"
;
import
PreviewImageDialog
from
"../PreviewImageDialog"
;
import
{
MemoBody
,
MemoHeader
}
from
"./components"
;
...
...
@@ -16,40 +17,31 @@ import {
useNsfwContent
,
}
from
"./hooks"
;
import
{
MemoViewContext
}
from
"./MemoViewContext"
;
import
type
{
MemoViewProps
}
from
"./types"
;
/**
* MemoView component displays a single memo card with full functionality including:
* - Creator information and display time
* - Memo content with markdown rendering
* - Attachments and location
* - Reactions and comments
* - Edit mode with inline editor
* - Keyboard shortcuts for quick actions
* - NSFW content blur protection
*/
const
MemoView
:
React
.
FC
<
MemoViewProps
>
=
observer
((
props
:
MemoViewProps
)
=>
{
interface
Props
{
memo
:
Memo
;
compact
?:
boolean
;
showCreator
?:
boolean
;
showVisibility
?:
boolean
;
showPinned
?:
boolean
;
showNsfwContent
?:
boolean
;
className
?:
string
;
parentPage
?:
string
;
}
const
MemoView
:
React
.
FC
<
Props
>
=
observer
((
props
:
Props
)
=>
{
const
{
memo
:
memoData
,
className
}
=
props
;
const
cardRef
=
useRef
<
HTMLDivElement
>
(
null
);
// State
const
[
reactionSelectorOpen
,
setReactionSelectorOpen
]
=
useState
(
false
);
// Custom hooks for data fetching
const
creator
=
useMemoCreator
(
memoData
.
creator
);
// Custom hooks for derived state
const
{
commentAmount
,
relativeTimeFormat
,
isArchived
,
readonly
,
isInMemoDetailPage
,
parentPage
}
=
useMemoViewDerivedState
({
memo
:
memoData
,
parentPage
:
props
.
parentPage
,
});
// Custom hooks for UI state management
const
{
commentAmount
,
relativeTimeFormat
,
isArchived
,
readonly
,
isInMemoDetailPage
,
parentPage
}
=
useMemoViewDerivedState
(
memoData
,
props
.
parentPage
,
);
const
{
nsfw
,
showNSFWContent
,
toggleNsfwVisibility
}
=
useNsfwContent
(
memoData
,
props
.
showNsfwContent
);
const
{
previewState
,
openPreview
,
setPreviewOpen
}
=
useImagePreview
();
const
{
showEditor
,
openEditor
,
handleEditorConfirm
,
handleEditorCancel
}
=
useMemoEditor
();
// Custom hooks for actions
const
{
archiveMemo
,
unpinMemo
}
=
useMemoActions
(
memoData
);
const
{
handleGotoMemoDetailPage
,
handleMemoContentClick
,
handleMemoContentDoubleClick
}
=
useMemoHandlers
({
memoName
:
memoData
.
name
,
...
...
@@ -58,9 +50,7 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
openEditor
,
openPreview
,
});
// Keyboard shortcuts
const
{
handleShortcutActivation
}
=
useKeyboardShortcuts
(
cardRef
,
{
useKeyboardShortcuts
(
cardRef
,
{
enabled
:
true
,
readonly
,
showEditor
,
...
...
@@ -69,8 +59,6 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
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
(
()
=>
({
memo
:
memoData
,
...
...
@@ -87,7 +75,6 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
[
memoData
,
creator
,
isArchived
,
readonly
,
isInMemoDetailPage
,
parentPage
,
commentAmount
,
relativeTimeFormat
,
nsfw
,
showNSFWContent
],
);
// Render inline editor when editing
if
(
showEditor
)
{
return
(
<
MemoEditor
...
...
@@ -101,16 +88,9 @@ const MemoView: React.FC<MemoViewProps> = observer((props: MemoViewProps) => {
);
}
// Render memo card
return
(
<
MemoViewContext
.
Provider
value=
{
contextValue
}
>
<
article
className=
{
cn
(
MEMO_CARD_BASE_CLASSES
,
className
)
}
ref=
{
cardRef
}
tabIndex=
{
readonly
?
-
1
:
0
}
onFocus=
{
()
=>
handleShortcutActivation
(
true
)
}
onBlur=
{
()
=>
handleShortcutActivation
(
false
)
}
>
<
article
className=
{
cn
(
MEMO_CARD_BASE_CLASSES
,
className
)
}
ref=
{
cardRef
}
tabIndex=
{
readonly
?
-
1
:
0
}
>
<
MemoHeader
showCreator=
{
props
.
showCreator
}
showVisibility=
{
props
.
showVisibility
}
...
...
web/src/components/MemoView/MemoViewContext.tsx
View file @
6dcf7cc7
...
...
@@ -2,43 +2,21 @@ import { createContext, useContext } from "react";
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_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
{
/** The memo data */
memo
:
Memo
;
/** The memo creator user data */
creator
:
User
|
undefined
;
/** Whether the memo is in archived state */
isArchived
:
boolean
;
/** Whether the current user can only view (not edit) the memo */
readonly
:
boolean
;
/** Whether we're currently on the memo detail page */
isInMemoDetailPage
:
boolean
;
/** Parent page path for navigation state */
parentPage
:
string
;
/** Number of comments on this memo */
commentAmount
:
number
;
/** Time format to use (datetime for old memos, auto for recent) */
relativeTimeFormat
:
"datetime"
|
"auto"
;
/** Whether this memo contains NSFW content */
nsfw
:
boolean
;
/** Whether to show NSFW content without blur */
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
);
/**
* Hook to access MemoView context
* @throws Error if used outside of MemoViewContext.Provider
*/
export
const
useMemoViewContext
=
():
MemoViewContextValue
=>
{
const
context
=
useContext
(
MemoViewContext
);
if
(
!
context
)
{
...
...
web/src/components/MemoView/components/MemoBody.tsx
View file @
6dcf7cc7
...
...
@@ -5,18 +5,15 @@ import MemoContent from "../../MemoContent";
import
{
MemoReactionListView
}
from
"../../MemoReactionListView"
;
import
{
AttachmentList
,
LocationDisplay
,
RelationList
}
from
"../../memo-metadata"
;
import
{
useMemoViewContext
}
from
"../MemoViewContext"
;
import
type
{
MemoBodyProps
}
from
"../types"
;
/**
* MemoBody component displays the main content of a memo including:
* - Memo content (markdown)
* - Location display
* - Attachments
* - Related memos
* - Reactions
* - NSFW content overlay
*/
const
MemoBody
:
React
.
FC
<
MemoBodyProps
>
=
({
compact
,
onContentClick
,
onContentDoubleClick
,
onToggleNsfwVisibility
})
=>
{
interface
Props
{
compact
?:
boolean
;
onContentClick
:
(
e
:
React
.
MouseEvent
)
=>
void
;
onContentDoubleClick
:
(
e
:
React
.
MouseEvent
)
=>
void
;
onToggleNsfwVisibility
:
()
=>
void
;
}
const
MemoBody
:
React
.
FC
<
Props
>
=
({
compact
,
onContentClick
,
onContentDoubleClick
,
onToggleNsfwVisibility
})
=>
{
const
t
=
useTranslate
();
// Get shared state from context
...
...
web/src/components/MemoView/components/MemoHeader.tsx
View file @
6dcf7cc7
...
...
@@ -12,20 +12,20 @@ import { ReactionSelector } from "../../reactions";
import
UserAvatar
from
"../../UserAvatar"
;
import
VisibilityIcon
from
"../../VisibilityIcon"
;
import
{
useMemoViewContext
}
from
"../MemoViewContext"
;
import
type
{
MemoHeaderProps
}
from
"../types"
;
/**
* MemoHeader component displays the top section of a memo card including:
* - Creator info (avatar, name) when showCreator is true
* - Display time (relative or absolute)
* - Reaction selector
* - Comment count link
* - Visibility icon
* - Pin indicator
* - NSFW hide button
* - Action menu
*/
const
MemoHeader
:
React
.
FC
<
MemoHeader
Props
>
=
({
interface
Props
{
showCreator
?:
boolean
;
showVisibility
?:
boolean
;
showPinned
?:
boolean
;
onEdit
:
()
=>
void
;
onGotoDetail
:
()
=>
void
;
onUnpin
:
()
=>
void
;
onToggleNsfwVisibility
?:
()
=>
void
;
reactionSelectorOpen
:
boolean
;
onReactionSelectorOpenChange
:
(
open
:
boolean
)
=>
void
;
}
const
MemoHeader
:
React
.
FC
<
Props
>
=
({
showCreator
,
showVisibility
,
showPinned
,
...
...
@@ -130,9 +130,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
);
};
/**
* Creator display with avatar and name
*/
interface
CreatorDisplayProps
{
creator
:
User
;
displayTime
:
React
.
ReactNode
;
...
...
@@ -163,9 +160,6 @@ const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, o
</
div
>
);
/**
* Simple time display without creator info
*/
interface
TimeDisplayProps
{
displayTime
:
React
.
ReactNode
;
onGotoDetail
:
()
=>
void
;
...
...
web/src/components/MemoView/constants.ts
View file @
6dcf7cc7
/**
* Constants for MemoView component
*/
/** CSS class for memo card styling */
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"
;
/** 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
;
/** Time threshold for relative time format (24 hours in milliseconds) */
export
const
RELATIVE_TIME_THRESHOLD_MS
=
1000
*
60
*
60
*
24
;
web/src/components/MemoView/hooks/index.ts
View file @
6dcf7cc7
export
type
{
UseMemoEditorReturn
}
from
"./useMemoEditor"
;
export
{
useMemoEditor
}
from
"./useMemoEditor"
;
export
type
{
UseMemoHandlersOptions
,
UseMemoHandlersReturn
}
from
"./useMemoHandlers"
;
export
{
useMemoHandlers
}
from
"./useMemoHandlers"
;
export
type
{
UseMemoViewDerivedStateOptions
,
UseMemoViewDerivedStateReturn
}
from
"./useMemoViewDerivedState"
;
export
{
useMemoViewDerivedState
}
from
"./useMemoViewDerivedState"
;
export
{
useImagePreview
,
useKeyboardShortcuts
,
useMemoActions
,
useMemoCreator
,
useNsfwContent
}
from
"./useMemoViewState"
;
web/src/components/MemoView/hooks/useMemoEditor.ts
View file @
6dcf7cc7
import
{
use
Callback
,
use
State
}
from
"react"
;
import
{
useState
}
from
"react"
;
import
{
userStore
}
from
"@/store"
;
export
interface
UseMemoEditorReturn
{
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
=>
{
export
const
useMemoEditor
=
()
=>
{
const
[
showEditor
,
setShowEditor
]
=
useState
(
false
);
const
openEditor
=
useCallback
(()
=>
{
setShowEditor
(
true
);
},
[]);
const
handleEditorConfirm
=
useCallback
(()
=>
{
setShowEditor
(
false
);
userStore
.
setStatsStateId
();
},
[]);
const
handleEditorCancel
=
useCallback
(()
=>
{
setShowEditor
(
false
);
},
[]);
return
{
showEditor
,
openEditor
,
handleEditorConfirm
,
handleEditorCancel
,
openEditor
:
()
=>
setShowEditor
(
true
),
handleEditorConfirm
:
()
=>
{
setShowEditor
(
false
);
userStore
.
setStatsStateId
();
},
handleEditorCancel
:
()
=>
setShowEditor
(
false
),
};
};
web/src/components/MemoView/hooks/useMemoHandlers.ts
View file @
6dcf7cc7
import
{
useCallback
}
from
"react"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
instanceStore
}
from
"@/store"
;
import
type
{
UseImagePreviewReturn
}
from
"../types"
;
export
interface
UseMemoHandlersOptions
{
interface
UseMemoHandlersOptions
{
memoName
:
string
;
parentPage
:
string
;
readonly
:
boolean
;
openEditor
:
()
=>
void
;
openPreview
:
UseImagePreviewReturn
[
"openPreview"
]
;
openPreview
:
(
url
:
string
)
=>
void
;
}
export
interface
UseMemoHandlersReturn
{
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
=>
{
export
const
useMemoHandlers
=
(
options
:
UseMemoHandlersOptions
)
=>
{
const
{
memoName
,
parentPage
,
readonly
,
openEditor
,
openPreview
}
=
options
;
const
navigateTo
=
useNavigateTo
();
// These useCallbacks are necessary since they have real dependencies
const
handleGotoMemoDetailPage
=
useCallback
(()
=>
{
navigateTo
(
`/
${
memoName
}
`
,
{
state
:
{
from
:
parentPage
},
});
navigateTo
(
`/
${
memoName
}
`
,
{
state
:
{
from
:
parentPage
}
});
},
[
memoName
,
parentPage
,
navigateTo
]);
const
handleMemoContentClick
=
useCallback
(
(
e
:
React
.
MouseEvent
)
=>
{
const
targetEl
=
e
.
target
as
HTMLElement
;
if
(
targetEl
.
tagName
===
"IMG"
)
{
// Check if the image is inside a link
const
linkElement
=
targetEl
.
closest
(
"a"
);
if
(
linkElement
)
{
// If image is inside a link, only navigate to the link (don't show preview)
return
;
}
if
(
linkElement
)
return
;
// If image is inside a link, don't show preview
const
imgUrl
=
targetEl
.
getAttribute
(
"src"
);
if
(
imgUrl
)
{
openPreview
(
imgUrl
);
}
if
(
imgUrl
)
openPreview
(
imgUrl
);
}
},
[
openPreview
],
...
...
@@ -55,9 +35,7 @@ export const useMemoHandlers = (options: UseMemoHandlersOptions): UseMemoHandler
const
handleMemoContentDoubleClick
=
useCallback
(
(
e
:
React
.
MouseEvent
)
=>
{
if
(
readonly
)
return
;
const
instanceMemoRelatedSetting
=
instanceStore
.
state
.
memoRelatedSetting
;
if
(
instanceMemoRelatedSetting
.
enableDoubleClickEdit
)
{
if
(
instanceStore
.
state
.
memoRelatedSetting
.
enableDoubleClickEdit
)
{
e
.
preventDefault
();
openEditor
();
}
...
...
@@ -65,9 +43,5 @@ export const useMemoHandlers = (options: UseMemoHandlersOptions): UseMemoHandler
[
readonly
,
openEditor
],
);
return
{
handleGotoMemoDetailPage
,
handleMemoContentClick
,
handleMemoContentDoubleClick
,
};
return
{
handleGotoMemoDetailPage
,
handleMemoContentClick
,
handleMemoContentDoubleClick
};
};
web/src/components/MemoView/hooks/useMemoViewDerivedState.ts
View file @
6dcf7cc7
import
{
useMemo
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
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
{
RELATIVE_TIME_THRESHOLD_MS
}
from
"../constants"
;
export
interface
UseMemoViewDerivedStateOptions
{
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
;
export
const
useMemoViewDerivedState
=
(
memo
:
Memo
,
parentPageProp
?:
string
)
=>
{
const
location
=
useLocation
();
const
user
=
useCurrentUser
();
// Compute all derived state
const
commentAmount
=
useMemo
(
()
=>
memo
.
relations
.
filter
((
relation
)
=>
relation
.
type
===
MemoRelation_Type
.
COMMENT
&&
relation
.
relatedMemo
?.
name
===
memo
.
name
).
length
,
[
memo
.
relations
,
memo
.
name
],
);
const
relativeTimeFormat
:
"datetime"
|
"auto"
=
useMemo
(
()
=>
(
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
readonly
=
useMemo
(()
=>
memo
.
creator
!==
user
?.
name
&&
!
isSuperUser
(
user
),
[
memo
.
creator
,
user
]);
const
commentAmount
=
memo
.
relations
.
filter
(
(
relation
)
=>
relation
.
type
===
MemoRelation_Type
.
COMMENT
&&
relation
.
relatedMemo
?.
name
===
memo
.
name
,
).
length
;
const
isInMemoDetailPage
=
useMemo
(()
=>
location
.
pathname
.
startsWith
(
`/
${
memo
.
name
}
`
),
[
location
.
pathname
,
memo
.
name
]);
const
relativeTimeFormat
:
"datetime"
|
"auto"
=
memo
.
displayTime
&&
Date
.
now
()
-
memo
.
displayTime
.
getTime
()
>
RELATIVE_TIME_THRESHOLD_MS
?
"datetime"
:
"auto"
;
const
parentPage
=
useMemo
(()
=>
parentPageProp
||
location
.
pathname
,
[
parentPageProp
,
location
.
pathname
]);
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
;
return
{
commentAmount
,
relativeTimeFormat
,
isArchived
,
readonly
,
isInMemoDetailPage
,
parentPage
,
};
return
{
commentAmount
,
relativeTimeFormat
,
isArchived
,
readonly
,
isInMemoDetailPage
,
parentPage
};
};
web/src/components/MemoView/hooks/useMemoViewState.ts
View file @
6dcf7cc7
import
{
use
Callback
,
use
Effect
,
useRef
,
useState
}
from
"react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
{
instanceStore
,
memoStore
,
userStore
}
from
"@/store"
;
import
{
State
}
from
"@/types/proto/api/v1/common"
;
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
KEYBOARD_SHORTCUTS
,
TEXT_INPUT_TYPES
}
from
"../constants"
;
import
type
{
ImagePreviewState
,
UseImagePreviewReturn
,
UseKeyboardShortcutsOptions
,
UseMemoActionsReturn
,
UseNsfwContentReturn
,
}
from
"../types"
;
/**
* Hook for handling memo actions (archive, unpin)
*/
export
const
useMemoActions
=
(
memo
:
Memo
):
UseMemoActionsReturn
=>
{
interface
ImagePreviewState
{
open
:
boolean
;
urls
:
string
[];
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
isArchived
=
memo
.
state
===
State
.
ARCHIVED
;
const
archiveMemo
=
useCallback
(
async
()
=>
{
if
(
isArchived
)
{
return
;
}
const
archiveMemo
=
async
()
=>
{
if
(
isArchived
)
return
;
try
{
await
memoStore
.
updateMemo
(
{
name
:
memo
.
name
,
state
:
State
.
ARCHIVED
,
},
[
"state"
],
);
await
memoStore
.
updateMemo
({
name
:
memo
.
name
,
state
:
State
.
ARCHIVED
},
[
"state"
]);
toast
.
success
(
t
(
"message.archived-successfully"
));
userStore
.
setStatsStateId
();
}
catch
(
error
:
unknown
)
{
...
...
@@ -40,66 +36,37 @@ export const useMemoActions = (memo: Memo): UseMemoActionsReturn => {
const
err
=
error
as
{
details
?:
string
};
toast
.
error
(
err
?.
details
||
"Failed to archive memo"
);
}
},
[
isArchived
,
memo
.
name
,
t
]);
const
unpinMemo
=
useCallback
(
async
()
=>
{
if
(
!
memo
.
pinned
)
{
return
;
}
};
await
memoStore
.
updateMemo
(
{
name
:
memo
.
name
,
pinned
:
false
,
},
[
"pinned"
],
);
},
[
memo
.
name
,
memo
.
pinned
]);
const
unpinMemo
=
async
()
=>
{
if
(
!
memo
.
pinned
)
return
;
await
memoStore
.
updateMemo
({
name
:
memo
.
name
,
pinned
:
false
},
[
"pinned"
]);
};
return
{
archiveMemo
,
unpinMemo
};
};
/**
* Hook for handling keyboard shortcuts on the memo card
*/
export
const
useKeyboardShortcuts
=
(
cardRef
:
React
.
RefObject
<
HTMLDivElement
|
null
>
,
options
:
UseKeyboardShortcutsOptions
,
):
{
shortcutActive
:
boolean
;
handleShortcutActivation
:
(
active
:
boolean
)
=>
void
;
}
=>
{
const
{
enabled
,
readonly
,
showEditor
,
isArchived
,
onEdit
,
onArchive
}
=
options
;
const
[
shortcutActive
,
setShortcutActive
]
=
useState
(
false
);
const
isTextInputElement
=
useCallback
((
element
:
HTMLElement
|
null
):
boolean
=>
{
const
isTextInputElement
=
(
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
;
},
[]);
};
export
const
useKeyboardShortcuts
=
(
cardRef
:
React
.
RefObject
<
HTMLDivElement
|
null
>
,
options
:
UseKeyboardShortcutsOptions
)
=>
{
const
{
enabled
,
readonly
,
showEditor
,
isArchived
,
onEdit
,
onArchive
}
=
options
;
useEffect
(()
=>
{
if
(
!
enabled
||
readonly
||
showEditor
||
!
cardRef
.
current
)
{
return
;
}
if
(
!
enabled
||
readonly
||
showEditor
||
!
cardRef
.
current
)
return
;
const
cardEl
=
cardRef
.
current
;
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
const
target
=
event
.
target
as
HTMLElement
|
null
;
if
(
!
cardEl
.
contains
(
target
)
||
isTextInputElement
(
target
))
{
return
;
}
if
(
event
.
metaKey
||
event
.
ctrlKey
||
event
.
altKey
)
{
return
;
}
if
(
!
cardEl
.
contains
(
target
)
||
isTextInputElement
(
target
))
return
;
if
(
event
.
metaKey
||
event
.
ctrlKey
||
event
.
altKey
)
return
;
const
key
=
event
.
key
.
toLowerCase
();
if
(
key
===
KEYBOARD_SHORTCUTS
.
EDIT
)
{
...
...
@@ -113,29 +80,10 @@ export const useKeyboardShortcuts = (
cardEl
.
addEventListener
(
"keydown"
,
handleKeyDown
);
return
()
=>
cardEl
.
removeEventListener
(
"keydown"
,
handleKeyDown
);
},
[
enabled
,
readonly
,
showEditor
,
isArchived
,
onEdit
,
onArchive
,
cardRef
,
isTextInputElement
]);
useEffect
(()
=>
{
if
(
showEditor
||
readonly
)
{
setShortcutActive
(
false
);
}
},
[
showEditor
,
readonly
]);
const
handleShortcutActivation
=
useCallback
(
(
active
:
boolean
)
=>
{
if
(
readonly
)
return
;
setShortcutActive
(
active
);
},
[
readonly
],
);
return
{
shortcutActive
,
handleShortcutActivation
};
},
[
enabled
,
readonly
,
showEditor
,
isArchived
,
onEdit
,
onArchive
,
cardRef
]);
};
/**
* Hook for managing NSFW content visibility
*/
export
const
useNsfwContent
=
(
memo
:
Memo
,
initialShowNsfw
?:
boolean
):
UseNsfwContentReturn
=>
{
export
const
useNsfwContent
=
(
memo
:
Memo
,
initialShowNsfw
?:
boolean
)
=>
{
const
[
showNSFWContent
,
setShowNSFWContent
]
=
useState
(
initialShowNsfw
??
false
);
const
instanceMemoRelatedSetting
=
instanceStore
.
state
.
memoRelatedSetting
;
...
...
@@ -143,50 +91,23 @@ export const useNsfwContent = (memo: Memo, initialShowNsfw?: boolean): UseNsfwCo
instanceMemoRelatedSetting
.
enableBlurNsfwContent
&&
memo
.
tags
?.
some
((
tag
)
=>
instanceMemoRelatedSetting
.
nsfwTags
.
some
((
nsfwTag
)
=>
tag
===
nsfwTag
||
tag
.
startsWith
(
`
${
nsfwTag
}
/`
)));
const
toggleNsfwVisibility
=
useCallback
(()
=>
{
setShowNSFWContent
((
prev
)
=>
!
prev
);
},
[]);
return
{
nsfw
:
nsfw
??
false
,
showNSFWContent
,
toggleNsfwVisibility
,
toggleNsfwVisibility
:
()
=>
setShowNSFWContent
((
prev
)
=>
!
prev
)
,
};
};
/**
* Hook for managing image preview dialog state
*/
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
}));
},
[]);
export
const
useImagePreview
=
()
=>
{
const
[
previewState
,
setPreviewState
]
=
useState
<
ImagePreviewState
>
({
open
:
false
,
urls
:
[],
index
:
0
});
return
{
previewState
,
openPreview
,
closePreview
,
setPreviewOpen
,
openPreview
:
(
url
:
string
)
=>
setPreviewState
({
open
:
true
,
urls
:
[
url
],
index
:
0
}),
setPreviewOpen
:
(
open
:
boolean
)
=>
setPreviewState
((
prev
)
=>
({
...
prev
,
open
})),
};
};
/**
* Hook for fetching and managing memo creator data
*/
export
const
useMemoCreator
=
(
creatorName
:
string
)
=>
{
const
[
creator
,
setCreator
]
=
useState
(
userStore
.
getUserByName
(
creatorName
));
const
fetchedRef
=
useRef
(
false
);
...
...
@@ -194,7 +115,6 @@ export const useMemoCreator = (creatorName: string) => {
useEffect
(()
=>
{
if
(
fetchedRef
.
current
)
return
;
fetchedRef
.
current
=
true
;
(
async
()
=>
{
const
user
=
await
userStore
.
getOrFetchUserByName
(
creatorName
);
setCreator
(
user
);
...
...
web/src/components/MemoView/index.ts
View file @
6dcf7cc7
...
...
@@ -10,33 +10,4 @@
export
{
MemoBody
,
MemoHeader
}
from
"./components"
;
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
type
{
MemoViewContextValue
}
from
"./MemoViewContext"
;
export
{
MemoViewContext
,
useMemoViewContext
}
from
"./MemoViewContext"
;
export
type
{
ImagePreviewState
,
MemoBodyProps
,
MemoHeaderProps
,
MemoViewProps
,
UseImagePreviewReturn
,
UseKeyboardShortcutsOptions
,
UseMemoActionsReturn
,
UseNsfwContentReturn
,
}
from
"./types"
;
web/src/components/MemoView/types.ts
deleted
100644 → 0
View file @
7aa8262e
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
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment