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
d2acebcc
Commit
d2acebcc
authored
Jan 02, 2026
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: auth checks in reaction selector
parent
ef8e3cfb
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
24 additions
and
73 deletions
+24
-73
MemoView.tsx
web/src/components/MemoView/MemoView.tsx
+14
-7
MemoViewContext.tsx
web/src/components/MemoView/MemoViewContext.tsx
+4
-7
MemoHeader.tsx
web/src/components/MemoView/components/MemoHeader.tsx
+6
-14
index.ts
web/src/components/MemoView/hooks/index.ts
+0
-1
useMemoViewDerivedState.ts
web/src/components/MemoView/hooks/useMemoViewDerivedState.ts
+0
-16
types.ts
web/src/components/MemoView/types.ts
+0
-28
No files found.
web/src/components/MemoView/MemoView.tsx
View file @
d2acebcc
import
{
memo
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useUser
}
from
"@/hooks/useUserQueries"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
State
}
from
"@/types/proto/api/v1/common_pb"
;
import
{
isSuperUser
}
from
"@/utils/user"
;
import
MemoEditor
from
"../MemoEditor"
;
import
PreviewImageDialog
from
"../PreviewImageDialog"
;
import
{
MemoBody
,
MemoHeader
}
from
"./components"
;
import
{
MEMO_CARD_BASE_CLASSES
}
from
"./constants"
;
import
{
useImagePreview
,
useMemoActions
,
useMemoHandlers
,
use
MemoViewDerivedState
,
use
NsfwContent
}
from
"./hooks"
;
import
{
useImagePreview
,
useMemoActions
,
useMemoHandlers
,
useNsfwContent
}
from
"./hooks"
;
import
{
MemoViewContext
}
from
"./MemoViewContext"
;
import
type
{
MemoViewProps
}
from
"./types"
;
const
MemoView
:
React
.
FC
<
MemoViewProps
>
=
(
props
:
MemoViewProps
)
=>
{
const
{
memo
:
memoData
,
className
}
=
props
;
const
{
memo
:
memoData
,
className
,
parentPage
:
parentPageProp
}
=
props
;
const
cardRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
[
reactionSelectorOpen
,
setReactionSelectorOpen
]
=
useState
(
false
);
const
[
showEditor
,
setShowEditor
]
=
useState
(
false
);
const
currentUser
=
useCurrentUser
();
const
creator
=
useUser
(
memoData
.
creator
).
data
;
const
{
isArchived
,
readonly
,
parentPage
}
=
useMemoViewDerivedState
(
memoData
,
props
.
parentPage
);
const
isArchived
=
memoData
.
state
===
State
.
ARCHIVED
;
const
readonly
=
memoData
.
creator
!==
currentUser
?.
name
&&
!
isSuperUser
(
currentUser
);
const
parentPage
=
parentPageProp
||
"/"
;
const
{
nsfw
,
showNSFWContent
,
toggleNsfwVisibility
}
=
useNsfwContent
(
memoData
,
props
.
showNsfwContent
);
const
{
previewState
,
openPreview
,
setPreviewOpen
}
=
useImagePreview
();
const
{
unpinMemo
}
=
useMemoActions
(
memoData
,
isArchived
);
...
...
@@ -37,11 +43,14 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
()
=>
({
memo
:
memoData
,
creator
,
currentUser
,
parentPage
,
isArchived
,
readonly
,
showNSFWContent
,
nsfw
,
}),
[
memoData
,
creator
,
parentPage
,
showNSFWContent
,
nsfw
],
[
memoData
,
creator
,
currentUser
,
parentPage
,
isArchived
,
readonly
,
showNSFWContent
,
nsfw
],
);
if
(
showEditor
)
{
...
...
@@ -68,8 +77,6 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
onGotoDetail=
{
handleGotoMemoDetailPage
}
onUnpin=
{
unpinMemo
}
onToggleNsfwVisibility=
{
toggleNsfwVisibility
}
reactionSelectorOpen=
{
reactionSelectorOpen
}
onReactionSelectorOpenChange=
{
setReactionSelectorOpen
}
/>
<
MemoBody
...
...
web/src/components/MemoView/MemoViewContext.tsx
View file @
d2acebcc
import
{
timestampDate
}
from
"@bufbuild/protobuf/wkt"
;
import
{
createContext
,
useContext
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
State
}
from
"@/types/proto/api/v1/common_pb"
;
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service_pb"
;
import
{
isSuperUser
}
from
"@/utils/user"
;
import
{
RELATIVE_TIME_THRESHOLD_MS
}
from
"./constants"
;
export
interface
MemoViewContextValue
{
memo
:
Memo
;
creator
:
User
|
undefined
;
currentUser
:
User
|
undefined
;
parentPage
:
string
;
isArchived
:
boolean
;
readonly
:
boolean
;
showNSFWContent
:
boolean
;
nsfw
:
boolean
;
}
...
...
@@ -28,12 +28,9 @@ export const useMemoViewContext = (): MemoViewContextValue => {
};
export
const
useMemoViewDerived
=
()
=>
{
const
{
memo
}
=
useMemoViewContext
();
const
{
memo
,
isArchived
,
readonly
}
=
useMemoViewContext
();
const
location
=
useLocation
();
const
currentUser
=
useCurrentUser
();
const
isArchived
=
memo
.
state
===
State
.
ARCHIVED
;
const
readonly
=
memo
.
creator
!==
currentUser
?.
name
&&
!
isSuperUser
(
currentUser
);
const
isInMemoDetailPage
=
location
.
pathname
.
startsWith
(
`/
${
memo
.
name
}
`
);
const
commentAmount
=
memo
.
relations
.
filter
(
...
...
web/src/components/MemoView/components/MemoHeader.tsx
View file @
d2acebcc
import
{
timestampDate
}
from
"@bufbuild/protobuf/wkt"
;
import
{
BookmarkIcon
,
EyeOffIcon
,
MessageCircleMoreIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
i18n
from
"@/i18n"
;
...
...
@@ -23,13 +24,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
onGotoDetail
,
onUnpin
,
onToggleNsfwVisibility
,
reactionSelectorOpen
,
onReactionSelectorOpenChange
,
})
=>
{
const
t
=
useTranslate
();
const
[
reactionSelectorOpen
,
setReactionSelectorOpen
]
=
useState
(
false
);
const
{
memo
,
creator
,
parentPage
,
showNSFWContent
,
nsfw
}
=
useMemoViewContext
();
const
{
is
Archived
,
readonly
,
is
InMemoDetailPage
,
commentAmount
,
relativeTimeFormat
}
=
useMemoViewDerived
();
const
{
memo
,
creator
,
currentUser
,
parentPage
,
isArchived
,
readonly
,
showNSFWContent
,
nsfw
}
=
useMemoViewContext
();
const
{
isInMemoDetailPage
,
commentAmount
,
relativeTimeFormat
}
=
useMemoViewDerived
();
const
displayTime
=
isArchived
?
(
(
memo
.
displayTime
?
timestampDate
(
memo
.
displayTime
)
:
undefined
)?.
toLocaleString
(
i18n
.
language
)
...
...
@@ -43,7 +43,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
return
(
<
div
className=
"w-full flex flex-row justify-between items-center gap-2"
>
{
/* Left section: Creator info or time */
}
<
div
className=
"w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center"
>
{
showCreator
&&
creator
?
(
<
CreatorDisplay
creator=
{
creator
}
displayTime=
{
displayTime
}
onGotoDetail=
{
onGotoDetail
}
/>
...
...
@@ -52,18 +51,15 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
)
}
</
div
>
{
/* Right section: Actions */
}
<
div
className=
"flex flex-row justify-end items-center select-none shrink-0 gap-2"
>
{
/* Reaction selector */
}
{
!
isArchived
&&
(
{
currentUser
&&
!
isArchived
&&
(
<
ReactionSelector
className=
{
cn
(
"border-none w-auto h-auto"
,
reactionSelectorOpen
&&
"block!"
,
"hidden group-hover:block"
)
}
memo=
{
memo
}
onOpenChange=
{
onReactionSelectorOpenChange
}
onOpenChange=
{
setReactionSelectorOpen
}
/>
)
}
{
/* Comment count link */
}
{
!
isInMemoDetailPage
&&
(
<
Link
className=
{
cn
(
...
...
@@ -79,7 +75,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</
Link
>
)
}
{
/* Visibility icon */
}
{
showVisibility
&&
memo
.
visibility
!==
Visibility
.
PRIVATE
&&
(
<
Tooltip
>
<
TooltipTrigger
>
...
...
@@ -93,7 +88,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</
Tooltip
>
)
}
{
/* Pinned indicator */
}
{
showPinned
&&
memo
.
pinned
&&
(
<
TooltipProvider
>
<
Tooltip
>
...
...
@@ -109,14 +103,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</
TooltipProvider
>
)
}
{
/* NSFW hide button */
}
{
nsfw
&&
showNSFWContent
&&
onToggleNsfwVisibility
&&
(
<
span
className=
"cursor-pointer"
>
<
EyeOffIcon
className=
"w-4 h-auto text-primary"
onClick=
{
onToggleNsfwVisibility
}
/>
</
span
>
)
}
{
/* Action menu */
}
<
MemoActionMenu
memo=
{
memo
}
readonly=
{
readonly
}
onEdit=
{
onEdit
}
/>
</
div
>
</
div
>
...
...
web/src/components/MemoView/hooks/index.ts
View file @
d2acebcc
export
{
useImagePreview
}
from
"./useImagePreview"
;
export
{
useMemoActions
}
from
"./useMemoActions"
;
export
{
useMemoHandlers
}
from
"./useMemoHandlers"
;
export
{
useMemoViewDerivedState
}
from
"./useMemoViewDerivedState"
;
export
{
useNsfwContent
}
from
"./useNsfwContent"
;
web/src/components/MemoView/hooks/useMemoViewDerivedState.ts
deleted
100644 → 0
View file @
ef8e3cfb
import
{
useLocation
}
from
"react-router-dom"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
State
}
from
"@/types/proto/api/v1/common_pb"
;
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
isSuperUser
}
from
"@/utils/user"
;
export
const
useMemoViewDerivedState
=
(
memo
:
Memo
,
parentPageProp
?:
string
)
=>
{
const
location
=
useLocation
();
const
user
=
useCurrentUser
();
const
isArchived
=
memo
.
state
===
State
.
ARCHIVED
;
const
readonly
=
memo
.
creator
!==
user
?.
name
&&
!
isSuperUser
(
user
);
const
parentPage
=
parentPageProp
||
location
.
pathname
;
return
{
isArchived
,
readonly
,
parentPage
};
};
web/src/components/MemoView/types.ts
View file @
d2acebcc
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service_pb"
;
/**
* Props for the MemoView component.
* MemoView is the main component for displaying a memo card with all its metadata,
* content, and interactive elements.
*/
export
interface
MemoViewProps
{
/** The memo object to display */
memo
:
Memo
;
/** Whether to show compact view (hides some metadata) */
compact
?:
boolean
;
/** Whether to show the creator's profile information */
showCreator
?:
boolean
;
/** Whether to show the visibility indicator */
showVisibility
?:
boolean
;
/** Whether to show the pinned indicator */
showPinned
?:
boolean
;
/** Whether to show NSFW content by default */
showNsfwContent
?:
boolean
;
/** Additional CSS classes to apply to the root element */
className
?:
string
;
/** The parent page URL for navigation context */
parentPage
?:
string
;
}
/**
* Props for the MemoHeader component.
* Displays memo metadata like creator, timestamp, and action buttons.
*/
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.
* Displays memo content, attachments, and relations.
*/
export
interface
MemoBodyProps
{
// Display options
compact
?:
boolean
;
// Callbacks
onContentClick
:
(
e
:
React
.
MouseEvent
)
=>
void
;
onContentDoubleClick
:
(
e
:
React
.
MouseEvent
)
=>
void
;
onToggleNsfwVisibility
:
()
=>
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