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
e7951491
Commit
e7951491
authored
Mar 13, 2024
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: tweak memo view display
parent
8fe6874b
Changes
15
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
378 additions
and
455 deletions
+378
-455
user_service.proto
proto/api/v2/user_service.proto
+6
-4
README.md
proto/gen/api/v2/README.md
+1
-0
user_service.pb.go
proto/gen/api/v2/user_service.pb.go
+249
-239
apidocs.swagger.yaml
server/route/api/v2/apidocs.swagger.yaml
+4
-0
user_service.go
server/route/api/v2/user_service.go
+13
-10
10001__user.sql
store/db/sqlite/seed/10001__user.sql
+12
-6
full-logo.webp
web/public/full-logo.webp
+0
-0
MemoActionMenu.tsx
web/src/components/MemoActionMenu.tsx
+1
-13
MemoView.tsx
web/src/components/MemoView.tsx
+35
-29
MyAccountSection.tsx
web/src/components/Settings/MyAccountSection.tsx
+8
-5
UpdateAccountDialog.tsx
web/src/components/UpdateAccountDialog.tsx
+25
-1
UserAvatar.tsx
web/src/components/UserAvatar.tsx
+3
-3
en.json
web/src/locales/en.json
+2
-1
Explore.tsx
web/src/pages/Explore.tsx
+1
-1
MemoDetail.tsx
web/src/pages/MemoDetail.tsx
+18
-143
No files found.
proto/api/v2/user_service.proto
View file @
e7951491
...
...
@@ -98,13 +98,15 @@ message User {
string
avatar_url
=
7
;
string
password
=
8
[(
google.api.field_behavior
)
=
INPUT_ONLY
]
;
string
description
=
8
;
RowStatus
row_status
=
9
;
string
password
=
9
[(
google.api.field_behavior
)
=
INPUT_ONLY
]
;
google.protobuf.Timestamp
create_time
=
10
;
RowStatus
row_status
=
10
;
google.protobuf.Timestamp
update_time
=
11
;
google.protobuf.Timestamp
create_time
=
11
;
google.protobuf.Timestamp
update_time
=
12
;
}
message
ListUsersRequest
{}
...
...
proto/gen/api/v2/README.md
View file @
e7951491
...
...
@@ -684,6 +684,7 @@ Used internally for obfuscating the page token.
| email |
[
string
](
#string
)
| | |
| nickname |
[
string
](
#string
)
| | |
| avatar_url |
[
string
](
#string
)
| | |
| description |
[
string
](
#string
)
| | |
| password |
[
string
](
#string
)
| | |
| row_status |
[
RowStatus
](
#memos-api-v2-RowStatus
)
| | |
| create_time |
[
google.protobuf.Timestamp
](
#google-protobuf-Timestamp
)
| | |
...
...
proto/gen/api/v2/user_service.pb.go
View file @
e7951491
This diff is collapsed.
Click to expand it.
server/route/api/v2/apidocs.swagger.yaml
View file @
e7951491
...
...
@@ -1500,6 +1500,8 @@ paths:
type
:
string
avatarUrl
:
type
:
string
description
:
type
:
string
password
:
type
:
string
rowStatus
:
...
...
@@ -2223,6 +2225,8 @@ definitions:
type
:
string
avatarUrl
:
type
:
string
description
:
type
:
string
password
:
type
:
string
rowStatus
:
...
...
server/route/api/v2/user_service.go
View file @
e7951491
...
...
@@ -151,6 +151,8 @@ func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUs
update
.
Email
=
&
request
.
User
.
Email
}
else
if
field
==
"avatar_url"
{
update
.
AvatarURL
=
&
request
.
User
.
AvatarUrl
}
else
if
field
==
"description"
{
update
.
Description
=
&
request
.
User
.
Description
}
else
if
field
==
"role"
{
role
:=
convertUserRoleToStore
(
request
.
User
.
Role
)
update
.
Role
=
&
role
...
...
@@ -499,16 +501,17 @@ func (s *APIV2Service) UpsertAccessTokenToStore(ctx context.Context, user *store
func
convertUserFromStore
(
user
*
store
.
User
)
*
apiv2pb
.
User
{
return
&
apiv2pb
.
User
{
Name
:
fmt
.
Sprintf
(
"%s%s"
,
UserNamePrefix
,
user
.
Username
),
Id
:
user
.
ID
,
RowStatus
:
convertRowStatusFromStore
(
user
.
RowStatus
),
CreateTime
:
timestamppb
.
New
(
time
.
Unix
(
user
.
CreatedTs
,
0
)),
UpdateTime
:
timestamppb
.
New
(
time
.
Unix
(
user
.
UpdatedTs
,
0
)),
Role
:
convertUserRoleFromStore
(
user
.
Role
),
Username
:
user
.
Username
,
Email
:
user
.
Email
,
Nickname
:
user
.
Nickname
,
AvatarUrl
:
user
.
AvatarURL
,
Name
:
fmt
.
Sprintf
(
"%s%s"
,
UserNamePrefix
,
user
.
Username
),
Id
:
user
.
ID
,
RowStatus
:
convertRowStatusFromStore
(
user
.
RowStatus
),
CreateTime
:
timestamppb
.
New
(
time
.
Unix
(
user
.
CreatedTs
,
0
)),
UpdateTime
:
timestamppb
.
New
(
time
.
Unix
(
user
.
UpdatedTs
,
0
)),
Role
:
convertUserRoleFromStore
(
user
.
Role
),
Username
:
user
.
Username
,
Email
:
user
.
Email
,
Nickname
:
user
.
Nickname
,
AvatarUrl
:
user
.
AvatarURL
,
Description
:
user
.
Description
,
}
}
...
...
store/db/sqlite/seed/10001__user.sql
View file @
e7951491
...
...
@@ -5,7 +5,8 @@ INSERT INTO
`role`
,
`email`
,
`nickname`
,
`password_hash`
`password_hash`
,
`description`
)
VALUES
(
...
...
@@ -15,7 +16,8 @@ VALUES
'demo@usememos.com'
,
'Derobot'
,
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
,
'👋 Welcome to memos.'
);
INSERT
INTO
...
...
@@ -25,7 +27,8 @@ INSERT INTO
`role`
,
`email`
,
`nickname`
,
`password_hash`
`password_hash`
,
`description`
)
VALUES
(
...
...
@@ -35,7 +38,8 @@ VALUES
'jack@usememos.com'
,
'Jack'
,
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
,
'The REAL Jack.'
);
INSERT
INTO
...
...
@@ -46,7 +50,8 @@ INSERT INTO
`role`
,
`email`
,
`nickname`
,
`password_hash`
`password_hash`
,
`description`
)
VALUES
(
...
...
@@ -57,5 +62,6 @@ VALUES
'bob@usememos.com'
,
'Bob'
,
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
,
'Sorry, I am busy right now.'
);
\ No newline at end of file
web/public/full-logo.webp
0 → 100644
View file @
e7951491
File added
web/src/components/MemoActionMenu.tsx
View file @
e7951491
import
{
D
ivider
,
D
ropdown
,
Menu
,
MenuButton
,
MenuItem
}
from
"@mui/joy"
;
import
{
Dropdown
,
Menu
,
MenuButton
,
MenuItem
}
from
"@mui/joy"
;
import
classNames
from
"classnames"
;
import
copy
from
"copy-to-clipboard"
;
import
toast
from
"react-hot-toast"
;
import
Icon
from
"@/components/Icon"
;
import
{
useMemoStore
}
from
"@/store/v1"
;
...
...
@@ -88,11 +87,6 @@ const MemoActionMenu = (props: Props) => {
});
};
const
handleCopyMemoId
=
()
=>
{
copy
(
memo
.
name
);
toast
.
success
(
"Copied to clipboard!"
);
};
return
(
<
Dropdown
>
<
MenuButton
slots=
{
{
root
:
"div"
}
}
>
...
...
@@ -127,12 +121,6 @@ const MemoActionMenu = (props: Props) => {
<
Icon
.
Trash
className=
"w-4 h-auto"
/>
{
t
(
"common.delete"
)
}
</
MenuItem
>
<
Divider
className=
"!my-1"
/>
<
div
className=
"-mt-0.5 pl-2 pr-2 text-xs text-gray-400"
>
<
div
className=
"mt-1 font-mono max-w-20 cursor-pointer truncate"
onClick=
{
handleCopyMemoId
}
>
ID:
{
memo
.
name
}
</
div
>
</
div
>
</
Menu
>
</
Dropdown
>
);
...
...
web/src/components/MemoView.tsx
View file @
e7951491
...
...
@@ -25,7 +25,6 @@ import VisibilityIcon from "./VisibilityIcon";
interface
Props
{
memo
:
Memo
;
showCreator
?:
boolean
;
showVisibility
?:
boolean
;
showPinned
?:
boolean
;
className
?:
string
;
...
...
@@ -92,43 +91,40 @@ const MemoView: React.FC<Props> = (props: Props) => {
return
(
<
div
className=
{
classNames
(
"group relative flex flex-col justify-start items-start w-full px-4 pt-
2 pb-3
mb-2 bg-white dark:bg-zinc-800 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700"
,
"group relative flex flex-col justify-start items-start w-full px-4 pt-
4 pb-3
mb-2 bg-white dark:bg-zinc-800 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700"
,
"memos-"
+
memo
.
id
,
memo
.
pinned
&&
props
.
showPinned
&&
"border-gray-200 border dark:border-zinc-700"
,
className
,
)
}
ref=
{
memoContainerRef
}
>
<
div
className=
"w-full h-7 flex flex-row justify-between items-center mb-1"
>
<
div
className=
"w-auto flex flex-row justify-start items-center mr-1"
>
{
props
.
showCreator
&&
creator
&&
(
<>
<
Link
to=
{
`/u/${encodeURIComponent(extractUsernameFromName(memo.creator))}`
}
unstable_viewTransition
>
<
Tooltip
title=
{
"Creator"
}
placement=
"top"
>
<
span
className=
"flex flex-row justify-start items-center"
>
<
UserAvatar
className=
"!w-5 !h-5 mr-1"
avatarUrl=
{
creator
.
avatarUrl
}
/>
<
span
className=
"text-sm text-gray-600 max-w-[8em] truncate dark:text-gray-400"
>
{
creator
.
nickname
||
creator
.
username
}
</
span
>
</
span
>
</
Tooltip
>
<
div
className=
"w-full h-7 flex flex-row justify-between items-center mb-2 gap-2"
>
<
div
className=
"w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center"
>
{
creator
&&
(
<
div
className=
"w-full flex flex-row justify-start items-center"
>
<
Link
className=
"w-auto hover:opacity-80"
to=
{
`/u/${encodeURIComponent(extractUsernameFromName(memo.creator))}`
}
unstable_viewTransition
>
<
UserAvatar
className=
"mr-2 shrink-0"
avatarUrl=
{
creator
.
avatarUrl
}
/>
</
Link
>
<
Icon
.
Dot
className=
"w-4 h-auto text-gray-400 dark:text-zinc-400"
/
>
</>
)
}
<
span
className=
"text-sm text-gray-400 select-none"
onClick=
{
handleGotoMemoDetailPage
}
>
{
displayTime
}
</
span
>
{
props
.
showPinned
&&
memo
.
pinned
&&
(
<>
<
Icon
.
Dot
className=
"w-4 h-auto text-gray-400 dark:text-zinc-400"
/
>
<
Tooltip
title=
{
"Pinned"
}
placement=
"top"
>
<
Icon
.
Bookmark
className=
"w-4 h-auto text-amber-500"
/
>
</
Tooltip
>
</>
<
div
className=
"w-full flex flex-col justify-center items-start"
>
<
Link
className=
"w-auto leading-none hover:opacity-80"
to=
{
`/u/${encodeURIComponent(extractUsernameFromName(memo.creator))}`
}
unstable_viewTransition
>
<
span
className=
"text-gray-600 text-lg leading-none max-w-[80%] truncate dark:text-gray-400"
>
{
creator
.
nickname
||
creator
.
username
}
</
span
>
</
Link
>
<
span
className=
"text-gray-400 text-sm leading-none max-w-[80%] truncate dark:text-gray-500"
>
{
creator
.
description
}
</
span
>
</
div
>
</
div
>
)
}
</
div
>
<
div
className=
"flex flex-row justify-end items-center select-none gap-1"
>
<
div
className=
"flex flex-row justify-end items-center select-none
shrink-0
gap-1"
>
<
div
className=
"w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-1"
>
{
props
.
showVisibility
&&
memo
.
visibility
!==
Visibility
.
PRIVATE
&&
(
<
Tooltip
title=
{
t
(
`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}`
as
any
)
}
placement=
"top"
>
...
...
@@ -150,6 +146,11 @@ const MemoView: React.FC<Props> = (props: Props) => {
<
Icon
.
MessageCircleMore
className=
"w-4 h-4 mx-auto text-gray-500 dark:text-gray-400"
/>
{
commentAmount
>
0
&&
<
span
className=
"text-xs text-gray-500 dark:text-gray-400"
>
{
commentAmount
}
</
span
>
}
</
Link
>
{
props
.
showPinned
&&
memo
.
pinned
&&
(
<
Tooltip
title=
{
"Pinned"
}
placement=
"top"
>
<
Icon
.
Bookmark
className=
"ml-1 w-4 h-auto text-amber-500"
/>
</
Tooltip
>
)
}
{
!
readonly
&&
<
MemoActionMenu
memo=
{
memo
}
hiddenActions=
{
props
.
showPinned
?
[]
:
[
"pin"
]
}
/>
}
</
div
>
</
div
>
...
...
@@ -162,6 +163,11 @@ const MemoView: React.FC<Props> = (props: Props) => {
compact=
{
true
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
<
div
className=
"w-full flex flex-row justify-between items-center mt-1"
>
<
span
className=
"text-sm leading-6 text-gray-400 select-none"
onClick=
{
handleGotoMemoDetailPage
}
>
{
displayTime
}
</
span
>
</
div
>
<
MemoRelationListView
memo=
{
memo
}
relations=
{
referencedMemos
}
/>
<
MemoReactionistView
memo=
{
memo
}
reactions=
{
memo
.
reactions
}
/>
</
div
>
...
...
web/src/components/Settings/MyAccountSection.tsx
View file @
e7951491
...
...
@@ -22,11 +22,14 @@ const MyAccountSection = () => {
return
(
<
div
className=
"w-full gap-2 pt-2 pb-4"
>
<
p
className=
"font-medium text-gray-700 dark:text-gray-500"
>
{
t
(
"setting.account-section.title"
)
}
</
p
>
<
div
className=
"mt-1 flex flex-row justify-start items-center"
>
<
UserAvatar
className=
"mr-2"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
div
className=
"flex flex-col justify-center items-start"
>
<
span
className=
"text-2xl font-medium"
>
{
user
.
nickname
}
</
span
>
<
span
className=
"-mt-2 text-base text-gray-500 dark:text-gray-400"
>
(
{
user
.
username
}
)
</
span
>
<
div
className=
"w-full mt-2 flex flex-row justify-start items-center"
>
<
UserAvatar
className=
"mr-2 shrink-0 w-10 h-10"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
div
className=
"max-w-[calc(100%-3rem)] flex flex-col justify-center items-start"
>
<
p
className=
"w-full"
>
<
span
className=
"text-xl leading-none font-medium"
>
{
user
.
nickname
}
</
span
>
<
span
className=
"ml-1 text-base leading-none text-gray-500 dark:text-gray-400"
>
(
{
user
.
username
}
)
</
span
>
</
p
>
<
p
className=
"w-4/5 leading-none text-sm truncate"
>
{
user
.
description
}
</
p
>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center mt-2 space-x-2"
>
...
...
web/src/components/UpdateAccountDialog.tsx
View file @
e7951491
import
{
Button
,
IconButton
,
Input
}
from
"@mui/joy"
;
import
{
Button
,
IconButton
,
Input
,
Textarea
}
from
"@mui/joy"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
...
...
@@ -18,6 +18,7 @@ interface State {
username
:
string
;
nickname
:
string
;
email
:
string
;
description
:
string
;
}
const
UpdateAccountDialog
:
React
.
FC
<
Props
>
=
({
destroy
}:
Props
)
=>
{
...
...
@@ -29,6 +30,7 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
username
:
currentUser
.
name
.
replace
(
UserNamePrefix
,
""
),
nickname
:
currentUser
.
nickname
,
email
:
currentUser
.
email
,
description
:
currentUser
.
description
,
});
const
handleCloseBtnClick
=
()
=>
{
...
...
@@ -85,6 +87,15 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
});
};
const
handleDescriptionChanged
=
(
e
:
React
.
ChangeEvent
<
HTMLTextAreaElement
>
)
=>
{
setState
((
state
)
=>
{
return
{
...
state
,
description
:
e
.
target
.
value
as
string
,
};
});
};
const
handleSaveBtnClick
=
async
()
=>
{
if
(
state
.
username
===
""
)
{
toast
.
error
(
t
(
"message.fill-all"
));
...
...
@@ -105,6 +116,9 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
if
(
!
isEqual
(
currentUser
.
avatarUrl
,
state
.
avatarUrl
))
{
updateMask
.
push
(
"avatar_url"
);
}
if
(
!
isEqual
(
currentUser
.
description
,
state
.
description
))
{
updateMask
.
push
(
"description"
);
}
await
userStore
.
updateUser
(
UserPb
.
fromPartial
({
name
:
currentUser
.
name
,
...
...
@@ -112,6 +126,7 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
nickname
:
state
.
nickname
,
email
:
state
.
email
,
avatarUrl
:
state
.
avatarUrl
,
description
:
state
.
description
,
}),
updateMask
,
);
...
...
@@ -164,6 +179,15 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
<
span
className=
"text-sm text-gray-400 ml-1"
>
(
{
t
(
"setting.account-section.email-note"
)
}
)
</
span
>
</
p
>
<
Input
className=
"w-full"
type=
"email"
value=
{
state
.
email
}
onChange=
{
handleEmailChanged
}
/>
<
p
className=
"text-sm"
>
{
t
(
"common.description"
)
}
</
p
>
<
Textarea
className=
"w-full"
color=
"neutral"
minRows=
{
2
}
maxRows=
{
4
}
value=
{
state
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
<
div
className=
"w-full flex flex-row justify-end items-center pt-4 space-x-2"
>
<
Button
color=
"neutral"
variant=
"plain"
onClick=
{
handleCloseBtnClick
}
>
{
t
(
"common.cancel"
)
}
...
...
web/src/components/UserAvatar.tsx
View file @
e7951491
...
...
@@ -8,10 +8,10 @@ interface Props {
const
UserAvatar
=
(
props
:
Props
)
=>
{
const
{
avatarUrl
,
className
}
=
props
;
return
(
<
div
className=
{
classNames
(
`w-8 h-8 overflow-clip rounded-
full
`
,
className
)
}
>
<
div
className=
{
classNames
(
`w-8 h-8 overflow-clip rounded-
lg
`
,
className
)
}
>
<
img
className=
"w-full h-auto
rounded-full
shadow min-w-full min-h-full object-cover dark:opacity-80"
src=
{
avatarUrl
||
"/logo.webp"
}
className=
"w-full h-auto shadow min-w-full min-h-full object-cover dark:opacity-80"
src=
{
avatarUrl
||
"/
full-
logo.webp"
}
decoding=
"async"
loading=
"lazy"
alt=
""
...
...
web/src/locales/en.json
View file @
e7951491
...
...
@@ -60,7 +60,8 @@
"profile"
:
"Profile"
,
"inbox"
:
"Inbox"
,
"search"
:
"Search"
,
"role"
:
"Role"
"role"
:
"Role"
,
"description"
:
"Description"
},
"router"
:
{
"go-to-home"
:
"Go to Home"
,
...
...
web/src/pages/Explore.tsx
View file @
e7951491
...
...
@@ -56,7 +56,7 @@ const Explore = () => {
<
div
className=
"relative w-full h-auto flex flex-col justify-start items-start px-4 sm:px-6"
>
<
MemoFilter
className=
"px-2 pb-2"
/>
{
sortedMemos
.
map
((
memo
)
=>
(
<
MemoView
key=
{
`${memo.id}-${memo.displayTime}`
}
memo=
{
memo
}
showCreator
/>
<
MemoView
key=
{
`${memo.id}-${memo.displayTime}`
}
memo=
{
memo
}
/>
))
}
{
isRequesting
?
(
<
div
className=
"flex flex-row justify-center items-center w-full my-4 text-gray-400"
>
...
...
web/src/pages/MemoDetail.tsx
View file @
e7951491
import
{
Select
,
Tooltip
,
Option
,
IconButton
}
from
"@mui/joy"
;
import
copy
from
"copy-to-clipboard"
;
import
{
ClientError
}
from
"nice-grpc-web"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Link
,
useParams
}
from
"react-router-dom"
;
import
Icon
from
"@/components/Icon"
;
import
MemoActionMenu
from
"@/components/MemoActionMenu"
;
import
MemoContent
from
"@/components/MemoContent"
;
import
MemoEditor
from
"@/components/MemoEditor"
;
import
showMemoEditorDialog
from
"@/components/MemoEditor/MemoEditorDialog"
;
import
MemoRelationListView
from
"@/components/MemoRelationListView"
;
import
MemoResourceListView
from
"@/components/MemoResourceListView"
;
import
MemoView
from
"@/components/MemoView"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
showShareMemoDialog
from
"@/components/ShareMemoDialog"
;
import
UserAvatar
from
"@/components/UserAvatar"
;
import
VisibilityIcon
from
"@/components/VisibilityIcon"
;
import
{
getDateTimeString
}
from
"@/helpers/datetime"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
use
UserStore
,
useMemoStore
,
extractUsernameFromNam
e
}
from
"@/store/v1"
;
import
{
use
MemoStor
e
}
from
"@/store/v1"
;
import
{
MemoRelation_Type
}
from
"@/types/proto/api/v2/memo_relation_service"
;
import
{
Memo
,
Visibility
}
from
"@/types/proto/api/v2/memo_service"
;
import
{
User
}
from
"@/types/proto/api/v2/user_service"
;
import
{
Memo
}
from
"@/types/proto/api/v2/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
convertVisibilityToString
}
from
"@/utils/memo"
;
const
MemoDetail
=
()
=>
{
const
t
=
useTranslate
();
...
...
@@ -32,30 +19,20 @@ const MemoDetail = () => {
const
navigateTo
=
useNavigateTo
();
const
currentUser
=
useCurrentUser
();
const
memoStore
=
useMemoStore
();
const
userStore
=
useUserStore
();
const
[
creator
,
setCreator
]
=
useState
<
User
>
();
const
memoName
=
params
.
memoName
;
const
memo
=
memoStore
.
getMemoByName
(
memoName
||
""
);
const
[
parentMemo
,
setParentMemo
]
=
useState
<
Memo
|
undefined
>
(
undefined
);
const
referenceRelations
=
memo
?.
relations
.
filter
((
relation
)
=>
relation
.
type
===
MemoRelation_Type
.
REFERENCE
)
||
[];
const
commentRelations
=
memo
?.
relations
.
filter
((
relation
)
=>
relation
.
relatedMemoId
===
memo
?.
id
&&
relation
.
type
===
MemoRelation_Type
.
COMMENT
)
||
[];
const
comments
=
commentRelations
.
map
((
relation
)
=>
memoStore
.
getMemoById
(
relation
.
memoId
)).
filter
((
memo
)
=>
memo
)
as
any
as
Memo
[];
const
readonly
=
memo
?.
creatorId
!==
currentUser
?.
id
;
// Prepare memo.
useEffect
(()
=>
{
if
(
memoName
)
{
memoStore
.
getOrFetchMemoByName
(
memoName
)
.
then
(
async
(
memo
)
=>
{
const
user
=
await
userStore
.
getOrFetchUserByUsername
(
extractUsernameFromName
(
memo
.
creator
));
setCreator
(
user
);
})
.
catch
((
error
:
ClientError
)
=>
{
toast
.
error
(
error
.
details
);
navigateTo
(
"/403"
);
});
memoStore
.
getOrFetchMemoByName
(
memoName
).
catch
((
error
:
ClientError
)
=>
{
toast
.
error
(
error
.
details
);
navigateTo
(
"/403"
);
});
}
else
{
navigateTo
(
"/404"
);
}
...
...
@@ -83,130 +60,28 @@ const MemoDetail = () => {
return
null
;
}
const
handleMemoVisibilityOptionChanged
=
async
(
visibility
:
Visibility
)
=>
{
await
memoStore
.
updateMemo
(
{
id
:
memo
.
id
,
visibility
:
visibility
,
},
[
"visibility"
],
);
};
const
handleEditMemoClick
=
()
=>
{
showMemoEditorDialog
({
memoId
:
memo
.
id
,
cacheKey
:
`
${
memo
.
id
}
-
${
memo
.
updateTime
}
`
,
});
};
const
handleCopyLinkBtnClick
=
()
=>
{
copy
(
`
${
window
.
location
.
origin
}
/m/
${
memo
.
name
}
`
);
if
(
memo
.
visibility
!==
Visibility
.
PUBLIC
)
{
toast
.
success
(
t
(
"message.succeed-copy-link-not-public"
));
}
else
{
toast
.
success
(
t
(
"message.succeed-copy-link"
));
}
};
const
handleCommentCreated
=
async
(
commentId
:
number
)
=>
{
await
memoStore
.
getOrFetchMemoById
(
commentId
);
await
memoStore
.
getOrFetchMemoById
(
memo
.
id
,
{
skipCache
:
true
});
};
const
handleMemoArchived
=
()
=>
{
navigateTo
(
"/archived"
);
toast
.
success
(
"Memo archived"
);
};
const
handleMemoDeleted
=
()
=>
{
navigateTo
(
"/"
);
toast
.
success
(
"Memo deleted"
);
};
return
(
<
section
className=
"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8"
>
<
MobileHeader
/>
<
div
className=
"w-full px-4 sm:px-6"
>
<
div
className=
"relative flex-grow w-full min-h-full flex flex-col justify-start items-start border dark:border-zinc-700 bg-white dark:bg-zinc-800 shadow hover:shadow-xl transition-all p-4 pb-3 rounded-lg"
>
<
div
className=
"mb-3"
>
<
Link
to=
{
`/u/${encodeURIComponent(extractUsernameFromName(memo.creator))}`
}
unstable_viewTransition
>
<
span
className=
"w-full flex flex-row justify-start items-center"
>
<
UserAvatar
className=
"!w-10 !h-10 mr-2"
avatarUrl=
{
creator
?.
avatarUrl
}
/>
<
div
className=
"flex flex-col justify-start items-start gap-1"
>
<
span
className=
"text-lg leading-none text-gray-600 max-w-[8em] truncate dark:text-gray-400"
>
{
creator
?.
nickname
}
</
span
>
<
span
className=
"text-sm leading-none text-gray-400 select-none"
>
{
getDateTimeString
(
memo
.
displayTime
)
}
</
span
>
</
div
>
</
span
>
{
parentMemo
&&
(
<
div
className=
"w-auto inline-block mb-2"
>
<
Link
className=
"px-3 py-1 border rounded-lg max-w-xs w-auto text-sm flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-400 dark:border-gray-500 hover:shadow hover:opacity-80"
to=
{
`/m/${parentMemo.name}`
}
unstable_viewTransition
>
<
Icon
.
ArrowUpLeftFromCircle
className=
"w-4 h-auto shrink-0 opacity-60 mr-2"
/>
<
span
className=
"truncate"
>
{
parentMemo
.
content
}
</
span
>
</
Link
>
</
div
>
{
parentMemo
&&
(
<
div
className=
"w-auto mb-2"
>
<
Link
className=
"px-3 py-1 border rounded-lg max-w-xs w-auto text-sm flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-400 dark:border-gray-500 hover:shadow hover:opacity-80"
to=
{
`/m/${parentMemo.name}`
}
unstable_viewTransition
>
<
Icon
.
ArrowUpLeftFromCircle
className=
"w-4 h-auto shrink-0 opacity-60 mr-2"
/>
<
span
className=
"truncate"
>
{
parentMemo
.
content
}
</
span
>
</
Link
>
</
div
>
)
}
<
MemoContent
key=
{
`${memo.id}-${memo.updateTime}`
}
memoId=
{
memo
.
id
}
content=
{
memo
.
content
}
readonly=
{
readonly
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
<
MemoRelationListView
memo=
{
memo
}
relations=
{
referenceRelations
}
/>
<
div
className=
"w-full mt-3 select-none flex flex-row justify-between items-center gap-2"
>
<
div
className=
"flex flex-row justify-start items-center"
>
{
!
readonly
&&
(
<
Select
className=
"w-auto text-sm"
variant=
"plain"
value=
{
memo
.
visibility
}
startDecorator=
{
<
VisibilityIcon
visibility=
{
memo
.
visibility
}
/>
}
onChange=
{
(
_
,
visibility
)
=>
{
if
(
visibility
)
{
handleMemoVisibilityOptionChanged
(
visibility
);
}
}
}
>
{
[
Visibility
.
PRIVATE
,
Visibility
.
PROTECTED
,
Visibility
.
PUBLIC
].
map
((
item
)
=>
(
<
Option
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
{
t
(
`memo.visibility.${convertVisibilityToString(item).toLowerCase()}`
as
any
)
}
</
Option
>
))
}
</
Select
>
)
}
</
div
>
<
div
className=
"flex flex-row sm:justify-end items-center"
>
{
!
readonly
&&
(
<
Tooltip
title=
{
"Edit"
}
placement=
"top"
>
<
IconButton
size=
"sm"
onClick=
{
handleEditMemoClick
}
>
<
Icon
.
Edit3
className=
"w-4 h-auto text-gray-600 dark:text-gray-400"
/>
</
IconButton
>
</
Tooltip
>
)
}
<
Tooltip
title=
{
"Copy link"
}
placement=
"top"
>
<
IconButton
size=
"sm"
onClick=
{
handleCopyLinkBtnClick
}
>
<
Icon
.
Link
className=
"w-4 h-auto text-gray-600 dark:text-gray-400"
/>
</
IconButton
>
</
Tooltip
>
<
Tooltip
title=
{
"Share"
}
placement=
"top"
>
<
IconButton
size=
"sm"
onClick=
{
()
=>
showShareMemoDialog
(
memo
.
id
)
}
>
<
Icon
.
Share
className=
"w-4 h-auto text-gray-600 dark:text-gray-400"
/>
</
IconButton
>
</
Tooltip
>
{
!
readonly
&&
(
<
MemoActionMenu
className=
"ml-1"
memo=
{
memo
}
hiddenActions=
{
[
"pin"
,
"edit"
,
"share"
]
}
onArchived=
{
handleMemoArchived
}
onDeleted=
{
handleMemoDeleted
}
/>
)
}
</
div
>
</
div
>
</
div
>
)
}
<
MemoView
key=
{
`${memo.id}-${memo.displayTime}`
}
className=
"shadow hover:shadow-xl transition-all"
memo=
{
memo
}
/>
<
div
className=
"pt-8 pb-16 w-full"
>
<
h2
id=
"comments"
className=
"sr-only"
>
Comments
...
...
@@ -225,7 +100,7 @@ const MemoDetail = () => {
<
span
className=
"text-gray-400 text-sm ml-0.5"
>
(
{
comments
.
length
}
)
</
span
>
</
div
>
{
comments
.
map
((
comment
)
=>
(
<
MemoView
key=
{
`${memo.id}-${memo.displayTime}`
}
memo=
{
comment
}
showCreator
/>
<
MemoView
key=
{
`${memo.id}-${memo.displayTime}`
}
memo=
{
comment
}
/>
))
}
</>
)
}
...
...
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