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
d86f0bac
Commit
d86f0bac
authored
Feb 08, 2024
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: implement reaction frontend
parent
e5f244cb
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
229 additions
and
4 deletions
+229
-4
MemoReactionistView.tsx
web/src/components/MemoReactionistView.tsx
+43
-0
MemoRelationListView.tsx
web/src/components/MemoRelationListView.tsx
+2
-2
MemoView.tsx
web/src/components/MemoView.tsx
+3
-1
ReactionSelector.tsx
web/src/components/ReactionSelector.tsx
+79
-0
ReactionView.tsx
web/src/components/ReactionView.tsx
+100
-0
MemoDetail.tsx
web/src/pages/MemoDetail.tsx
+1
-1
resourceName.ts
web/src/store/v1/resourceName.ts
+1
-0
No files found.
web/src/components/MemoReactionistView.tsx
0 → 100644
View file @
d86f0bac
import
{
uniq
}
from
"lodash-es"
;
import
{
memo
,
useEffect
,
useState
}
from
"react"
;
import
{
extractUsernameFromName
,
useUserStore
}
from
"@/store/v1"
;
import
{
Memo
}
from
"@/types/proto/api/v2/memo_service"
;
import
{
Reaction
,
Reaction_Type
}
from
"@/types/proto/api/v2/reaction_service"
;
import
{
User
}
from
"@/types/proto/api/v2/user_service"
;
import
ReactionSelector
from
"./ReactionSelector"
;
import
ReactionView
from
"./ReactionView"
;
interface
Props
{
memo
:
Memo
;
reactions
:
Reaction
[];
}
const
MemoReactionListView
=
(
props
:
Props
)
=>
{
const
{
memo
,
reactions
}
=
props
;
const
userStore
=
useUserStore
();
const
[
reactionGroup
,
setReactionGroup
]
=
useState
<
Map
<
Reaction_Type
,
User
[]
>>
(
new
Map
());
useEffect
(()
=>
{
(
async
()
=>
{
const
reactionGroup
=
new
Map
<
Reaction_Type
,
User
[]
>
();
for
(
const
reaction
of
reactions
)
{
const
user
=
await
userStore
.
getOrFetchUserByUsername
(
extractUsernameFromName
(
reaction
.
creator
));
const
users
=
reactionGroup
.
get
(
reaction
.
reactionType
)
||
[];
users
.
push
(
user
);
reactionGroup
.
set
(
reaction
.
reactionType
,
uniq
(
users
));
}
setReactionGroup
(
reactionGroup
);
})();
},
[
reactions
]);
return
(
<
div
className=
"w-full mt-2 flex flex-row justify-start items-start flex-wrap gap-1"
>
<
ReactionSelector
memo=
{
memo
}
/>
{
Array
.
from
(
reactionGroup
).
map
(([
reactionType
,
users
])
=>
{
return
<
ReactionView
key=
{
`${reactionType.toString()} ${users.length}`
}
memo=
{
memo
}
reactionType=
{
reactionType
}
users=
{
users
}
/>;
})
}
</
div
>
);
};
export
default
memo
(
MemoReactionListView
);
web/src/components/MemoRelationListView.tsx
View file @
d86f0bac
...
@@ -8,11 +8,11 @@ import Icon from "./Icon";
...
@@ -8,11 +8,11 @@ import Icon from "./Icon";
interface
Props
{
interface
Props
{
memo
:
Memo
;
memo
:
Memo
;
relation
List
:
MemoRelation
[];
relation
s
:
MemoRelation
[];
}
}
const
MemoRelationListView
=
(
props
:
Props
)
=>
{
const
MemoRelationListView
=
(
props
:
Props
)
=>
{
const
{
memo
,
relationList
}
=
props
;
const
{
memo
,
relation
s
:
relation
List
}
=
props
;
const
memoStore
=
useMemoStore
();
const
memoStore
=
useMemoStore
();
const
[
referencingMemoList
,
setReferencingMemoList
]
=
useState
<
Memo
[]
>
([]);
const
[
referencingMemoList
,
setReferencingMemoList
]
=
useState
<
Memo
[]
>
([]);
const
[
referencedMemoList
,
setReferencedMemoList
]
=
useState
<
Memo
[]
>
([]);
const
[
referencedMemoList
,
setReferencedMemoList
]
=
useState
<
Memo
[]
>
([]);
...
...
web/src/components/MemoView.tsx
View file @
d86f0bac
...
@@ -20,6 +20,7 @@ import { showCommonDialog } from "./Dialog/CommonDialog";
...
@@ -20,6 +20,7 @@ import { showCommonDialog } from "./Dialog/CommonDialog";
import
Icon
from
"./Icon"
;
import
Icon
from
"./Icon"
;
import
MemoContent
from
"./MemoContent"
;
import
MemoContent
from
"./MemoContent"
;
import
showMemoEditorDialog
from
"./MemoEditor/MemoEditorDialog"
;
import
showMemoEditorDialog
from
"./MemoEditor/MemoEditorDialog"
;
import
MemoReactionistView
from
"./MemoReactionistView"
;
import
MemoRelationListView
from
"./MemoRelationListView"
;
import
MemoRelationListView
from
"./MemoRelationListView"
;
import
MemoResourceListView
from
"./MemoResourceListView"
;
import
MemoResourceListView
from
"./MemoResourceListView"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
...
@@ -265,7 +266,8 @@ const MemoView: React.FC<Props> = (props: Props) => {
...
@@ -265,7 +266,8 @@ const MemoView: React.FC<Props> = (props: Props) => {
onClick=
{
handleMemoContentClick
}
onClick=
{
handleMemoContentClick
}
/>
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
<
MemoRelationListView
memo=
{
memo
}
relationList=
{
referenceRelations
}
/>
<
MemoRelationListView
memo=
{
memo
}
relations=
{
referenceRelations
}
/>
<
MemoReactionistView
memo=
{
memo
}
reactions=
{
memo
.
reactions
}
/>
</
div
>
</
div
>
);
);
};
};
...
...
web/src/components/ReactionSelector.tsx
0 → 100644
View file @
d86f0bac
import
{
Dropdown
,
Menu
,
MenuButton
}
from
"@mui/joy"
;
import
{
useRef
,
useState
}
from
"react"
;
import
useClickAway
from
"react-use/lib/useClickAway"
;
import
Icon
from
"@/components/Icon"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
{
MemoNamePrefix
,
useMemoStore
}
from
"@/store/v1"
;
import
{
Memo
}
from
"@/types/proto/api/v2/memo_service"
;
import
{
Reaction_Type
}
from
"@/types/proto/api/v2/reaction_service"
;
import
{
stringifyReactionType
}
from
"./ReactionView"
;
interface
Props
{
memo
:
Memo
;
}
const
REACTION_TYPES
=
[
Reaction_Type
.
THUMBS_UP
,
Reaction_Type
.
HEART
,
Reaction_Type
.
ROCKET
,
Reaction_Type
.
LAUGH
,
Reaction_Type
.
EYES
,
Reaction_Type
.
THUMBS_DOWN
,
];
const
ReactionSelector
=
(
props
:
Props
)
=>
{
const
{
memo
}
=
props
;
const
memoStore
=
useMemoStore
();
const
[
open
,
setOpen
]
=
useState
(
false
);
const
containerRef
=
useRef
<
HTMLDivElement
>
(
null
);
useClickAway
(
containerRef
,
()
=>
{
setOpen
(
false
);
});
const
handleReactionClick
=
async
(
reaction
:
Reaction_Type
)
=>
{
try
{
await
memoServiceClient
.
upsertMemoReaction
({
id
:
memo
.
id
,
reaction
:
{
contentId
:
`
${
MemoNamePrefix
}${
memo
.
id
}
`
,
reactionType
:
reaction
,
},
});
await
memoStore
.
getOrFetchMemoById
(
memo
.
id
,
{
skipCache
:
true
,
});
}
catch
(
error
)
{
// skip error.
}
};
return
(
<
Dropdown
open=
{
open
}
onOpenChange=
{
(
_
,
isOpen
)
=>
setOpen
(
isOpen
)
}
>
<
MenuButton
slots=
{
{
root
:
"div"
}
}
slotProps=
{
{}
}
>
<
span
className=
"h-7 w-7 flex justify-center items-center rounded-full border dark:border-zinc-700 hover:opacity-80"
>
<
Icon
.
Smile
className=
"w-4 h-4 mx-auto dark:text-gray-400"
/>
</
span
>
</
MenuButton
>
<
Menu
className=
"relative text-sm"
component=
"div"
size=
"sm"
placement=
"bottom-start"
>
<
div
ref=
{
containerRef
}
>
<
div
className=
"flex-row justify-start items-start py-0.5 px-2 h-auto font-mono space-x-1"
>
{
REACTION_TYPES
.
map
((
reactionType
)
=>
{
return
(
<
div
key=
{
reactionType
}
className=
"inline-flex w-auto cursor-pointer rounded text-lg px-1 text-gray-500 dark:text-gray-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
onClick=
{
()
=>
handleReactionClick
(
reactionType
)
}
>
{
stringifyReactionType
(
reactionType
)
}
</
div
>
);
})
}
</
div
>
</
div
>
</
Menu
>
</
Dropdown
>
);
};
export
default
ReactionSelector
;
web/src/components/ReactionView.tsx
0 → 100644
View file @
d86f0bac
import
{
Tooltip
}
from
"@mui/joy"
;
import
classNames
from
"classnames"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
MemoNamePrefix
,
useMemoStore
}
from
"@/store/v1"
;
import
{
Memo
}
from
"@/types/proto/api/v2/memo_service"
;
import
{
Reaction_Type
}
from
"@/types/proto/api/v2/reaction_service"
;
import
{
User
}
from
"@/types/proto/api/v2/user_service"
;
interface
Props
{
memo
:
Memo
;
reactionType
:
Reaction_Type
;
users
:
User
[];
}
export
const
stringifyReactionType
=
(
reactionType
:
Reaction_Type
):
string
=>
{
switch
(
reactionType
)
{
case
Reaction_Type
.
EYES
:
return
"👀"
;
case
Reaction_Type
.
HEART
:
return
"💗"
;
case
Reaction_Type
.
LAUGH
:
return
"😂"
;
case
Reaction_Type
.
ROCKET
:
return
"🚀"
;
case
Reaction_Type
.
THUMBS_DOWN
:
return
"👎"
;
case
Reaction_Type
.
THUMBS_UP
:
return
"👍"
;
default
:
return
""
;
}
};
const
stringifyUsers
=
(
users
:
User
[]):
string
=>
{
if
(
users
.
length
===
0
)
{
return
""
;
}
if
(
users
.
length
<
5
)
{
return
users
.
map
((
user
)
=>
user
.
nickname
||
user
.
username
).
join
(
", "
);
}
return
`
${
users
.
slice
(
0
,
5
)
.
map
((
user
)
=>
user
.
nickname
||
user
.
username
)
.
join
(
", "
)}
and
${
users
.
length
-
5
}
others`
;
};
const
ReactionView
=
(
props
:
Props
)
=>
{
const
{
memo
,
reactionType
,
users
}
=
props
;
const
currenUser
=
useCurrentUser
();
const
memoStore
=
useMemoStore
();
const
hasReaction
=
users
.
some
((
user
)
=>
currenUser
&&
user
.
username
===
currenUser
.
username
);
const
handleReactionClick
=
async
()
=>
{
if
(
!
currenUser
)
{
return
;
}
const
index
=
users
.
findIndex
((
user
)
=>
user
.
username
===
currenUser
.
username
);
try
{
if
(
index
===
-
1
)
{
await
memoServiceClient
.
upsertMemoReaction
({
id
:
memo
.
id
,
reaction
:
{
contentId
:
`
${
MemoNamePrefix
}${
memo
.
id
}
`
,
reactionType
,
},
});
}
else
{
const
reactions
=
memo
.
reactions
.
filter
(
(
reaction
)
=>
reaction
.
reactionType
===
reactionType
&&
reaction
.
creator
===
currenUser
.
name
,
);
for
(
const
reaction
of
reactions
)
{
await
memoServiceClient
.
deleteMemoReaction
({
id
:
reaction
.
id
});
}
}
}
catch
(
error
)
{
// Skip error.
}
await
memoStore
.
getOrFetchMemoById
(
memo
.
id
,
{
skipCache
:
true
});
};
return
(
<
Tooltip
title=
{
stringifyUsers
(
users
)
}
placement=
"top"
>
<
div
className=
{
classNames
(
"h-7 border px-2 py-0.5 rounded-full font-memo flex flex-row justify-center items-center gap-1 dark:border-zinc-700"
,
currenUser
&&
"cursor-pointer"
,
hasReaction
&&
"bg-blue-50 border-blue-100 dark:bg-zinc-900"
,
)
}
onClick=
{
handleReactionClick
}
>
<
span
>
{
stringifyReactionType
(
reactionType
)
}
</
span
>
<
span
className=
"text-sm text-gray-500 dark:text-gray-400"
>
{
users
.
length
}
</
span
>
</
div
>
</
Tooltip
>
);
};
export
default
ReactionView
;
web/src/pages/MemoDetail.tsx
View file @
d86f0bac
...
@@ -139,7 +139,7 @@ const MemoDetail = () => {
...
@@ -139,7 +139,7 @@ const MemoDetail = () => {
)
}
)
}
<
MemoContent
key=
{
`${memo.id}-${memo.updateTime}`
}
memoId=
{
memo
.
id
}
content=
{
memo
.
content
}
readonly=
{
readonly
}
/>
<
MemoContent
key=
{
`${memo.id}-${memo.updateTime}`
}
memoId=
{
memo
.
id
}
content=
{
memo
.
content
}
readonly=
{
readonly
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
<
MemoRelationListView
memo=
{
memo
}
relation
List
=
{
referenceRelations
}
/>
<
MemoRelationListView
memo=
{
memo
}
relation
s
=
{
referenceRelations
}
/>
<
div
className=
"w-full mt-3 flex flex-row justify-between items-center gap-2"
>
<
div
className=
"w-full mt-3 flex flex-row justify-between items-center gap-2"
>
<
div
className=
"flex flex-row justify-start items-center"
>
<
div
className=
"flex flex-row justify-start items-center"
>
{
!
readonly
&&
(
{
!
readonly
&&
(
...
...
web/src/store/v1/resourceName.ts
View file @
d86f0bac
export
const
UserNamePrefix
=
"users/"
;
export
const
UserNamePrefix
=
"users/"
;
export
const
MemoNamePrefix
=
"memos/"
;
export
const
extractUsernameFromName
=
(
name
:
string
=
""
)
=>
{
export
const
extractUsernameFromName
=
(
name
:
string
=
""
)
=>
{
return
name
.
slice
(
UserNamePrefix
.
length
);
return
name
.
slice
(
UserNamePrefix
.
length
);
...
...
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