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
533591af
Commit
533591af
authored
Jul 08, 2025
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: theme in user setting
parent
f9076197
Changes
13
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
128 additions
and
30 deletions
+128
-30
user_service.proto
proto/api/v1/user_service.proto
+5
-0
user_service.pb.go
proto/gen/api/v1/user_service.pb.go
+16
-4
apidocs.swagger.yaml
proto/gen/apidocs.swagger.yaml
+12
-0
user_setting.pb.go
proto/gen/store/user_setting.pb.go
+15
-4
user_setting.proto
proto/store/user_setting.proto
+3
-0
user_service.go
server/router/api/v1/user_service.go
+30
-9
workspace_service.go
server/router/api/v1/workspace_service.go
+7
-1
App.tsx
web/src/App.tsx
+5
-3
AppearanceSelect.tsx
web/src/components/AppearanceSelect.tsx
+2
-3
LocaleSelect.tsx
web/src/components/LocaleSelect.tsx
+2
-3
PreferencesSection.tsx
web/src/components/Settings/PreferencesSection.tsx
+10
-0
UpdateCustomizedProfileDialog.tsx
web/src/components/UpdateCustomizedProfileDialog.tsx
+2
-2
user_service.ts
web/src/types/proto/api/v1/user_service.ts
+19
-1
No files found.
proto/api/v1/user_service.proto
View file @
533591af
...
...
@@ -369,6 +369,11 @@ message UserSetting {
// The default visibility of the memo.
string
memo_visibility
=
4
[(
google.api.field_behavior
)
=
OPTIONAL
];
// The preferred theme of the user.
// This references a CSS file in the web/public/themes/ directory.
// If not set, the default theme will be used.
string
theme
=
5
[(
google.api.field_behavior
)
=
OPTIONAL
];
}
message
GetUserSettingRequest
{
...
...
proto/gen/api/v1/user_service.pb.go
View file @
533591af
...
...
@@ -943,6 +943,10 @@ type UserSetting struct {
Appearance
string
`protobuf:"bytes,3,opt,name=appearance,proto3" json:"appearance,omitempty"`
// The default visibility of the memo.
MemoVisibility
string
`protobuf:"bytes,4,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
// The preferred theme of the user.
// This references a CSS file in the web/public/themes/ directory.
// If not set, the default theme will be used.
Theme
string
`protobuf:"bytes,5,opt,name=theme,proto3" json:"theme,omitempty"`
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
}
...
...
@@ -1005,6 +1009,13 @@ func (x *UserSetting) GetMemoVisibility() string {
return
""
}
func
(
x
*
UserSetting
)
GetTheme
()
string
{
if
x
!=
nil
{
return
x
.
Theme
}
return
""
}
type
GetUserSettingRequest
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
// Required. The resource name of the user.
...
...
@@ -2005,14 +2016,15 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"
\x16
memos.api.v1/UserStats
\x12\f
users/{user}*
\t
userStats2
\t
userStats
\"
D
\n
"
+
"
\x13
GetUserStatsRequest
\x12
-
\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x19\xe0
A
\x02\xfa
A
\x13\n
"
+
"
\x11
memos.api.v1/UserR
\x04
name
\"\x
de
\x01\n
"
+
"
\x11
memos.api.v1/UserR
\x04
name
\"\x
f9
\x01\n
"
+
"
\v
UserSetting
\x12\x17\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x03\xe0
A
\b
R
\x04
name
\x12\x1b\n
"
+
"
\x06
locale
\x18\x02
\x01
(
\t
B
\x03\xe0
A
\x01
R
\x06
locale
\x12
#
\n
"
+
"
\n
"
+
"appearance
\x18\x03
\x01
(
\t
B
\x03\xe0
A
\x01
R
\n
"
+
"appearance
\x12
,
\n
"
+
"
\x0f
memo_visibility
\x18\x04
\x01
(
\t
B
\x03\xe0
A
\x01
R
\x0e
memoVisibility:F
\xea
AC
\n
"
+
"
\x0f
memo_visibility
\x18\x04
\x01
(
\t
B
\x03\xe0
A
\x01
R
\x0e
memoVisibility
\x12\x19\n
"
+
"
\x05
theme
\x18\x05
\x01
(
\t
B
\x03\xe0
A
\x01
R
\x05
theme:F
\xea
AC
\n
"
+
"
\x18
memos.api.v1/UserSetting
\x12\f
users/{user}*
\f
userSettings2
\v
userSetting
\"
F
\n
"
+
"
\x15
GetUserSettingRequest
\x12
-
\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x19\xe0
A
\x02\xfa
A
\x13\n
"
+
...
...
proto/gen/apidocs.swagger.yaml
View file @
533591af
...
...
@@ -2218,6 +2218,12 @@ paths:
memoVisibility
:
type
:
string
description
:
The default visibility of the memo.
theme
:
type
:
string
description
:
|-
The preferred theme of the user.
This references a CSS file in the web/public/themes/ directory.
If not set, the default theme will be used.
title
:
Required. The user setting to update.
required
:
-
setting
...
...
@@ -2866,6 +2872,12 @@ definitions:
memoVisibility
:
type
:
string
description
:
The default visibility of the memo.
theme
:
type
:
string
description
:
|-
The preferred theme of the user.
This references a CSS file in the web/public/themes/ directory.
If not set, the default theme will be used.
title
:
User settings message
apiv1Webhook
:
type
:
object
...
...
proto/gen/store/user_setting.pb.go
View file @
533591af
...
...
@@ -239,6 +239,9 @@ type GeneralUserSetting struct {
Appearance
string
`protobuf:"bytes,2,opt,name=appearance,proto3" json:"appearance,omitempty"`
// The user's memo visibility setting.
MemoVisibility
string
`protobuf:"bytes,3,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
// The user's theme preference.
// This references a CSS file in the web/public/themes/ directory.
Theme
string
`protobuf:"bytes,4,opt,name=theme,proto3" json:"theme,omitempty"`
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
}
...
...
@@ -294,6 +297,13 @@ func (x *GeneralUserSetting) GetMemoVisibility() string {
return
""
}
func
(
x
*
GeneralUserSetting
)
GetTheme
()
string
{
if
x
!=
nil
{
return
x
.
Theme
}
return
""
}
type
SessionsUserSetting
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
Sessions
[]
*
SessionsUserSetting_Session
`protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"`
...
...
@@ -822,13 +832,14 @@ const file_store_user_setting_proto_rawDesc = "" +
"
\r
ACCESS_TOKENS
\x10\x03\x12\r\n
"
+
"
\t
SHORTCUTS
\x10\x04\x12\f\n
"
+
"
\b
WEBHOOKS
\x10\x05
B
\a\n
"
+
"
\x05
value
\"
u
\n
"
+
"
\x05
value
\"
\x8b\x01
\n
"
+
"
\x12
GeneralUserSetting
\x12\x16\n
"
+
"
\x06
locale
\x18\x01
\x01
(
\t
R
\x06
locale
\x12\x1e\n
"
+
"
\n
"
+
"appearance
\x18\x02
\x01
(
\t
R
\n
"
+
"appearance
\x12
'
\n
"
+
"
\x0f
memo_visibility
\x18\x03
\x01
(
\t
R
\x0e
memoVisibility
\"\xf3\x03\n
"
+
"
\x0f
memo_visibility
\x18\x03
\x01
(
\t
R
\x0e
memoVisibility
\x12\x14\n
"
+
"
\x05
theme
\x18\x04
\x01
(
\t
R
\x05
theme
\"\xf3\x03\n
"
+
"
\x13
SessionsUserSetting
\x12
D
\n
"
+
"
\b
sessions
\x18\x01
\x03
(
\v
2(.memos.store.SessionsUserSetting.SessionR
\b
sessions
\x1a\xfd\x01\n
"
+
"
\a
Session
\x12\x1d\n
"
+
...
...
proto/store/user_setting.proto
View file @
533591af
...
...
@@ -40,6 +40,9 @@ message GeneralUserSetting {
string
appearance
=
2
;
// The user's memo visibility setting.
string
memo_visibility
=
3
;
// The user's theme preference.
// This references a CSS file in the web/public/themes/ directory.
string
theme
=
4
;
}
message
SessionsUserSetting
{
...
...
server/router/api/v1/user_service.go
View file @
533591af
...
...
@@ -249,7 +249,8 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get workspace general setting: %v"
,
err
)
}
for
_
,
field
:=
range
request
.
UpdateMask
.
Paths
{
if
field
==
"username"
{
switch
field
{
case
"username"
:
if
workspaceGeneralSetting
.
DisallowChangeUsername
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied: disallow change username"
)
}
...
...
@@ -257,35 +258,35 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid username: %s"
,
request
.
User
.
Username
)
}
update
.
Username
=
&
request
.
User
.
Username
}
else
if
field
==
"display_name"
{
case
"display_name"
:
if
workspaceGeneralSetting
.
DisallowChangeNickname
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied: disallow change nickname"
)
}
update
.
Nickname
=
&
request
.
User
.
DisplayName
}
else
if
field
==
"email"
{
case
"email"
:
update
.
Email
=
&
request
.
User
.
Email
}
else
if
field
==
"avatar_url"
{
case
"avatar_url"
:
update
.
AvatarURL
=
&
request
.
User
.
AvatarUrl
}
else
if
field
==
"description"
{
case
"description"
:
update
.
Description
=
&
request
.
User
.
Description
}
else
if
field
==
"role"
{
case
"role"
:
// Only allow admin to update role.
if
currentUser
.
Role
!=
store
.
RoleAdmin
&&
currentUser
.
Role
!=
store
.
RoleHost
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
role
:=
convertUserRoleToStore
(
request
.
User
.
Role
)
update
.
Role
=
&
role
}
else
if
field
==
"password"
{
case
"password"
:
passwordHash
,
err
:=
bcrypt
.
GenerateFromPassword
([]
byte
(
request
.
User
.
Password
),
bcrypt
.
DefaultCost
)
if
err
!=
nil
{
return
nil
,
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"failed to generate password hash"
)
.
SetInternal
(
err
)
}
passwordHashStr
:=
string
(
passwordHash
)
update
.
PasswordHash
=
&
passwordHashStr
}
else
if
field
==
"state"
{
case
"state"
:
rowStatus
:=
convertStateToStore
(
request
.
User
.
State
)
update
.
RowStatus
=
&
rowStatus
}
else
{
default
:
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid update path: %s"
,
field
)
}
}
...
...
@@ -334,6 +335,7 @@ func getDefaultUserSetting() *v1pb.UserSetting {
Locale
:
"en"
,
Appearance
:
"system"
,
MemoVisibility
:
"PRIVATE"
,
Theme
:
""
,
}
}
...
...
@@ -370,9 +372,24 @@ func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUser
userSettingMessage
.
Locale
=
general
.
Locale
userSettingMessage
.
Appearance
=
general
.
Appearance
userSettingMessage
.
MemoVisibility
=
general
.
MemoVisibility
userSettingMessage
.
Theme
=
general
.
Theme
}
}
}
// Backfill theme if empty: use workspace theme or default to "default"
if
userSettingMessage
.
Theme
==
""
{
workspaceGeneralSetting
,
err
:=
s
.
Store
.
GetWorkspaceGeneralSetting
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get workspace general setting: %v"
,
err
)
}
workspaceTheme
:=
workspaceGeneralSetting
.
Theme
if
workspaceTheme
==
""
{
workspaceTheme
=
"default"
}
userSettingMessage
.
Theme
=
workspaceTheme
}
return
userSettingMessage
,
nil
}
...
...
@@ -411,6 +428,7 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
Locale
:
"en"
,
Appearance
:
"system"
,
MemoVisibility
:
"PRIVATE"
,
Theme
:
""
,
}
// If there's an existing setting, use its values as defaults
...
...
@@ -419,6 +437,7 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
generalSetting
.
Locale
=
existing
.
Locale
generalSetting
.
Appearance
=
existing
.
Appearance
generalSetting
.
MemoVisibility
=
existing
.
MemoVisibility
generalSetting
.
Theme
=
existing
.
Theme
}
// Apply updates based on the update mask
...
...
@@ -430,6 +449,8 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
generalSetting
.
Appearance
=
request
.
Setting
.
Appearance
case
"memo_visibility"
:
generalSetting
.
MemoVisibility
=
request
.
Setting
.
MemoVisibility
case
"theme"
:
generalSetting
.
Theme
=
request
.
Setting
.
Theme
default
:
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid update path: %s"
,
field
)
}
...
...
server/router/api/v1/workspace_service.go
View file @
533591af
...
...
@@ -149,8 +149,14 @@ func convertWorkspaceGeneralSettingFromStore(setting *storepb.WorkspaceGeneralSe
if
setting
==
nil
{
return
nil
}
// Backfill theme if empty
theme
:=
setting
.
Theme
if
theme
==
""
{
theme
=
"default"
}
generalSetting
:=
&
v1pb
.
WorkspaceGeneralSetting
{
Theme
:
setting
.
T
heme
,
Theme
:
t
heme
,
DisallowUserRegistration
:
setting
.
DisallowUserRegistration
,
DisallowPasswordAuth
:
setting
.
DisallowPasswordAuth
,
AdditionalScript
:
setting
.
AdditionalScript
,
...
...
web/src/App.tsx
View file @
533591af
...
...
@@ -104,10 +104,12 @@ const App = observer(() => {
});
},
[
userSetting
?.
locale
,
userSetting
?.
appearance
]);
// Load theme when
workspace setting changes, validate API response
// Load theme when
user setting changes (user theme is already backfilled with workspace theme)
useEffect
(()
=>
{
loadTheme
(
workspaceGeneralSetting
.
theme
);
},
[
workspaceGeneralSetting
.
theme
]);
if
(
userSetting
?.
theme
)
{
loadTheme
(
userSetting
.
theme
);
}
},
[
userSetting
?.
theme
]);
return
<
Outlet
/>;
});
...
...
web/src/components/AppearanceSelect.tsx
View file @
533591af
...
...
@@ -6,13 +6,12 @@ import { useTranslate } from "@/utils/i18n";
interface
Props
{
value
:
Appearance
;
onChange
:
(
appearance
:
Appearance
)
=>
void
;
className
?:
string
;
}
const
appearanceList
=
[
"system"
,
"light"
,
"dark"
]
as
const
;
const
AppearanceSelect
:
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
onChange
,
value
,
className
}
=
props
;
const
{
onChange
,
value
}
=
props
;
const
t
=
useTranslate
();
const
getPrefixIcon
=
(
appearance
:
Appearance
)
=>
{
...
...
@@ -32,7 +31,7 @@ const AppearanceSelect: FC<Props> = (props: Props) => {
return
(
<
Select
value=
{
value
}
onValueChange=
{
handleSelectChange
}
>
<
SelectTrigger
className=
{
`min-w-40 w-auto whitespace-nowrap ${className ?? ""}`
}
>
<
SelectTrigger
>
<
SelectValue
placeholder=
"Select appearance"
/>
</
SelectTrigger
>
<
SelectContent
>
...
...
web/src/components/LocaleSelect.tsx
View file @
533591af
...
...
@@ -5,12 +5,11 @@ import { locales } from "@/i18n";
interface
Props
{
value
:
Locale
;
className
?:
string
;
onChange
:
(
locale
:
Locale
)
=>
void
;
}
const
LocaleSelect
:
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
onChange
,
value
,
className
}
=
props
;
const
{
onChange
,
value
}
=
props
;
const
handleSelectChange
=
async
(
locale
:
Locale
)
=>
{
onChange
(
locale
);
...
...
@@ -18,7 +17,7 @@ const LocaleSelect: FC<Props> = (props: Props) => {
return
(
<
Select
value=
{
value
}
onValueChange=
{
handleSelectChange
}
>
<
SelectTrigger
className=
{
`min-w-40 w-auto whitespace-nowrap ${className ?? ""}`
}
>
<
SelectTrigger
>
<
div
className=
"flex items-center gap-2"
>
<
GlobeIcon
className=
"w-4 h-auto"
/>
<
SelectValue
placeholder=
"Select language"
/>
...
...
web/src/components/Settings/PreferencesSection.tsx
View file @
533591af
...
...
@@ -8,6 +8,7 @@ import { useTranslate } from "@/utils/i18n";
import
{
convertVisibilityFromString
,
convertVisibilityToString
}
from
"@/utils/memo"
;
import
AppearanceSelect
from
"../AppearanceSelect"
;
import
LocaleSelect
from
"../LocaleSelect"
;
import
ThemeSelector
from
"../ThemeSelector"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
WebhookSection
from
"./WebhookSection"
;
...
...
@@ -27,6 +28,10 @@ const PreferencesSection = observer(() => {
await
userStore
.
updateUserSetting
({
memoVisibility
:
value
},
[
"memo_visibility"
]);
};
const
handleThemeChange
=
async
(
theme
:
string
)
=>
{
await
userStore
.
updateUserSetting
({
theme
},
[
"theme"
]);
};
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"common.basic"
)
}
</
p
>
...
...
@@ -41,6 +46,11 @@ const PreferencesSection = observer(() => {
<
AppearanceSelect
value=
{
setting
.
appearance
as
Appearance
}
onChange=
{
handleAppearanceSelectChange
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.preference-section.theme"
)
}
</
span
>
<
ThemeSelector
value=
{
setting
.
theme
}
onValueChange=
{
handleThemeChange
}
/>
</
div
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.preference"
)
}
</
p
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
...
...
web/src/components/UpdateCustomizedProfileDialog.tsx
View file @
533591af
...
...
@@ -137,12 +137,12 @@ export function UpdateCustomizedProfileDialog({ open, onOpenChange, onSuccess }:
<
div
className=
"grid gap-2"
>
<
Label
>
{
t
(
"setting.system-section.customize-server.locale"
)
}
</
Label
>
<
LocaleSelect
className=
"w-full"
value=
{
customProfile
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
<
LocaleSelect
value=
{
customProfile
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
>
{
t
(
"setting.system-section.customize-server.appearance"
)
}
</
Label
>
<
AppearanceSelect
className=
"w-full"
value=
{
customProfile
.
appearance
as
Appearance
}
onChange=
{
handleAppearanceSelectChange
}
/>
<
AppearanceSelect
value=
{
customProfile
.
appearance
as
Appearance
}
onChange=
{
handleAppearanceSelectChange
}
/>
</
div
>
</
div
>
...
...
web/src/types/proto/api/v1/user_service.ts
View file @
533591af
...
...
@@ -272,6 +272,12 @@ export interface UserSetting {
appearance
:
string
;
/** The default visibility of the memo. */
memoVisibility
:
string
;
/**
* The preferred theme of the user.
* This references a CSS file in the web/public/themes/ directory.
* If not set, the default theme will be used.
*/
theme
:
string
;
}
export
interface
GetUserSettingRequest
{
...
...
@@ -1532,7 +1538,7 @@ export const GetUserStatsRequest: MessageFns<GetUserStatsRequest> = {
};
function
createBaseUserSetting
():
UserSetting
{
return
{
name
:
""
,
locale
:
""
,
appearance
:
""
,
memoVisibility
:
""
};
return
{
name
:
""
,
locale
:
""
,
appearance
:
""
,
memoVisibility
:
""
,
theme
:
""
};
}
export
const
UserSetting
:
MessageFns
<
UserSetting
>
=
{
...
...
@@ -1549,6 +1555,9 @@ export const UserSetting: MessageFns<UserSetting> = {
if
(
message
.
memoVisibility
!==
""
)
{
writer
.
uint32
(
34
).
string
(
message
.
memoVisibility
);
}
if
(
message
.
theme
!==
""
)
{
writer
.
uint32
(
42
).
string
(
message
.
theme
);
}
return
writer
;
},
...
...
@@ -1591,6 +1600,14 @@ export const UserSetting: MessageFns<UserSetting> = {
message
.
memoVisibility
=
reader
.
string
();
continue
;
}
case
5
:
{
if
(
tag
!==
42
)
{
break
;
}
message
.
theme
=
reader
.
string
();
continue
;
}
}
if
((
tag
&
7
)
===
4
||
tag
===
0
)
{
break
;
...
...
@@ -1609,6 +1626,7 @@ export const UserSetting: MessageFns<UserSetting> = {
message
.
locale
=
object
.
locale
??
""
;
message
.
appearance
=
object
.
appearance
??
""
;
message
.
memoVisibility
=
object
.
memoVisibility
??
""
;
message
.
theme
=
object
.
theme
??
""
;
return
message
;
},
};
...
...
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