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
90679cc3
Commit
90679cc3
authored
Mar 28, 2024
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: add explore sidebar
parent
192ee7ac
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
210 additions
and
32 deletions
+210
-32
user_service.proto
proto/api/v2/user_service.proto
+2
-0
README.md
proto/gen/api/v2/README.md
+1
-1
user_service.pb.go
proto/gen/api/v2/user_service.pb.go
+2
-0
apidocs.swagger.yaml
server/route/api/v2/apidocs.swagger.yaml
+3
-0
user_service.go
server/route/api/v2/user_service.go
+16
-0
user.go
store/db/sqlite/user.go
+12
-3
user.go
store/user.go
+6
-0
ExploreSidebar.tsx
web/src/components/ExploreSidebar/ExploreSidebar.tsx
+23
-0
ExploreSidebarDrawer.tsx
web/src/components/ExploreSidebar/ExploreSidebarDrawer.tsx
+37
-0
index.ts
web/src/components/ExploreSidebar/index.ts
+4
-0
HomeSidebar.tsx
web/src/components/HomeSidebar/HomeSidebar.tsx
+3
-3
HomeSidebarDrawer.tsx
web/src/components/HomeSidebar/HomeSidebarDrawer.tsx
+1
-1
index.ts
web/src/components/HomeSidebar/index.ts
+4
-0
UserList.tsx
web/src/components/UserList.tsx
+47
-0
Explore.tsx
web/src/pages/Explore.tsx
+39
-22
Home.tsx
web/src/pages/Home.tsx
+1
-2
user.ts
web/src/store/v1/user.ts
+9
-0
No files found.
proto/api/v2/user_service.proto
View file @
90679cc3
...
...
@@ -131,6 +131,8 @@ message ListUsersResponse {
}
message
SearchUsersRequest
{
// Filter is used to filter users returned in the list.
// Format: "username == frank"
string
filter
=
1
;
}
...
...
proto/gen/api/v2/README.md
View file @
90679cc3
...
...
@@ -617,7 +617,7 @@ Used internally for obfuscating the page token.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| filter |
[
string
](
#string
)
| | |
| filter |
[
string
](
#string
)
| |
Filter is used to filter users returned in the list. Format:
"
username == frank
"
|
...
...
proto/gen/api/v2/user_service.pb.go
View file @
90679cc3
...
...
@@ -303,6 +303,8 @@ type SearchUsersRequest struct {
sizeCache
protoimpl
.
SizeCache
unknownFields
protoimpl
.
UnknownFields
// Filter is used to filter users returned in the list.
// Format: "username == frank"
Filter
string
`protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"`
}
...
...
server/route/api/v2/apidocs.swagger.yaml
View file @
90679cc3
...
...
@@ -562,6 +562,9 @@ paths:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
filter
description
:
|-
Filter is used to filter users returned in the list.
Format: "username == frank"
in: query
required: false
type: string
...
...
server/route/api/v2/user_service.go
View file @
90679cc3
...
...
@@ -60,6 +60,12 @@ func (s *APIV2Service) SearchUsers(ctx context.Context, request *apiv2pb.SearchU
if
filter
.
Username
!=
nil
{
userFind
.
Username
=
filter
.
Username
}
if
filter
.
Random
{
userFind
.
Random
=
true
}
if
filter
.
Limit
!=
nil
{
userFind
.
Limit
=
filter
.
Limit
}
users
,
err
:=
s
.
Store
.
ListUsers
(
ctx
,
userFind
)
if
err
!=
nil
{
...
...
@@ -540,10 +546,14 @@ func convertUserRoleToStore(role apiv2pb.User_Role) store.Role {
// SearchUsersFilterCELAttributes are the CEL attributes for SearchUsersFilter.
var
SearchUsersFilterCELAttributes
=
[]
cel
.
EnvOption
{
cel
.
Variable
(
"username"
,
cel
.
StringType
),
cel
.
Variable
(
"random"
,
cel
.
BoolType
),
cel
.
Variable
(
"limit"
,
cel
.
IntType
),
}
type
SearchUsersFilter
struct
{
Username
*
string
Random
bool
Limit
*
int
}
func
parseSearchUsersFilter
(
expression
string
)
(
*
SearchUsersFilter
,
error
)
{
...
...
@@ -572,6 +582,12 @@ func findSearchUsersField(callExpr *expr.Expr_Call, filter *SearchUsersFilter) {
if
idExpr
.
Name
==
"username"
{
username
:=
callExpr
.
Args
[
1
]
.
GetConstExpr
()
.
GetStringValue
()
filter
.
Username
=
&
username
}
else
if
idExpr
.
Name
==
"random"
{
random
:=
callExpr
.
Args
[
1
]
.
GetConstExpr
()
.
GetBoolValue
()
filter
.
Random
=
random
}
else
if
idExpr
.
Name
==
"limit"
{
limit
:=
int
(
callExpr
.
Args
[
1
]
.
GetConstExpr
()
.
GetInt64Value
())
filter
.
Limit
=
&
limit
}
return
}
...
...
store/db/sqlite/user.go
View file @
90679cc3
...
...
@@ -2,6 +2,8 @@ package sqlite
import
(
"context"
"fmt"
"slices"
"strings"
"github.com/usememos/memos/store"
...
...
@@ -99,6 +101,11 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
where
,
args
=
append
(
where
,
"nickname = ?"
),
append
(
args
,
*
v
)
}
orderBy
:=
[]
string
{
"created_ts DESC"
,
"row_status DESC"
}
if
find
.
Random
{
orderBy
=
slices
.
Concat
([]
string
{
"RANDOM()"
},
orderBy
)
}
query
:=
`
SELECT
id,
...
...
@@ -113,9 +120,11 @@ func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User
updated_ts,
row_status
FROM user
WHERE `
+
strings
.
Join
(
where
,
" AND "
)
+
`
ORDER BY created_ts DESC, row_status DESC
`
WHERE `
+
strings
.
Join
(
where
,
" AND "
)
+
` ORDER BY `
+
strings
.
Join
(
orderBy
,
", "
)
if
v
:=
find
.
Limit
;
v
!=
nil
{
query
+=
fmt
.
Sprintf
(
" LIMIT %d"
,
*
v
)
}
rows
,
err
:=
d
.
db
.
QueryContext
(
ctx
,
query
,
args
...
)
if
err
!=
nil
{
return
nil
,
err
...
...
store/user.go
View file @
90679cc3
...
...
@@ -82,6 +82,12 @@ type FindUser struct {
Role
*
Role
Email
*
string
Nickname
*
string
// Random and limit are used in list users.
// Whether to return random users.
Random
bool
// The maximum number of users to return.
Limit
*
int
}
type
DeleteUser
struct
{
...
...
web/src/components/ExploreSidebar/ExploreSidebar.tsx
0 → 100644
View file @
90679cc3
import
classNames
from
"classnames"
;
import
SearchBar
from
"@/components/SearchBar"
;
import
UserList
from
"../UserList"
;
interface
Props
{
className
?:
string
;
}
const
ExploreSidebar
=
(
props
:
Props
)
=>
{
return
(
<
aside
className=
{
classNames
(
"relative w-full h-auto max-h-screen overflow-auto hide-scrollbar flex flex-col justify-start items-start"
,
props
.
className
,
)
}
>
<
SearchBar
/>
<
UserList
/>
</
aside
>
);
};
export
default
ExploreSidebar
;
web/src/components/ExploreSidebar/ExploreSidebarDrawer.tsx
0 → 100644
View file @
90679cc3
import
{
Drawer
,
IconButton
}
from
"@mui/joy"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
Icon
from
"../Icon"
;
import
ExploreSidebar
from
"./ExploreSidebar"
;
const
ExploreSidebarDrawer
=
()
=>
{
const
location
=
useLocation
();
const
[
open
,
setOpen
]
=
useState
(
false
);
useEffect
(()
=>
{
setOpen
(
false
);
},
[
location
.
pathname
]);
const
toggleDrawer
=
(
inOpen
:
boolean
)
=>
(
event
:
React
.
KeyboardEvent
|
React
.
MouseEvent
)
=>
{
if
(
event
.
type
===
"keydown"
&&
((
event
as
React
.
KeyboardEvent
).
key
===
"Tab"
||
(
event
as
React
.
KeyboardEvent
).
key
===
"Shift"
))
{
return
;
}
setOpen
(
inOpen
);
};
return
(
<>
<
IconButton
onClick=
{
toggleDrawer
(
true
)
}
>
<
Icon
.
Search
className=
"w-5 h-auto dark:text-gray-400"
/>
</
IconButton
>
<
Drawer
anchor=
"right"
size=
"sm"
open=
{
open
}
onClose=
{
toggleDrawer
(
false
)
}
>
<
div
className=
"w-full h-full px-5 bg-zinc-100 dark:bg-zinc-900"
>
<
ExploreSidebar
className=
"py-4"
/>
</
div
>
</
Drawer
>
</>
);
};
export
default
ExploreSidebarDrawer
;
web/src/components/ExploreSidebar/index.ts
0 → 100644
View file @
90679cc3
import
ExploreSidebar
from
"./ExploreSidebar"
;
import
ExploreSidebarDrawer
from
"./ExploreSidebarDrawer"
;
export
{
ExploreSidebar
,
ExploreSidebarDrawer
};
web/src/components/HomeSidebar.tsx
→
web/src/components/HomeSidebar
/HomeSidebar
.tsx
View file @
90679cc3
import
classNames
from
"classnames"
;
import
PersonalStatistics
from
"@/components/PersonalStatistics"
;
import
SearchBar
from
"@/components/SearchBar"
;
import
TagList
from
"@/components/TagList"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
PersonalStatistics
from
"./PersonalStatistics"
;
import
SearchBar
from
"./SearchBar"
;
import
TagList
from
"./TagList"
;
interface
Props
{
className
?:
string
;
...
...
web/src/components/HomeSidebarDrawer.tsx
→
web/src/components/HomeSidebar
/HomeSidebar
Drawer.tsx
View file @
90679cc3
import
{
Drawer
,
IconButton
}
from
"@mui/joy"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
Icon
from
"../Icon"
;
import
HomeSidebar
from
"./HomeSidebar"
;
import
Icon
from
"./Icon"
;
const
HomeSidebarDrawer
=
()
=>
{
const
location
=
useLocation
();
...
...
web/src/components/HomeSidebar/index.ts
0 → 100644
View file @
90679cc3
import
HomeSidebar
from
"./HomeSidebar"
;
import
HomeSidebarDrawer
from
"./HomeSidebarDrawer"
;
export
{
HomeSidebar
,
HomeSidebarDrawer
};
web/src/components/UserList.tsx
0 → 100644
View file @
90679cc3
import
{
IconButton
}
from
"@mui/joy"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
useUserStore
}
from
"@/store/v1"
;
import
{
User
}
from
"@/types/proto/api/v2/user_service"
;
import
Icon
from
"./Icon"
;
import
UserAvatar
from
"./UserAvatar"
;
const
UserList
=
()
=>
{
const
userStore
=
useUserStore
();
const
[
users
,
setUsers
]
=
useState
<
User
[]
>
([]);
const
fetchRecommendUsers
=
async
()
=>
{
const
users
=
await
userStore
.
searchUsers
(
`random == true && limit == 5`
);
setUsers
(
users
);
};
useEffect
(()
=>
{
fetchRecommendUsers
();
},
[]);
return
(
<
div
className=
"w-full mt-2 flex flex-col p-2 bg-gray-50 dark:bg-black rounded-lg"
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-gray-400 font-medium text-sm pl-1"
>
Users
</
span
>
<
IconButton
size=
"sm"
onClick=
{
fetchRecommendUsers
}
>
<
Icon
.
RefreshCcw
className=
"text-gray-400 w-4 h-auto"
/>
</
IconButton
>
</
div
>
{
users
.
map
((
user
)
=>
(
<
div
key=
{
user
.
name
}
className=
"w-full flex flex-row justify-start items-center px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-lg"
>
<
Link
className=
"w-full flex flex-row items-center"
to=
{
`/u/${encodeURIComponent(user.username)}`
}
unstable_viewTransition
>
<
UserAvatar
className=
"mr-2 shrink-0"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
div
className=
"w-full flex flex-col justify-center items-start"
>
<
span
className=
"text-gray-600 leading-tight max-w-[80%] truncate dark:text-gray-400"
>
{
user
.
nickname
||
user
.
username
}
</
span
>
</
div
>
</
Link
>
</
div
>
))
}
</
div
>
);
};
export
default
UserList
;
web/src/pages/Explore.tsx
View file @
90679cc3
import
{
Button
}
from
"@mui/joy"
;
import
classNames
from
"classnames"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
Empty
from
"@/components/Empty"
;
import
{
ExploreSidebar
,
ExploreSidebarDrawer
}
from
"@/components/ExploreSidebar"
;
import
Icon
from
"@/components/Icon"
;
import
MemoFilter
from
"@/components/MemoFilter"
;
import
MemoView
from
"@/components/MemoView"
;
...
...
@@ -9,11 +11,13 @@ import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import
{
getTimeStampByDate
}
from
"@/helpers/datetime"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useFilterWithUrlParams
from
"@/hooks/useFilterWithUrlParams"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
useMemoList
,
useMemoStore
}
from
"@/store/v1"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
const
Explore
=
()
=>
{
const
t
=
useTranslate
();
const
{
md
}
=
useResponsiveWidth
();
const
user
=
useCurrentUser
();
const
memoStore
=
useMemoStore
();
const
memoList
=
useMemoList
();
...
...
@@ -52,29 +56,42 @@ const Explore = () => {
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=
"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.name}-${memo.displayTime}`
}
memo=
{
memo
}
/>
))
}
{
isRequesting
?
(
<
div
className=
"flex flex-row justify-center items-center w-full my-4 text-gray-400"
>
<
Icon
.
Loader
className=
"w-4 h-auto animate-spin mr-1"
/>
<
p
className=
"text-sm italic"
>
{
t
(
"memo.fetching-data"
)
}
</
p
>
{
!
md
&&
(
<
MobileHeader
>
<
ExploreSidebarDrawer
/>
</
MobileHeader
>
)
}
<
div
className=
{
classNames
(
"w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4"
)
}
>
<
div
className=
{
classNames
(
md
?
"w-[calc(100%-15rem)]"
:
"w-full"
)
}
>
<
div
className=
"flex flex-col justify-start items-start w-full max-w-full"
>
<
MemoFilter
className=
"px-2 pb-2"
/>
{
sortedMemos
.
map
((
memo
)
=>
(
<
MemoView
key=
{
`${memo.name}-${memo.updateTime}`
}
memo=
{
memo
}
showVisibility
showPinned
/>
))
}
{
isRequesting
?
(
<
div
className=
"flex flex-row justify-center items-center w-full my-4 text-gray-400"
>
<
Icon
.
Loader
className=
"w-4 h-auto animate-spin mr-1"
/>
<
p
className=
"text-sm italic"
>
{
t
(
"memo.fetching-data"
)
}
</
p
>
</
div
>
)
:
!
nextPageTokenRef
.
current
?
(
sortedMemos
.
length
===
0
&&
(
<
div
className=
"w-full mt-12 mb-8 flex flex-col justify-center items-center italic"
>
<
Empty
/>
<
p
className=
"mt-2 text-gray-600 dark:text-gray-400"
>
{
t
(
"message.no-data"
)
}
</
p
>
</
div
>
)
)
:
(
<
div
className=
"w-full flex flex-row justify-center items-center my-4"
>
<
Button
variant=
"plain"
endDecorator=
{
<
Icon
.
ArrowDown
className=
"w-5 h-auto"
/>
}
onClick=
{
fetchMemos
}
>
{
t
(
"memo.fetch-more"
)
}
</
Button
>
</
div
>
)
}
</
div
>
)
:
!
nextPageTokenRef
.
current
?
(
sortedMemos
.
length
===
0
&&
(
<
div
className=
"w-full mt-12 mb-8 flex flex-col justify-center items-center italic"
>
<
Empty
/>
<
p
className=
"mt-2 text-gray-600 dark:text-gray-400"
>
{
t
(
"message.no-data"
)
}
</
p
>
</
div
>
)
)
:
(
<
div
className=
"w-full flex flex-row justify-center items-center my-4"
>
<
Button
variant=
"plain"
endDecorator=
{
<
Icon
.
ArrowDown
className=
"w-5 h-auto"
/>
}
onClick=
{
fetchMemos
}
>
{
t
(
"memo.fetch-more"
)
}
</
Button
>
</
div
>
{
md
&&
(
<
div
className=
"sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full"
>
<
ExploreSidebar
className=
"py-6"
/>
</
div
>
)
}
</
div
>
...
...
web/src/pages/Home.tsx
View file @
90679cc3
...
...
@@ -2,8 +2,7 @@ import { Button } from "@mui/joy";
import
classNames
from
"classnames"
;
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
"react"
;
import
Empty
from
"@/components/Empty"
;
import
HomeSidebar
from
"@/components/HomeSidebar"
;
import
HomeSidebarDrawer
from
"@/components/HomeSidebarDrawer"
;
import
{
HomeSidebar
,
HomeSidebarDrawer
}
from
"@/components/HomeSidebar"
;
import
Icon
from
"@/components/Icon"
;
import
MemoEditor
from
"@/components/MemoEditor"
;
import
showMemoEditorDialog
from
"@/components/MemoEditor/MemoEditorDialog"
;
...
...
web/src/store/v1/user.ts
View file @
90679cc3
...
...
@@ -62,6 +62,15 @@ export const useUserStore = create(
set
({
userMapByName
:
userMap
});
return
user
;
},
listUsers
:
async
()
=>
{
const
{
users
}
=
await
userServiceClient
.
listUsers
({});
const
userMap
=
get
().
userMapByName
;
for
(
const
user
of
users
)
{
userMap
[
user
.
name
]
=
user
;
}
set
({
userMapByName
:
userMap
});
return
users
;
},
searchUsers
:
async
(
filter
:
string
)
=>
{
const
{
users
}
=
await
userServiceClient
.
searchUsers
({
filter
,
...
...
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