From 81da20c9059b46840b4ebe0ddb5a3d6a6908226f Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 2 Dec 2025 09:08:46 +0800 Subject: [PATCH] refactor: simplify theme/locale to user preferences and improve initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove theme and locale from instance settings to eliminate duplication and simplify the codebase. These are user-specific preferences and should only exist in user settings, not instance-wide settings. Backend changes: - Remove theme from InstanceGeneralSetting proto - Remove locale from InstanceCustomProfile proto - Update instance service converters to remove theme/locale handling - Simplify RSS feed to use static locale Frontend changes: - Remove theme/locale from instanceStore state - Create unified initialization flow with clear fallback priority: * Theme: user setting → localStorage → system preference * Locale: user setting → browser language - Add applyUserPreferences() to centralize theme/locale application - Simplify App.tsx by removing redundant state synchronization - Update all components to use new helper functions: * getThemeWithFallback() for theme resolution * getLocaleWithFallback() for locale resolution - Remove theme/locale selectors from instance profile dialog Theme utilities refactor: - Organize code into clear sections with JSDoc comments - Extract localStorage operations into getStoredTheme/setStoredTheme helpers - Split DOM manipulation into focused functions - Improve type safety with Theme and ResolvedTheme types - Reduce code duplication and improve maintainability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- proto/api/v1/instance_service.proto | 4 - proto/gen/api/v1/activity_service_grpc.pb.go | 8 +- .../gen/api/v1/attachment_service_grpc.pb.go | 16 +- proto/gen/api/v1/auth_service_grpc.pb.go | 10 +- proto/gen/api/v1/idp_service_grpc.pb.go | 14 +- proto/gen/api/v1/instance_service.pb.go | 30 +-- proto/gen/api/v1/instance_service_grpc.pb.go | 10 +- proto/gen/api/v1/memo_service_grpc.pb.go | 32 +-- proto/gen/api/v1/shortcut_service_grpc.pb.go | 14 +- proto/gen/api/v1/user_service_grpc.pb.go | 50 ++-- proto/gen/openapi.yaml | 7 - proto/gen/store/instance_setting.pb.go | 28 +- proto/store/instance_setting.proto | 4 - server/router/api/v1/instance_service.go | 9 - server/router/rss/rss.go | 8 +- web/src/App.tsx | 35 +-- web/src/components/AuthFooter.tsx | 20 +- web/src/components/MemoContent/CodeBlock.tsx | 10 +- .../components/MemoContent/MermaidBlock.tsx | 7 +- .../components/Settings/InstanceSection.tsx | 9 - .../Settings/PreferencesSection.tsx | 12 +- web/src/components/ThemeSelect.tsx | 7 +- .../UpdateCustomizedProfileDialog.tsx | 19 -- web/src/components/UserMenu.tsx | 13 +- web/src/main.tsx | 8 +- web/src/store/index.ts | 3 - web/src/store/instance.ts | 73 +---- web/src/store/user.ts | 45 ++-- .../types/proto/api/v1/instance_service.ts | 33 +-- web/src/utils/i18n.ts | 13 + web/src/utils/theme.ts | 251 +++++++++++++----- 31 files changed, 363 insertions(+), 439 deletions(-) diff --git a/proto/api/v1/instance_service.proto b/proto/api/v1/instance_service.proto index 0b97bb827..418f4e1ce 100644 --- a/proto/api/v1/instance_service.proto +++ b/proto/api/v1/instance_service.proto @@ -83,9 +83,6 @@ message InstanceSetting { // General instance settings configuration. message GeneralSetting { - // theme is the name of the selected theme. - // This references a CSS file in the web/public/themes/ directory. - string theme = 1; // disallow_user_registration disallows user registration. bool disallow_user_registration = 2; // disallow_password_auth disallows password authentication. @@ -111,7 +108,6 @@ message InstanceSetting { string title = 1; string description = 2; string logo_url = 3; - string locale = 4; } } diff --git a/proto/gen/api/v1/activity_service_grpc.pb.go b/proto/gen/api/v1/activity_service_grpc.pb.go index d9d0594ef..ee48c8471 100644 --- a/proto/gen/api/v1/activity_service_grpc.pb.go +++ b/proto/gen/api/v1/activity_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/activity_service.proto @@ -80,10 +80,10 @@ type ActivityServiceServer interface { type UnimplementedActivityServiceServer struct{} func (UnimplementedActivityServiceServer) ListActivities(context.Context, *ListActivitiesRequest) (*ListActivitiesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListActivities not implemented") + return nil, status.Error(codes.Unimplemented, "method ListActivities not implemented") } func (UnimplementedActivityServiceServer) GetActivity(context.Context, *GetActivityRequest) (*Activity, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetActivity not implemented") + return nil, status.Error(codes.Unimplemented, "method GetActivity not implemented") } func (UnimplementedActivityServiceServer) mustEmbedUnimplementedActivityServiceServer() {} func (UnimplementedActivityServiceServer) testEmbeddedByValue() {} @@ -96,7 +96,7 @@ type UnsafeActivityServiceServer interface { } func RegisterActivityServiceServer(s grpc.ServiceRegistrar, srv ActivityServiceServer) { - // If the following call pancis, it indicates UnimplementedActivityServiceServer was + // If the following call panics, it indicates UnimplementedActivityServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/api/v1/attachment_service_grpc.pb.go b/proto/gen/api/v1/attachment_service_grpc.pb.go index fa07861a3..a453128ea 100644 --- a/proto/gen/api/v1/attachment_service_grpc.pb.go +++ b/proto/gen/api/v1/attachment_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/attachment_service.proto @@ -142,22 +142,22 @@ type AttachmentServiceServer interface { type UnimplementedAttachmentServiceServer struct{} func (UnimplementedAttachmentServiceServer) CreateAttachment(context.Context, *CreateAttachmentRequest) (*Attachment, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateAttachment not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateAttachment not implemented") } func (UnimplementedAttachmentServiceServer) ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListAttachments not implemented") + return nil, status.Error(codes.Unimplemented, "method ListAttachments not implemented") } func (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetAttachment not implemented") + return nil, status.Error(codes.Unimplemented, "method GetAttachment not implemented") } func (UnimplementedAttachmentServiceServer) GetAttachmentBinary(context.Context, *GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetAttachmentBinary not implemented") + return nil, status.Error(codes.Unimplemented, "method GetAttachmentBinary not implemented") } func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateAttachment not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateAttachment not implemented") } func (UnimplementedAttachmentServiceServer) DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteAttachment not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteAttachment not implemented") } func (UnimplementedAttachmentServiceServer) mustEmbedUnimplementedAttachmentServiceServer() {} func (UnimplementedAttachmentServiceServer) testEmbeddedByValue() {} @@ -170,7 +170,7 @@ type UnsafeAttachmentServiceServer interface { } func RegisterAttachmentServiceServer(s grpc.ServiceRegistrar, srv AttachmentServiceServer) { - // If the following call pancis, it indicates UnimplementedAttachmentServiceServer was + // If the following call panics, it indicates UnimplementedAttachmentServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/api/v1/auth_service_grpc.pb.go b/proto/gen/api/v1/auth_service_grpc.pb.go index 2872b28be..9f6555643 100644 --- a/proto/gen/api/v1/auth_service_grpc.pb.go +++ b/proto/gen/api/v1/auth_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/auth_service.proto @@ -102,13 +102,13 @@ type AuthServiceServer interface { type UnimplementedAuthServiceServer struct{} func (UnimplementedAuthServiceServer) GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetCurrentSession not implemented") + return nil, status.Error(codes.Unimplemented, "method GetCurrentSession not implemented") } func (UnimplementedAuthServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateSession not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateSession not implemented") } func (UnimplementedAuthServiceServer) DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteSession not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteSession not implemented") } func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} @@ -121,7 +121,7 @@ type UnsafeAuthServiceServer interface { } func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { - // If the following call pancis, it indicates UnimplementedAuthServiceServer was + // If the following call panics, it indicates UnimplementedAuthServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/api/v1/idp_service_grpc.pb.go b/proto/gen/api/v1/idp_service_grpc.pb.go index 3a9bee2e5..bce241228 100644 --- a/proto/gen/api/v1/idp_service_grpc.pb.go +++ b/proto/gen/api/v1/idp_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/idp_service.proto @@ -126,19 +126,19 @@ type IdentityProviderServiceServer interface { type UnimplementedIdentityProviderServiceServer struct{} func (UnimplementedIdentityProviderServiceServer) ListIdentityProviders(context.Context, *ListIdentityProvidersRequest) (*ListIdentityProvidersResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListIdentityProviders not implemented") + return nil, status.Error(codes.Unimplemented, "method ListIdentityProviders not implemented") } func (UnimplementedIdentityProviderServiceServer) GetIdentityProvider(context.Context, *GetIdentityProviderRequest) (*IdentityProvider, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetIdentityProvider not implemented") + return nil, status.Error(codes.Unimplemented, "method GetIdentityProvider not implemented") } func (UnimplementedIdentityProviderServiceServer) CreateIdentityProvider(context.Context, *CreateIdentityProviderRequest) (*IdentityProvider, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateIdentityProvider not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateIdentityProvider not implemented") } func (UnimplementedIdentityProviderServiceServer) UpdateIdentityProvider(context.Context, *UpdateIdentityProviderRequest) (*IdentityProvider, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateIdentityProvider not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateIdentityProvider not implemented") } func (UnimplementedIdentityProviderServiceServer) DeleteIdentityProvider(context.Context, *DeleteIdentityProviderRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteIdentityProvider not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteIdentityProvider not implemented") } func (UnimplementedIdentityProviderServiceServer) mustEmbedUnimplementedIdentityProviderServiceServer() { } @@ -152,7 +152,7 @@ type UnsafeIdentityProviderServiceServer interface { } func RegisterIdentityProviderServiceServer(s grpc.ServiceRegistrar, srv IdentityProviderServiceServer) { - // If the following call pancis, it indicates UnimplementedIdentityProviderServiceServer was + // If the following call panics, it indicates UnimplementedIdentityProviderServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/api/v1/instance_service.pb.go b/proto/gen/api/v1/instance_service.pb.go index 248d18be2..71b72df83 100644 --- a/proto/gen/api/v1/instance_service.pb.go +++ b/proto/gen/api/v1/instance_service.pb.go @@ -460,9 +460,6 @@ func (x *UpdateInstanceSettingRequest) GetUpdateMask() *fieldmaskpb.FieldMask { // General instance settings configuration. type InstanceSetting_GeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` - // theme is the name of the selected theme. - // This references a CSS file in the web/public/themes/ directory. - Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"` // disallow_user_registration disallows user registration. DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` // disallow_password_auth disallows password authentication. @@ -515,13 +512,6 @@ func (*InstanceSetting_GeneralSetting) Descriptor() ([]byte, []int) { return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0} } -func (x *InstanceSetting_GeneralSetting) GetTheme() string { - if x != nil { - return x.Theme - } - return "" -} - func (x *InstanceSetting_GeneralSetting) GetDisallowUserRegistration() bool { if x != nil { return x.DisallowUserRegistration @@ -758,7 +748,6 @@ type InstanceSetting_GeneralSetting_CustomProfile struct { Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` LogoUrl string `protobuf:"bytes,3,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` - Locale string `protobuf:"bytes,4,opt,name=locale,proto3" json:"locale,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -814,13 +803,6 @@ func (x *InstanceSetting_GeneralSetting_CustomProfile) GetLogoUrl() string { return "" } -func (x *InstanceSetting_GeneralSetting_CustomProfile) GetLocale() string { - if x != nil { - return x.Locale - } - return "" -} - // S3 configuration for cloud storage backend. // Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ type InstanceSetting_StorageSetting_S3Config struct { @@ -917,14 +899,13 @@ const file_api_v1_instance_service_proto_rawDesc = "" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + "\x04mode\x18\x03 \x01(\tR\x04mode\x12!\n" + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\"\x1b\n" + - "\x19GetInstanceProfileRequest\"\x9d\x10\n" + + "\x19GetInstanceProfileRequest\"\xef\x0f\n" + "\x0fInstanceSetting\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" + "\x0fgeneral_setting\x18\x02 \x01(\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12W\n" + "\x0fstorage_setting\x18\x03 \x01(\v2,.memos.api.v1.InstanceSetting.StorageSettingH\x00R\x0estorageSetting\x12d\n" + - "\x14memo_related_setting\x18\x04 \x01(\v20.memos.api.v1.InstanceSetting.MemoRelatedSettingH\x00R\x12memoRelatedSetting\x1a\xf8\x04\n" + - "\x0eGeneralSetting\x12\x14\n" + - "\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" + + "\x14memo_related_setting\x18\x04 \x01(\v20.memos.api.v1.InstanceSetting.MemoRelatedSettingH\x00R\x12memoRelatedSetting\x1a\xca\x04\n" + + "\x0eGeneralSetting\x12<\n" + "\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" + "\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" + "\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" + @@ -932,12 +913,11 @@ const file_api_v1_instance_service_proto_rawDesc = "" + "\x0ecustom_profile\x18\x06 \x01(\v2:.memos.api.v1.InstanceSetting.GeneralSetting.CustomProfileR\rcustomProfile\x121\n" + "\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" + "\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" + - "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\x1az\n" + + "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\x1ab\n" + "\rCustomProfile\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + - "\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" + - "\x06locale\x18\x04 \x01(\tR\x06locale\x1a\xbc\x04\n" + + "\blogo_url\x18\x03 \x01(\tR\alogoUrl\x1a\xbc\x04\n" + "\x0eStorageSetting\x12[\n" + "\fstorage_type\x18\x01 \x01(\x0e28.memos.api.v1.InstanceSetting.StorageSetting.StorageTypeR\vstorageType\x12+\n" + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + diff --git a/proto/gen/api/v1/instance_service_grpc.pb.go b/proto/gen/api/v1/instance_service_grpc.pb.go index a8d544f53..aaa18dd3e 100644 --- a/proto/gen/api/v1/instance_service_grpc.pb.go +++ b/proto/gen/api/v1/instance_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/instance_service.proto @@ -95,13 +95,13 @@ type InstanceServiceServer interface { type UnimplementedInstanceServiceServer struct{} func (UnimplementedInstanceServiceServer) GetInstanceProfile(context.Context, *GetInstanceProfileRequest) (*InstanceProfile, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetInstanceProfile not implemented") + return nil, status.Error(codes.Unimplemented, "method GetInstanceProfile not implemented") } func (UnimplementedInstanceServiceServer) GetInstanceSetting(context.Context, *GetInstanceSettingRequest) (*InstanceSetting, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetInstanceSetting not implemented") + return nil, status.Error(codes.Unimplemented, "method GetInstanceSetting not implemented") } func (UnimplementedInstanceServiceServer) UpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateInstanceSetting not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateInstanceSetting not implemented") } func (UnimplementedInstanceServiceServer) mustEmbedUnimplementedInstanceServiceServer() {} func (UnimplementedInstanceServiceServer) testEmbeddedByValue() {} @@ -114,7 +114,7 @@ type UnsafeInstanceServiceServer interface { } func RegisterInstanceServiceServer(s grpc.ServiceRegistrar, srv InstanceServiceServer) { - // If the following call pancis, it indicates UnimplementedInstanceServiceServer was + // If the following call panics, it indicates UnimplementedInstanceServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/api/v1/memo_service_grpc.pb.go b/proto/gen/api/v1/memo_service_grpc.pb.go index 2e4e91815..a7d4f46e8 100644 --- a/proto/gen/api/v1/memo_service_grpc.pb.go +++ b/proto/gen/api/v1/memo_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/memo_service.proto @@ -261,46 +261,46 @@ type MemoServiceServer interface { type UnimplementedMemoServiceServer struct{} func (UnimplementedMemoServiceServer) CreateMemo(context.Context, *CreateMemoRequest) (*Memo, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateMemo not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateMemo not implemented") } func (UnimplementedMemoServiceServer) ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListMemos not implemented") + return nil, status.Error(codes.Unimplemented, "method ListMemos not implemented") } func (UnimplementedMemoServiceServer) GetMemo(context.Context, *GetMemoRequest) (*Memo, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetMemo not implemented") + return nil, status.Error(codes.Unimplemented, "method GetMemo not implemented") } func (UnimplementedMemoServiceServer) UpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateMemo not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateMemo not implemented") } func (UnimplementedMemoServiceServer) DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteMemo not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteMemo not implemented") } func (UnimplementedMemoServiceServer) SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetMemoAttachments not implemented") + return nil, status.Error(codes.Unimplemented, "method SetMemoAttachments not implemented") } func (UnimplementedMemoServiceServer) ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListMemoAttachments not implemented") + return nil, status.Error(codes.Unimplemented, "method ListMemoAttachments not implemented") } func (UnimplementedMemoServiceServer) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetMemoRelations not implemented") + return nil, status.Error(codes.Unimplemented, "method SetMemoRelations not implemented") } func (UnimplementedMemoServiceServer) ListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListMemoRelations not implemented") + return nil, status.Error(codes.Unimplemented, "method ListMemoRelations not implemented") } func (UnimplementedMemoServiceServer) CreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateMemoComment not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateMemoComment not implemented") } func (UnimplementedMemoServiceServer) ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListMemoComments not implemented") + return nil, status.Error(codes.Unimplemented, "method ListMemoComments not implemented") } func (UnimplementedMemoServiceServer) ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListMemoReactions not implemented") + return nil, status.Error(codes.Unimplemented, "method ListMemoReactions not implemented") } func (UnimplementedMemoServiceServer) UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpsertMemoReaction not implemented") + return nil, status.Error(codes.Unimplemented, "method UpsertMemoReaction not implemented") } func (UnimplementedMemoServiceServer) DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteMemoReaction not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteMemoReaction not implemented") } func (UnimplementedMemoServiceServer) mustEmbedUnimplementedMemoServiceServer() {} func (UnimplementedMemoServiceServer) testEmbeddedByValue() {} @@ -313,7 +313,7 @@ type UnsafeMemoServiceServer interface { } func RegisterMemoServiceServer(s grpc.ServiceRegistrar, srv MemoServiceServer) { - // If the following call pancis, it indicates UnimplementedMemoServiceServer was + // If the following call panics, it indicates UnimplementedMemoServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/api/v1/shortcut_service_grpc.pb.go b/proto/gen/api/v1/shortcut_service_grpc.pb.go index 3a3f5b89e..f6913ef07 100644 --- a/proto/gen/api/v1/shortcut_service_grpc.pb.go +++ b/proto/gen/api/v1/shortcut_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/shortcut_service.proto @@ -126,19 +126,19 @@ type ShortcutServiceServer interface { type UnimplementedShortcutServiceServer struct{} func (UnimplementedShortcutServiceServer) ListShortcuts(context.Context, *ListShortcutsRequest) (*ListShortcutsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListShortcuts not implemented") + return nil, status.Error(codes.Unimplemented, "method ListShortcuts not implemented") } func (UnimplementedShortcutServiceServer) GetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetShortcut not implemented") + return nil, status.Error(codes.Unimplemented, "method GetShortcut not implemented") } func (UnimplementedShortcutServiceServer) CreateShortcut(context.Context, *CreateShortcutRequest) (*Shortcut, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateShortcut not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateShortcut not implemented") } func (UnimplementedShortcutServiceServer) UpdateShortcut(context.Context, *UpdateShortcutRequest) (*Shortcut, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateShortcut not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateShortcut not implemented") } func (UnimplementedShortcutServiceServer) DeleteShortcut(context.Context, *DeleteShortcutRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteShortcut not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteShortcut not implemented") } func (UnimplementedShortcutServiceServer) mustEmbedUnimplementedShortcutServiceServer() {} func (UnimplementedShortcutServiceServer) testEmbeddedByValue() {} @@ -151,7 +151,7 @@ type UnsafeShortcutServiceServer interface { } func RegisterShortcutServiceServer(s grpc.ServiceRegistrar, srv ShortcutServiceServer) { - // If the following call pancis, it indicates UnimplementedShortcutServiceServer was + // If the following call panics, it indicates UnimplementedShortcutServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/api/v1/user_service_grpc.pb.go b/proto/gen/api/v1/user_service_grpc.pb.go index 1d6527406..5f54cba22 100644 --- a/proto/gen/api/v1/user_service_grpc.pb.go +++ b/proto/gen/api/v1/user_service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: api/v1/user_service.proto @@ -403,73 +403,73 @@ type UserServiceServer interface { type UnimplementedUserServiceServer struct{} func (UnimplementedUserServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") + return nil, status.Error(codes.Unimplemented, "method ListUsers not implemented") } func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*User, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") + return nil, status.Error(codes.Unimplemented, "method GetUser not implemented") } func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*User, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateUser not implemented") } func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*User, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateUser not implemented") } func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented") } func (UnimplementedUserServiceServer) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetUserAvatar not implemented") + return nil, status.Error(codes.Unimplemented, "method GetUserAvatar not implemented") } func (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListAllUserStats not implemented") + return nil, status.Error(codes.Unimplemented, "method ListAllUserStats not implemented") } func (UnimplementedUserServiceServer) GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetUserStats not implemented") + return nil, status.Error(codes.Unimplemented, "method GetUserStats not implemented") } func (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetUserSetting not implemented") + return nil, status.Error(codes.Unimplemented, "method GetUserSetting not implemented") } func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateUserSetting not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateUserSetting not implemented") } func (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListUserSettings not implemented") + return nil, status.Error(codes.Unimplemented, "method ListUserSettings not implemented") } func (UnimplementedUserServiceServer) ListUserAccessTokens(context.Context, *ListUserAccessTokensRequest) (*ListUserAccessTokensResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListUserAccessTokens not implemented") + return nil, status.Error(codes.Unimplemented, "method ListUserAccessTokens not implemented") } func (UnimplementedUserServiceServer) CreateUserAccessToken(context.Context, *CreateUserAccessTokenRequest) (*UserAccessToken, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateUserAccessToken not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateUserAccessToken not implemented") } func (UnimplementedUserServiceServer) DeleteUserAccessToken(context.Context, *DeleteUserAccessTokenRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteUserAccessToken not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteUserAccessToken not implemented") } func (UnimplementedUserServiceServer) ListUserSessions(context.Context, *ListUserSessionsRequest) (*ListUserSessionsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListUserSessions not implemented") + return nil, status.Error(codes.Unimplemented, "method ListUserSessions not implemented") } func (UnimplementedUserServiceServer) RevokeUserSession(context.Context, *RevokeUserSessionRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method RevokeUserSession not implemented") + return nil, status.Error(codes.Unimplemented, "method RevokeUserSession not implemented") } func (UnimplementedUserServiceServer) ListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListUserWebhooks not implemented") + return nil, status.Error(codes.Unimplemented, "method ListUserWebhooks not implemented") } func (UnimplementedUserServiceServer) CreateUserWebhook(context.Context, *CreateUserWebhookRequest) (*UserWebhook, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateUserWebhook not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateUserWebhook not implemented") } func (UnimplementedUserServiceServer) UpdateUserWebhook(context.Context, *UpdateUserWebhookRequest) (*UserWebhook, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateUserWebhook not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateUserWebhook not implemented") } func (UnimplementedUserServiceServer) DeleteUserWebhook(context.Context, *DeleteUserWebhookRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteUserWebhook not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteUserWebhook not implemented") } func (UnimplementedUserServiceServer) ListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListUserNotifications not implemented") + return nil, status.Error(codes.Unimplemented, "method ListUserNotifications not implemented") } func (UnimplementedUserServiceServer) UpdateUserNotification(context.Context, *UpdateUserNotificationRequest) (*UserNotification, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateUserNotification not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateUserNotification not implemented") } func (UnimplementedUserServiceServer) DeleteUserNotification(context.Context, *DeleteUserNotificationRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteUserNotification not implemented") + return nil, status.Error(codes.Unimplemented, "method DeleteUserNotification not implemented") } func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} func (UnimplementedUserServiceServer) testEmbeddedByValue() {} @@ -482,7 +482,7 @@ type UnsafeUserServiceServer interface { } func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { - // If the following call pancis, it indicates UnimplementedUserServiceServer was + // If the following call panics, it indicates UnimplementedUserServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index ca0b288e9..0bd250136 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -2198,8 +2198,6 @@ components: type: string logoUrl: type: string - locale: - type: string description: Custom profile configuration for instance branding. GetCurrentSessionResponse: type: object @@ -2290,11 +2288,6 @@ components: InstanceSetting_GeneralSetting: type: object properties: - theme: - type: string - description: |- - theme is the name of the selected theme. - This references a CSS file in the web/public/themes/ directory. disallowUserRegistration: type: boolean description: disallow_user_registration disallows user registration. diff --git a/proto/gen/store/instance_setting.pb.go b/proto/gen/store/instance_setting.pb.go index e01305834..f4e11402a 100644 --- a/proto/gen/store/instance_setting.pb.go +++ b/proto/gen/store/instance_setting.pb.go @@ -313,9 +313,6 @@ func (x *InstanceBasicSetting) GetSchemaVersion() string { type InstanceGeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` - // theme is the name of the selected theme. - // This references a CSS file in the web/public/themes/ directory. - Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"` // disallow_user_registration disallows user registration. DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` // disallow_password_auth disallows password authentication. @@ -368,13 +365,6 @@ func (*InstanceGeneralSetting) Descriptor() ([]byte, []int) { return file_store_instance_setting_proto_rawDescGZIP(), []int{2} } -func (x *InstanceGeneralSetting) GetTheme() string { - if x != nil { - return x.Theme - } - return "" -} - func (x *InstanceGeneralSetting) GetDisallowUserRegistration() bool { if x != nil { return x.DisallowUserRegistration @@ -436,7 +426,6 @@ type InstanceCustomProfile struct { Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` LogoUrl string `protobuf:"bytes,3,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` - Locale string `protobuf:"bytes,4,opt,name=locale,proto3" json:"locale,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -492,13 +481,6 @@ func (x *InstanceCustomProfile) GetLogoUrl() string { return "" } -func (x *InstanceCustomProfile) GetLocale() string { - if x != nil { - return x.Locale - } - return "" -} - type InstanceStorageSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // storage_type is the storage type. @@ -771,9 +753,8 @@ const file_store_instance_setting_proto_rawDesc = "" + "\x14InstanceBasicSetting\x12\x1d\n" + "\n" + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12%\n" + - "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xec\x03\n" + - "\x16InstanceGeneralSetting\x12\x14\n" + - "\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" + + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xd6\x03\n" + + "\x16InstanceGeneralSetting\x12<\n" + "\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" + "\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" + "\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" + @@ -781,12 +762,11 @@ const file_store_instance_setting_proto_rawDesc = "" + "\x0ecustom_profile\x18\x06 \x01(\v2\".memos.store.InstanceCustomProfileR\rcustomProfile\x121\n" + "\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" + "\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" + - "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"\x82\x01\n" + + "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"j\n" + "\x15InstanceCustomProfile\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + - "\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" + - "\x06locale\x18\x04 \x01(\tR\x06locale\"\xd3\x02\n" + + "\blogo_url\x18\x03 \x01(\tR\alogoUrl\"\xd3\x02\n" + "\x16InstanceStorageSetting\x12R\n" + "\fstorage_type\x18\x01 \x01(\x0e2/.memos.store.InstanceStorageSetting.StorageTypeR\vstorageType\x12+\n" + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + diff --git a/proto/store/instance_setting.proto b/proto/store/instance_setting.proto index 40ffa296f..6314731c7 100644 --- a/proto/store/instance_setting.proto +++ b/proto/store/instance_setting.proto @@ -34,9 +34,6 @@ message InstanceBasicSetting { } message InstanceGeneralSetting { - // theme is the name of the selected theme. - // This references a CSS file in the web/public/themes/ directory. - string theme = 1; // disallow_user_registration disallows user registration. bool disallow_user_registration = 2; // disallow_password_auth disallows password authentication. @@ -61,7 +58,6 @@ message InstanceCustomProfile { string title = 1; string description = 2; string logo_url = 3; - string locale = 4; } message InstanceStorageSetting { diff --git a/server/router/api/v1/instance_service.go b/server/router/api/v1/instance_service.go index 18d9461ba..cf5798499 100644 --- a/server/router/api/v1/instance_service.go +++ b/server/router/api/v1/instance_service.go @@ -154,14 +154,8 @@ func convertInstanceGeneralSettingFromStore(setting *storepb.InstanceGeneralSett if setting == nil { return nil } - // Backfill theme if empty - theme := setting.Theme - if theme == "" { - theme = "default" - } generalSetting := &v1pb.InstanceSetting_GeneralSetting{ - Theme: theme, DisallowUserRegistration: setting.DisallowUserRegistration, DisallowPasswordAuth: setting.DisallowPasswordAuth, AdditionalScript: setting.AdditionalScript, @@ -175,7 +169,6 @@ func convertInstanceGeneralSettingFromStore(setting *storepb.InstanceGeneralSett Title: setting.CustomProfile.Title, Description: setting.CustomProfile.Description, LogoUrl: setting.CustomProfile.LogoUrl, - Locale: setting.CustomProfile.Locale, } } return generalSetting @@ -186,7 +179,6 @@ func convertInstanceGeneralSettingToStore(setting *v1pb.InstanceSetting_GeneralS return nil } generalSetting := &storepb.InstanceGeneralSetting{ - Theme: setting.Theme, DisallowUserRegistration: setting.DisallowUserRegistration, DisallowPasswordAuth: setting.DisallowPasswordAuth, AdditionalScript: setting.AdditionalScript, @@ -200,7 +192,6 @@ func convertInstanceGeneralSettingToStore(setting *v1pb.InstanceSetting_GeneralS Title: setting.CustomProfile.Title, Description: setting.CustomProfile.Description, LogoUrl: setting.CustomProfile.LogoUrl, - Locale: setting.CustomProfile.Locale, } } return generalSetting diff --git a/server/router/rss/rss.go b/server/router/rss/rss.go index c3349a427..a21a59c07 100644 --- a/server/router/rss/rss.go +++ b/server/router/rss/rss.go @@ -415,15 +415,9 @@ func getRSSHeading(ctx context.Context, stores *store.Store) (RSSHeading, error) } customProfile := settings.CustomProfile - // Use locale as language if available, default to en-us - language := "en-us" - if customProfile.Locale != "" { - language = customProfile.Locale - } - return RSSHeading{ Title: customProfile.Title, Description: customProfile.Description, - Language: language, + Language: "en-us", }, nil } diff --git a/web/src/App.tsx b/web/src/App.tsx index df13766ce..58b7179a4 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -5,7 +5,7 @@ import { Outlet } from "react-router-dom"; import useNavigateTo from "./hooks/useNavigateTo"; import { instanceStore, userStore } from "./store"; import { cleanupExpiredOAuthState } from "./utils/oauth"; -import { loadTheme, setupSystemThemeListener } from "./utils/theme"; +import { getThemeWithFallback, loadTheme, setupSystemThemeListener } from "./utils/theme"; const App = observer(() => { const { i18n } = useTranslation(); @@ -54,55 +54,44 @@ const App = observer(() => { link.href = instanceGeneralSetting.customProfile.logoUrl || "/logo.webp"; }, [instanceGeneralSetting.customProfile]); + // Update HTML lang and dir attributes based on current locale useEffect(() => { - const currentLocale = instanceStore.state.locale; - // This will trigger re-rendering of the whole app. - i18n.changeLanguage(currentLocale); + const currentLocale = i18n.language; document.documentElement.setAttribute("lang", currentLocale); if (["ar", "fa"].includes(currentLocale)) { document.documentElement.setAttribute("dir", "rtl"); } else { document.documentElement.setAttribute("dir", "ltr"); } - }, [instanceStore.state.locale]); + }, [i18n.language]); + // Apply theme when user setting changes useEffect(() => { if (!userGeneralSetting) { return; } - - instanceStore.state.setPartial({ - locale: userGeneralSetting.locale || instanceStore.state.locale, - theme: userGeneralSetting.theme || instanceStore.state.theme, - }); - }, [userGeneralSetting?.locale, userGeneralSetting?.theme]); - - // Load theme when instance theme changes or user setting changes - useEffect(() => { - const currentTheme = userGeneralSetting?.theme || instanceStore.state.theme; - if (currentTheme) { - loadTheme(currentTheme); - } - }, [userGeneralSetting?.theme, instanceStore.state.theme]); + const theme = getThemeWithFallback(userGeneralSetting.theme); + loadTheme(theme); + }, [userGeneralSetting?.theme]); // Listen for system theme changes when using "system" theme useEffect(() => { - const currentTheme = userGeneralSetting?.theme || instanceStore.state.theme; + const theme = getThemeWithFallback(userGeneralSetting?.theme); // Only set up listener if theme is "system" - if (currentTheme !== "system") { + if (theme !== "system") { return; } // Set up listener for OS theme preference changes const cleanup = setupSystemThemeListener(() => { // Reload theme when system preference changes - loadTheme(currentTheme); + loadTheme(theme); }); // Cleanup listener on unmount or when theme changes return cleanup; - }, [userGeneralSetting?.theme, instanceStore.state.theme]); + }, [userGeneralSetting?.theme]); return ; }); diff --git a/web/src/components/AuthFooter.tsx b/web/src/components/AuthFooter.tsx index a4d36cad0..f68f05ed0 100644 --- a/web/src/components/AuthFooter.tsx +++ b/web/src/components/AuthFooter.tsx @@ -1,6 +1,8 @@ import { observer } from "mobx-react-lite"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n"; import { cn } from "@/lib/utils"; -import { instanceStore } from "@/store"; +import { getInitialTheme, loadTheme } from "@/utils/theme"; import LocaleSelect from "./LocaleSelect"; import ThemeSelect from "./ThemeSelect"; @@ -9,10 +11,22 @@ interface Props { } const AuthFooter = observer(({ className }: Props) => { + const { i18n: i18nInstance } = useTranslation(); + const currentLocale = i18nInstance.language as Locale; + const currentTheme = getInitialTheme(); + + const handleLocaleChange = (locale: Locale) => { + i18n.changeLanguage(locale); + }; + + const handleThemeChange = (theme: string) => { + loadTheme(theme); + }; + return (
- instanceStore.state.setPartial({ locale })} /> - instanceStore.state.setPartial({ theme })} /> + +
); }); diff --git a/web/src/components/MemoContent/CodeBlock.tsx b/web/src/components/MemoContent/CodeBlock.tsx index 6ab45b006..b6a6dcce2 100644 --- a/web/src/components/MemoContent/CodeBlock.tsx +++ b/web/src/components/MemoContent/CodeBlock.tsx @@ -4,7 +4,8 @@ import { CheckIcon, CopyIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; import { useEffect, useMemo, useState } from "react"; import { cn } from "@/lib/utils"; -import { instanceStore } from "@/store"; +import { userStore } from "@/store"; +import { getThemeWithFallback, resolveTheme } from "@/utils/theme"; import { MermaidBlock } from "./MermaidBlock"; interface PreProps { @@ -93,8 +94,9 @@ export const CodeBlock = observer(({ children, className, ...props }: PreProps) ); } - const appTheme = instanceStore.state.theme; - const isDarkTheme = appTheme.includes("dark"); + const theme = getThemeWithFallback(userStore.state.userGeneralSetting?.theme); + const resolvedTheme = resolveTheme(theme); + const isDarkTheme = resolvedTheme.includes("dark"); // Dynamically load highlight.js theme based on app theme useEffect(() => { @@ -121,7 +123,7 @@ export const CodeBlock = observer(({ children, className, ...props }: PreProps) }; dynamicImportStyle(); - }, [appTheme, isDarkTheme]); + }, [resolvedTheme, isDarkTheme]); // Highlight code using highlight.js const highlightedCode = useMemo(() => { diff --git a/web/src/components/MemoContent/MermaidBlock.tsx b/web/src/components/MemoContent/MermaidBlock.tsx index f28978c92..951ba08ad 100644 --- a/web/src/components/MemoContent/MermaidBlock.tsx +++ b/web/src/components/MemoContent/MermaidBlock.tsx @@ -2,8 +2,8 @@ import mermaid from "mermaid"; import { observer } from "mobx-react-lite"; import { useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; -import { instanceStore, userStore } from "@/store"; -import { resolveTheme, setupSystemThemeListener } from "@/utils/theme"; +import { userStore } from "@/store"; +import { getThemeWithFallback, resolveTheme, setupSystemThemeListener } from "@/utils/theme"; interface MermaidBlockProps { children?: React.ReactNode; @@ -25,7 +25,8 @@ export const MermaidBlock = observer(({ children, className }: MermaidBlockProps const codeContent = String(codeElement?.props?.children || "").replace(/\n$/, ""); // Get theme preference (reactive via MobX observer) - const themePreference = userStore.state.userGeneralSetting?.theme || instanceStore.state.theme; + // Falls back to localStorage or system preference if no user setting + const themePreference = getThemeWithFallback(userStore.state.userGeneralSetting?.theme); // Resolve theme to actual value (handles "system" theme + system theme changes) const currentTheme = useMemo(() => resolveTheme(themePreference), [themePreference, systemThemeChange]); diff --git a/web/src/components/Settings/InstanceSection.tsx b/web/src/components/Settings/InstanceSection.tsx index 30512541d..e0e2203aa 100644 --- a/web/src/components/Settings/InstanceSection.tsx +++ b/web/src/components/Settings/InstanceSection.tsx @@ -13,7 +13,6 @@ import { instanceSettingNamePrefix } from "@/store/common"; import { IdentityProvider } from "@/types/proto/api/v1/idp_service"; import { InstanceSetting_GeneralSetting, InstanceSetting_Key } from "@/types/proto/api/v1/instance_service"; import { useTranslate } from "@/utils/i18n"; -import ThemeSelect from "../ThemeSelect"; import UpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"; import SettingGroup from "./SettingGroup"; import SettingRow from "./SettingRow"; @@ -79,14 +78,6 @@ const InstanceSection = observer(() => { - - updatePartialSetting({ theme: value })} - className="min-w-fit" - /> - -