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
07072b75
Commit
07072b75
authored
Nov 30, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: reorganize reaction components
parent
6dcf7cc7
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
88 additions
and
172 deletions
+88
-172
MemoReactionListView.tsx
.../components/MemoReactionListView/MemoReactionListView.tsx
+9
-9
ReactionSelector.tsx
web/src/components/MemoReactionListView/ReactionSelector.tsx
+13
-13
ReactionView.tsx
web/src/components/MemoReactionListView/ReactionView.tsx
+9
-6
hooks.ts
web/src/components/MemoReactionListView/hooks.ts
+54
-9
index.ts
web/src/components/MemoReactionListView/index.ts
+2
-2
types.ts
web/src/components/MemoReactionListView/types.ts
+0
-17
MemoHeader.tsx
web/src/components/MemoView/components/MemoHeader.tsx
+1
-1
hooks.ts
web/src/components/reactions/hooks.ts
+0
-77
index.ts
web/src/components/reactions/index.ts
+0
-12
types.ts
web/src/components/reactions/types.ts
+0
-26
No files found.
web/src/components/MemoReactionListView/MemoReactionListView.tsx
View file @
07072b75
...
@@ -2,17 +2,17 @@ import { observer } from "mobx-react-lite";
...
@@ -2,17 +2,17 @@ import { observer } from "mobx-react-lite";
import
{
memo
}
from
"react"
;
import
{
memo
}
from
"react"
;
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"
;
import
{
ReactionSelector
,
ReactionView
}
from
"../reactions
"
;
import
type
{
Memo
,
Reaction
}
from
"@/types/proto/api/v1/memo_service
"
;
import
{
useReactionGroups
}
from
"./hooks"
;
import
{
useReactionGroups
}
from
"./hooks"
;
import
type
{
MemoReactionListViewProps
}
from
"./types"
;
import
ReactionSelector
from
"./ReactionSelector"
;
import
ReactionView
from
"./ReactionView"
;
/**
interface
Props
{
* MemoReactionListView displays the reactions on a memo:
memo
:
Memo
;
* - Groups reactions by type
reactions
:
Reaction
[];
* - Shows reaction emoji with count
}
* - Allows adding new reactions (if not readonly)
*/
const
MemoReactionListView
=
observer
((
props
:
Props
)
=>
{
const
MemoReactionListView
=
observer
((
props
:
MemoReactionListViewProps
)
=>
{
const
{
memo
:
memoData
,
reactions
}
=
props
;
const
{
memo
:
memoData
,
reactions
}
=
props
;
const
currentUser
=
useCurrentUser
();
const
currentUser
=
useCurrentUser
();
const
reactionGroup
=
useReactionGroups
(
reactions
);
const
reactionGroup
=
useReactionGroups
(
reactions
);
...
...
web/src/components/
reactions
/ReactionSelector.tsx
→
web/src/components/
MemoReactionListView
/ReactionSelector.tsx
View file @
07072b75
import
{
SmilePlusIcon
}
from
"lucide-react"
;
import
{
SmilePlusIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
use
Callback
,
use
State
}
from
"react"
;
import
{
useState
}
from
"react"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
instanceStore
}
from
"@/store"
;
import
{
instanceStore
}
from
"@/store"
;
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useReactionActions
}
from
"./hooks"
;
import
{
useReactionActions
}
from
"./hooks"
;
import
type
{
ReactionSelectorProps
}
from
"./types"
;
/**
interface
Props
{
* ReactionSelector component provides a popover for selecting emoji reactions
memo
:
Memo
;
*/
className
?:
string
;
const
ReactionSelector
=
observer
((
props
:
ReactionSelectorProps
)
=>
{
onOpenChange
?:
(
open
:
boolean
)
=>
void
;
}
const
ReactionSelector
=
observer
((
props
:
Props
)
=>
{
const
{
memo
,
className
,
onOpenChange
}
=
props
;
const
{
memo
,
className
,
onOpenChange
}
=
props
;
const
[
open
,
setOpen
]
=
useState
(
false
);
const
[
open
,
setOpen
]
=
useState
(
false
);
const
handleOpenChange
=
useCallback
(
const
handleOpenChange
=
(
newOpen
:
boolean
)
=>
{
(
newOpen
:
boolean
)
=>
{
setOpen
(
newOpen
);
setOpen
(
newOpen
);
onOpenChange
?.(
newOpen
);
onOpenChange
?.(
newOpen
);
};
},
[
onOpenChange
],
);
const
{
hasReacted
,
handleReactionClick
}
=
useReactionActions
({
const
{
hasReacted
,
handleReactionClick
}
=
useReactionActions
({
memo
,
memo
,
...
...
web/src/components/
reactions
/ReactionView.tsx
→
web/src/components/
MemoReactionListView
/ReactionView.tsx
View file @
07072b75
...
@@ -3,14 +3,17 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
...
@@ -3,14 +3,17 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
cn
}
from
"@/lib/utils"
;
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
{
User
}
from
"@/types/proto/api/v1/user_service"
;
import
{
formatReactionTooltip
,
useReactionActions
}
from
"./hooks"
;
import
{
formatReactionTooltip
,
useReactionActions
}
from
"./hooks"
;
import
type
{
ReactionViewProps
}
from
"./types"
;
/**
interface
Props
{
* ReactionView component displays a single reaction pill with count
memo
:
Memo
;
* Clicking toggles the reaction for the current user
reactionType
:
string
;
*/
users
:
User
[];
const
ReactionView
=
observer
((
props
:
ReactionViewProps
)
=>
{
}
const
ReactionView
=
observer
((
props
:
Props
)
=>
{
const
{
memo
,
reactionType
,
users
}
=
props
;
const
{
memo
,
reactionType
,
users
}
=
props
;
const
currentUser
=
useCurrentUser
();
const
currentUser
=
useCurrentUser
();
const
hasReaction
=
users
.
some
((
user
)
=>
currentUser
&&
user
.
username
===
currentUser
.
username
);
const
hasReaction
=
users
.
some
((
user
)
=>
currentUser
&&
user
.
username
===
currentUser
.
username
);
...
...
web/src/components/MemoReactionListView/hooks.ts
View file @
07072b75
import
{
uniq
}
from
"lodash-es"
;
import
{
uniq
}
from
"lodash-es"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
userStore
}
from
"@/store"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
type
{
Reaction
}
from
"@/types/proto/api/v1/memo_service"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
memoStore
,
userStore
}
from
"@/store"
;
import
type
{
Memo
,
Reaction
}
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"
;
import
type
{
ReactionGroup
}
from
"./types"
;
/**
export
type
ReactionGroup
=
Map
<
string
,
User
[]
>
;
* Hook for grouping reactions by type and fetching user data
*/
export
const
useReactionGroups
=
(
reactions
:
Reaction
[]):
ReactionGroup
=>
{
export
const
useReactionGroups
=
(
reactions
:
Reaction
[]):
ReactionGroup
=>
{
const
[
reactionGroup
,
setReactionGroup
]
=
useState
<
ReactionGroup
>
(
new
Map
());
const
[
reactionGroup
,
setReactionGroup
]
=
useState
<
ReactionGroup
>
(
new
Map
());
useEffect
(()
=>
{
useEffect
(()
=>
{
const
fetchReactionGroups
=
async
()
=>
{
const
fetchReactionGroups
=
async
()
=>
{
const
newReactionGroup
=
new
Map
<
string
,
User
[]
>
();
const
newReactionGroup
=
new
Map
<
string
,
User
[]
>
();
for
(
const
reaction
of
reactions
)
{
for
(
const
reaction
of
reactions
)
{
const
user
=
await
userStore
.
getOrFetchUserByName
(
reaction
.
creator
);
const
user
=
await
userStore
.
getOrFetchUserByName
(
reaction
.
creator
);
const
users
=
newReactionGroup
.
get
(
reaction
.
reactionType
)
||
[];
const
users
=
newReactionGroup
.
get
(
reaction
.
reactionType
)
||
[];
users
.
push
(
user
);
users
.
push
(
user
);
newReactionGroup
.
set
(
reaction
.
reactionType
,
uniq
(
users
));
newReactionGroup
.
set
(
reaction
.
reactionType
,
uniq
(
users
));
}
}
setReactionGroup
(
newReactionGroup
);
setReactionGroup
(
newReactionGroup
);
};
};
fetchReactionGroups
();
fetchReactionGroups
();
},
[
reactions
]);
},
[
reactions
]);
return
reactionGroup
;
return
reactionGroup
;
};
};
interface
UseReactionActionsOptions
{
memo
:
Memo
;
onComplete
?:
()
=>
void
;
}
export
const
useReactionActions
=
({
memo
,
onComplete
}:
UseReactionActionsOptions
)
=>
{
const
currentUser
=
useCurrentUser
();
const
hasReacted
=
(
reactionType
:
string
)
=>
{
return
memo
.
reactions
.
some
((
r
)
=>
r
.
reactionType
===
reactionType
&&
r
.
creator
===
currentUser
?.
name
);
};
const
handleReactionClick
=
async
(
reactionType
:
string
)
=>
{
if
(
!
currentUser
)
return
;
try
{
if
(
hasReacted
(
reactionType
))
{
const
reactions
=
memo
.
reactions
.
filter
(
(
reaction
)
=>
reaction
.
reactionType
===
reactionType
&&
reaction
.
creator
===
currentUser
.
name
,
);
for
(
const
reaction
of
reactions
)
{
await
memoServiceClient
.
deleteMemoReaction
({
name
:
reaction
.
name
});
}
}
else
{
await
memoServiceClient
.
upsertMemoReaction
({
name
:
memo
.
name
,
reaction
:
{
contentId
:
memo
.
name
,
reactionType
},
});
}
await
memoStore
.
getOrFetchMemoByName
(
memo
.
name
,
{
skipCache
:
true
});
}
catch
{
// skip error
}
onComplete
?.();
};
return
{
hasReacted
,
handleReactionClick
};
};
export
const
formatReactionTooltip
=
(
users
:
User
[],
reactionType
:
string
):
string
=>
{
if
(
users
.
length
===
0
)
return
""
;
const
formatUserName
=
(
user
:
User
)
=>
user
.
displayName
||
user
.
username
;
if
(
users
.
length
<
5
)
{
return
`
${
users
.
map
(
formatUserName
).
join
(
", "
)}
reacted with
${
reactionType
.
toLowerCase
()}
`
;
}
return
`
${
users
.
slice
(
0
,
4
).
map
(
formatUserName
).
join
(
", "
)}
and
${
users
.
length
-
4
}
more reacted with
${
reactionType
.
toLowerCase
()}
`
;
};
web/src/components/MemoReactionListView/index.ts
View file @
07072b75
export
{
useReactionGroups
}
from
"./hooks"
;
export
{
default
,
default
as
MemoReactionListView
}
from
"./MemoReactionListView"
;
export
{
default
,
default
as
MemoReactionListView
}
from
"./MemoReactionListView"
;
export
type
{
MemoReactionListViewProps
,
ReactionGroup
}
from
"./types"
;
export
{
default
as
ReactionSelector
}
from
"./ReactionSelector"
;
export
{
default
as
ReactionView
}
from
"./ReactionView"
;
web/src/components/MemoReactionListView/types.ts
deleted
100644 → 0
View file @
6dcf7cc7
import
type
{
Memo
,
Reaction
}
from
"@/types/proto/api/v1/memo_service"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service"
;
/**
* Props for MemoReactionListView component
*/
export
interface
MemoReactionListViewProps
{
/** The memo that reactions belong to */
memo
:
Memo
;
/** List of reactions to display */
reactions
:
Reaction
[];
}
/**
* Grouped reactions with users who reacted
*/
export
type
ReactionGroup
=
Map
<
string
,
User
[]
>
;
web/src/components/MemoView/components/MemoHeader.tsx
View file @
07072b75
...
@@ -8,7 +8,7 @@ import type { User } from "@/types/proto/api/v1/user_service";
...
@@ -8,7 +8,7 @@ import type { User } from "@/types/proto/api/v1/user_service";
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
convertVisibilityToString
}
from
"@/utils/memo"
;
import
{
convertVisibilityToString
}
from
"@/utils/memo"
;
import
MemoActionMenu
from
"../../MemoActionMenu"
;
import
MemoActionMenu
from
"../../MemoActionMenu"
;
import
{
ReactionSelector
}
from
"../../
reactions
"
;
import
{
ReactionSelector
}
from
"../../
MemoReactionListView
"
;
import
UserAvatar
from
"../../UserAvatar"
;
import
UserAvatar
from
"../../UserAvatar"
;
import
VisibilityIcon
from
"../../VisibilityIcon"
;
import
VisibilityIcon
from
"../../VisibilityIcon"
;
import
{
useMemoViewContext
}
from
"../MemoViewContext"
;
import
{
useMemoViewContext
}
from
"../MemoViewContext"
;
...
...
web/src/components/reactions/hooks.ts
deleted
100644 → 0
View file @
6dcf7cc7
import
{
useCallback
}
from
"react"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
memoStore
}
from
"@/store"
;
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service"
;
interface
UseReactionActionsOptions
{
memo
:
Memo
;
onComplete
?:
()
=>
void
;
}
/**
* Hook for handling reaction add/remove operations
*/
export
const
useReactionActions
=
({
memo
,
onComplete
}:
UseReactionActionsOptions
)
=>
{
const
currentUser
=
useCurrentUser
();
const
hasReacted
=
useCallback
(
(
reactionType
:
string
)
=>
{
return
memo
.
reactions
.
some
((
r
)
=>
r
.
reactionType
===
reactionType
&&
r
.
creator
===
currentUser
?.
name
);
},
[
memo
.
reactions
,
currentUser
?.
name
],
);
const
handleReactionClick
=
useCallback
(
async
(
reactionType
:
string
)
=>
{
if
(
!
currentUser
)
return
;
try
{
if
(
hasReacted
(
reactionType
))
{
const
reactions
=
memo
.
reactions
.
filter
(
(
reaction
)
=>
reaction
.
reactionType
===
reactionType
&&
reaction
.
creator
===
currentUser
.
name
,
);
for
(
const
reaction
of
reactions
)
{
await
memoServiceClient
.
deleteMemoReaction
({
name
:
reaction
.
name
});
}
}
else
{
await
memoServiceClient
.
upsertMemoReaction
({
name
:
memo
.
name
,
reaction
:
{
contentId
:
memo
.
name
,
reactionType
,
},
});
}
await
memoStore
.
getOrFetchMemoByName
(
memo
.
name
,
{
skipCache
:
true
});
}
catch
{
// skip error
}
onComplete
?.();
},
[
memo
,
currentUser
,
hasReacted
,
onComplete
],
);
return
{
hasReacted
,
handleReactionClick
,
};
};
/**
* Format users list for tooltip display
*/
export
const
formatReactionTooltip
=
(
users
:
User
[],
reactionType
:
string
):
string
=>
{
if
(
users
.
length
===
0
)
{
return
""
;
}
const
formatUserName
=
(
user
:
User
)
=>
user
.
displayName
||
user
.
username
;
if
(
users
.
length
<
5
)
{
return
`
${
users
.
map
(
formatUserName
).
join
(
", "
)}
reacted with
${
reactionType
.
toLowerCase
()}
`
;
}
return
`
${
users
.
slice
(
0
,
4
).
map
(
formatUserName
).
join
(
", "
)}
and
${
users
.
length
-
4
}
more reacted with
${
reactionType
.
toLowerCase
()}
`
;
};
web/src/components/reactions/index.ts
deleted
100644 → 0
View file @
6dcf7cc7
/**
* Reaction components for memos
*
* This module provides components for displaying and managing reactions on memos:
* - ReactionSelector: Popover for selecting emoji reactions
* - ReactionView: Display a single reaction with count and tooltip
*/
export
{
formatReactionTooltip
,
useReactionActions
}
from
"./hooks"
;
export
{
default
as
ReactionSelector
}
from
"./ReactionSelector"
;
export
{
default
as
ReactionView
}
from
"./ReactionView"
;
export
type
{
ReactionSelectorProps
,
ReactionViewProps
}
from
"./types"
;
web/src/components/reactions/types.ts
deleted
100644 → 0
View file @
6dcf7cc7
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service"
;
/**
* Props for ReactionSelector component
*/
export
interface
ReactionSelectorProps
{
/** The memo to add reactions to */
memo
:
Memo
;
/** Additional CSS classes */
className
?:
string
;
/** Callback when popover open state changes */
onOpenChange
?:
(
open
:
boolean
)
=>
void
;
}
/**
* Props for ReactionView component
*/
export
interface
ReactionViewProps
{
/** The memo that the reaction belongs to */
memo
:
Memo
;
/** The emoji/reaction type */
reactionType
:
string
;
/** Users who added this reaction */
users
:
User
[];
}
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