feat(settings): move tag metadata to user settings (#6017)

This commit is contained in:
boojack
2026-06-07 23:58:00 +08:00
committed by GitHub
parent a50ce09e81
commit 9eabb554d5
33 changed files with 1066 additions and 237 deletions
+2
View File
@@ -204,6 +204,8 @@ message InstanceSetting {
}
// Tag metadata configuration.
// Active tag metadata is stored in per-user tag settings.
// This message remains for backward compatibility with existing clients and migrations.
message TagsSetting {
// Map of tag name pattern to tag metadata.
// Each key is treated as an anchored regular expression (^pattern$),
+20
View File
@@ -10,6 +10,7 @@ import "google/api/resource.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
import "google/type/color.proto";
option go_package = "gen/api/v1";
@@ -426,6 +427,7 @@ message UserSetting {
oneof value {
GeneralSetting general_setting = 2;
WebhooksSetting webhooks_setting = 5;
TagsSetting tags_setting = 6;
}
// Enumeration of user setting keys.
@@ -435,6 +437,8 @@ message UserSetting {
GENERAL = 1;
// WEBHOOKS is the key for user webhooks.
WEBHOOKS = 4;
// TAGS is the key for per-user tag metadata.
TAGS = 5;
}
// General user settings configuration.
@@ -449,6 +453,22 @@ message UserSetting {
string theme = 4 [(google.api.field_behavior) = OPTIONAL];
}
// Tag metadata for user-specific display rules.
message TagMetadata {
// Optional background color for the tag label.
// When unset, the default tag color is used.
google.type.Color background_color = 1 [(google.api.field_behavior) = OPTIONAL];
// Whether memos with this tag should have their content blurred.
bool blur_content = 2 [(google.api.field_behavior) = OPTIONAL];
}
// User-specific tag metadata.
message TagsSetting {
// Map of tag name pattern to tag metadata.
// Each key is treated as an anchored regular expression (^pattern$).
map<string, TagMetadata> tags = 1 [(google.api.field_behavior) = OPTIONAL];
}
// User webhooks configuration.
message WebhooksSetting {
// List of user webhooks.
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/ai_service.proto
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/attachment_service.proto
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/auth_service.proto
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/idp_service.proto
+2
View File
@@ -1133,6 +1133,8 @@ func (x *InstanceSetting_TagMetadata) GetBlurContent() bool {
}
// Tag metadata configuration.
// Active tag metadata is stored in per-user tag settings.
// This message remains for backward compatibility with existing clients and migrations.
type InstanceSetting_TagsSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Map of tag name pattern to tag metadata.
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/instance_service.proto
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/memo_service.proto
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/shortcut_service.proto
+254 -111
View File
@@ -8,6 +8,7 @@ package apiv1
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
color "google.golang.org/genproto/googleapis/type/color"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -86,6 +87,8 @@ const (
UserSetting_GENERAL UserSetting_Key = 1
// WEBHOOKS is the key for user webhooks.
UserSetting_WEBHOOKS UserSetting_Key = 4
// TAGS is the key for per-user tag metadata.
UserSetting_TAGS UserSetting_Key = 5
)
// Enum value maps for UserSetting_Key.
@@ -94,11 +97,13 @@ var (
0: "KEY_UNSPECIFIED",
1: "GENERAL",
4: "WEBHOOKS",
5: "TAGS",
}
UserSetting_Key_value = map[string]int32{
"KEY_UNSPECIFIED": 0,
"GENERAL": 1,
"WEBHOOKS": 4,
"TAGS": 5,
}
)
@@ -1103,6 +1108,7 @@ type UserSetting struct {
//
// *UserSetting_GeneralSetting_
// *UserSetting_WebhooksSetting_
// *UserSetting_TagsSetting_
Value isUserSetting_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -1170,6 +1176,15 @@ func (x *UserSetting) GetWebhooksSetting() *UserSetting_WebhooksSetting {
return nil
}
func (x *UserSetting) GetTagsSetting() *UserSetting_TagsSetting {
if x != nil {
if x, ok := x.Value.(*UserSetting_TagsSetting_); ok {
return x.TagsSetting
}
}
return nil
}
type isUserSetting_Value interface {
isUserSetting_Value()
}
@@ -1182,10 +1197,16 @@ type UserSetting_WebhooksSetting_ struct {
WebhooksSetting *UserSetting_WebhooksSetting `protobuf:"bytes,5,opt,name=webhooks_setting,json=webhooksSetting,proto3,oneof"`
}
type UserSetting_TagsSetting_ struct {
TagsSetting *UserSetting_TagsSetting `protobuf:"bytes,6,opt,name=tags_setting,json=tagsSetting,proto3,oneof"`
}
func (*UserSetting_GeneralSetting_) isUserSetting_Value() {}
func (*UserSetting_WebhooksSetting_) isUserSetting_Value() {}
func (*UserSetting_TagsSetting_) isUserSetting_Value() {}
type GetUserSettingRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the user setting.
@@ -2949,6 +2970,109 @@ func (x *UserSetting_GeneralSetting) GetTheme() string {
return ""
}
// Tag metadata for user-specific display rules.
type UserSetting_TagMetadata struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Optional background color for the tag label.
// When unset, the default tag color is used.
BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"`
// Whether memos with this tag should have their content blurred.
BlurContent bool `protobuf:"varint,2,opt,name=blur_content,json=blurContent,proto3" json:"blur_content,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserSetting_TagMetadata) Reset() {
*x = UserSetting_TagMetadata{}
mi := &file_api_v1_user_service_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserSetting_TagMetadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserSetting_TagMetadata) ProtoMessage() {}
func (x *UserSetting_TagMetadata) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserSetting_TagMetadata.ProtoReflect.Descriptor instead.
func (*UserSetting_TagMetadata) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13, 1}
}
func (x *UserSetting_TagMetadata) GetBackgroundColor() *color.Color {
if x != nil {
return x.BackgroundColor
}
return nil
}
func (x *UserSetting_TagMetadata) GetBlurContent() bool {
if x != nil {
return x.BlurContent
}
return false
}
// User-specific tag metadata.
type UserSetting_TagsSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Map of tag name pattern to tag metadata.
// Each key is treated as an anchored regular expression (^pattern$).
Tags map[string]*UserSetting_TagMetadata `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserSetting_TagsSetting) Reset() {
*x = UserSetting_TagsSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserSetting_TagsSetting) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserSetting_TagsSetting) ProtoMessage() {}
func (x *UserSetting_TagsSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserSetting_TagsSetting.ProtoReflect.Descriptor instead.
func (*UserSetting_TagsSetting) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13, 2}
}
func (x *UserSetting_TagsSetting) GetTags() map[string]*UserSetting_TagMetadata {
if x != nil {
return x.Tags
}
return nil
}
// User webhooks configuration.
type UserSetting_WebhooksSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -2960,7 +3084,7 @@ type UserSetting_WebhooksSetting struct {
func (x *UserSetting_WebhooksSetting) Reset() {
*x = UserSetting_WebhooksSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[44]
mi := &file_api_v1_user_service_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2972,7 +3096,7 @@ func (x *UserSetting_WebhooksSetting) String() string {
func (*UserSetting_WebhooksSetting) ProtoMessage() {}
func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[44]
mi := &file_api_v1_user_service_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2985,7 +3109,7 @@ func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserSetting_WebhooksSetting.ProtoReflect.Descriptor instead.
func (*UserSetting_WebhooksSetting) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13, 1}
return file_api_v1_user_service_proto_rawDescGZIP(), []int{13, 3}
}
func (x *UserSetting_WebhooksSetting) GetWebhooks() []*UserWebhook {
@@ -3013,7 +3137,7 @@ type UserNotification_MemoCommentPayload struct {
func (x *UserNotification_MemoCommentPayload) Reset() {
*x = UserNotification_MemoCommentPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[45]
mi := &file_api_v1_user_service_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3025,7 +3149,7 @@ func (x *UserNotification_MemoCommentPayload) String() string {
func (*UserNotification_MemoCommentPayload) ProtoMessage() {}
func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[45]
mi := &file_api_v1_user_service_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3087,7 +3211,7 @@ type UserNotification_MemoMentionPayload struct {
func (x *UserNotification_MemoMentionPayload) Reset() {
*x = UserNotification_MemoMentionPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[46]
mi := &file_api_v1_user_service_proto_msgTypes[49]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3099,7 +3223,7 @@ func (x *UserNotification_MemoMentionPayload) String() string {
func (*UserNotification_MemoMentionPayload) ProtoMessage() {}
func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[46]
mi := &file_api_v1_user_service_proto_msgTypes[49]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3147,7 +3271,7 @@ var File_api_v1_user_service_proto protoreflect.FileDescriptor
const file_api_v1_user_service_proto_rawDesc = "" +
"\n" +
"\x19api/v1/user_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc1\x04\n" +
"\x19api/v1/user_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17google/type/color.proto\"\xc1\x04\n" +
"\x04User\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x120\n" +
"\x04role\x18\x02 \x01(\x0e2\x17.memos.api.v1.User.RoleB\x03\xe0A\x02R\x04role\x12\x1f\n" +
@@ -3231,21 +3355,31 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\x05state\x18\x01 \x01(\x0e2\x13.memos.api.v1.StateB\x03\xe0A\x01R\x05state\x12\x1b\n" +
"\x06filter\x18\x02 \x01(\tB\x03\xe0A\x01R\x06filter\"I\n" +
"\x18ListAllUserStatsResponse\x12-\n" +
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xb4\x04\n" +
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xbf\a\n" +
"\vUserSetting\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12S\n" +
"\x0fgeneral_setting\x18\x02 \x01(\v2(.memos.api.v1.UserSetting.GeneralSettingH\x00R\x0egeneralSetting\x12V\n" +
"\x10webhooks_setting\x18\x05 \x01(\v2).memos.api.v1.UserSetting.WebhooksSettingH\x00R\x0fwebhooksSetting\x1av\n" +
"\x10webhooks_setting\x18\x05 \x01(\v2).memos.api.v1.UserSetting.WebhooksSettingH\x00R\x0fwebhooksSetting\x12J\n" +
"\ftags_setting\x18\x06 \x01(\v2%.memos.api.v1.UserSetting.TagsSettingH\x00R\vtagsSetting\x1av\n" +
"\x0eGeneralSetting\x12\x1b\n" +
"\x06locale\x18\x01 \x01(\tB\x03\xe0A\x01R\x06locale\x12,\n" +
"\x0fmemo_visibility\x18\x03 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility\x12\x19\n" +
"\x05theme\x18\x04 \x01(\tB\x03\xe0A\x01R\x05theme\x1aH\n" +
"\x05theme\x18\x04 \x01(\tB\x03\xe0A\x01R\x05theme\x1ay\n" +
"\vTagMetadata\x12B\n" +
"\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorB\x03\xe0A\x01R\x0fbackgroundColor\x12&\n" +
"\fblur_content\x18\x02 \x01(\bB\x03\xe0A\x01R\vblurContent\x1a\xb7\x01\n" +
"\vTagsSetting\x12H\n" +
"\x04tags\x18\x01 \x03(\v2/.memos.api.v1.UserSetting.TagsSetting.TagsEntryB\x03\xe0A\x01R\x04tags\x1a^\n" +
"\tTagsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12;\n" +
"\x05value\x18\x02 \x01(\v2%.memos.api.v1.UserSetting.TagMetadataR\x05value:\x028\x01\x1aH\n" +
"\x0fWebhooksSetting\x125\n" +
"\bwebhooks\x18\x01 \x03(\v2\x19.memos.api.v1.UserWebhookR\bwebhooks\"5\n" +
"\bwebhooks\x18\x01 \x03(\v2\x19.memos.api.v1.UserWebhookR\bwebhooks\"?\n" +
"\x03Key\x12\x13\n" +
"\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" +
"\aGENERAL\x10\x01\x12\f\n" +
"\bWEBHOOKS\x10\x04:]\xeaAZ\n" +
"\bWEBHOOKS\x10\x04\x12\b\n" +
"\x04TAGS\x10\x05:]\xeaAZ\n" +
"\x18memos.api.v1/UserSetting\x12#users/{username}/settings/{setting}*\fuserSettings2\vuserSettingB\a\n" +
"\x05value\"M\n" +
"\x15GetUserSettingRequest\x124\n" +
@@ -3439,7 +3573,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte {
}
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 47)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 50)
var file_api_v1_user_service_proto_goTypes = []any{
(User_Role)(0), // 0: memos.api.v1.User.Role
(UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key
@@ -3489,113 +3623,121 @@ var file_api_v1_user_service_proto_goTypes = []any{
(*UserStats_MemoTypeStats)(nil), // 45: memos.api.v1.UserStats.MemoTypeStats
nil, // 46: memos.api.v1.UserStats.TagCountEntry
(*UserSetting_GeneralSetting)(nil), // 47: memos.api.v1.UserSetting.GeneralSetting
(*UserSetting_WebhooksSetting)(nil), // 48: memos.api.v1.UserSetting.WebhooksSetting
(*UserNotification_MemoCommentPayload)(nil), // 49: memos.api.v1.UserNotification.MemoCommentPayload
(*UserNotification_MemoMentionPayload)(nil), // 50: memos.api.v1.UserNotification.MemoMentionPayload
(State)(0), // 51: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 53: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 54: google.protobuf.Empty
(*UserSetting_TagMetadata)(nil), // 48: memos.api.v1.UserSetting.TagMetadata
(*UserSetting_TagsSetting)(nil), // 49: memos.api.v1.UserSetting.TagsSetting
(*UserSetting_WebhooksSetting)(nil), // 50: memos.api.v1.UserSetting.WebhooksSetting
nil, // 51: memos.api.v1.UserSetting.TagsSetting.TagsEntry
(*UserNotification_MemoCommentPayload)(nil), // 52: memos.api.v1.UserNotification.MemoCommentPayload
(*UserNotification_MemoMentionPayload)(nil), // 53: memos.api.v1.UserNotification.MemoMentionPayload
(State)(0), // 54: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 55: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 56: google.protobuf.FieldMask
(*color.Color)(nil), // 57: google.type.Color
(*emptypb.Empty)(nil), // 58: google.protobuf.Empty
}
var file_api_v1_user_service_proto_depIdxs = []int32{
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
51, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
52, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
52, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
54, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
55, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
55, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
4, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
4, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User
53, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
56, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
4, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
4, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
53, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
56, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
45, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
46, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
52, // 12: memos.api.v1.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp
52, // 13: memos.api.v1.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp
51, // 14: memos.api.v1.ListAllUserStatsRequest.state:type_name -> memos.api.v1.State
55, // 12: memos.api.v1.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp
55, // 13: memos.api.v1.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp
54, // 14: memos.api.v1.ListAllUserStatsRequest.state:type_name -> memos.api.v1.State
13, // 15: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
47, // 16: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
48, // 17: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
17, // 18: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
53, // 19: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
17, // 20: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
22, // 21: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity
52, // 22: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
52, // 23: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
52, // 24: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
28, // 25: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
28, // 26: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
52, // 27: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
52, // 28: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
34, // 29: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
34, // 30: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
34, // 31: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
53, // 32: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
4, // 33: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
2, // 34: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
52, // 35: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
3, // 36: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
49, // 37: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
50, // 38: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
40, // 39: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
40, // 40: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
53, // 41: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
34, // 42: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
5, // 43: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
7, // 44: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
9, // 45: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
10, // 46: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
11, // 47: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
12, // 48: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
15, // 49: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
14, // 50: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
18, // 51: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
19, // 52: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
20, // 53: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
23, // 54: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest
25, // 55: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest
26, // 56: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest
27, // 57: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest
29, // 58: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
31, // 59: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
33, // 60: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
35, // 61: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
37, // 62: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
38, // 63: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
39, // 64: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
41, // 65: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
43, // 66: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
44, // 67: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
6, // 68: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
8, // 69: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
4, // 70: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
4, // 71: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
4, // 72: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
54, // 73: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
16, // 74: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
13, // 75: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
17, // 76: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
17, // 77: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
21, // 78: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
24, // 79: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse
22, // 80: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
22, // 81: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
54, // 82: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty
30, // 83: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
32, // 84: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
54, // 85: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
36, // 86: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
34, // 87: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
34, // 88: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
54, // 89: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
42, // 90: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
40, // 91: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
54, // 92: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
68, // [68:93] is the sub-list for method output_type
43, // [43:68] is the sub-list for method input_type
43, // [43:43] is the sub-list for extension type_name
43, // [43:43] is the sub-list for extension extendee
0, // [0:43] is the sub-list for field type_name
50, // 17: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
49, // 18: memos.api.v1.UserSetting.tags_setting:type_name -> memos.api.v1.UserSetting.TagsSetting
17, // 19: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
56, // 20: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
17, // 21: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
22, // 22: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity
55, // 23: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
55, // 24: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
55, // 25: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
28, // 26: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
28, // 27: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
55, // 28: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
55, // 29: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
34, // 30: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
34, // 31: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
34, // 32: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
56, // 33: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
4, // 34: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
2, // 35: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
55, // 36: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
3, // 37: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
52, // 38: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
53, // 39: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
40, // 40: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
40, // 41: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
56, // 42: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
57, // 43: memos.api.v1.UserSetting.TagMetadata.background_color:type_name -> google.type.Color
51, // 44: memos.api.v1.UserSetting.TagsSetting.tags:type_name -> memos.api.v1.UserSetting.TagsSetting.TagsEntry
34, // 45: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
48, // 46: memos.api.v1.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.UserSetting.TagMetadata
5, // 47: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
7, // 48: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
9, // 49: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
10, // 50: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
11, // 51: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
12, // 52: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
15, // 53: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
14, // 54: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
18, // 55: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
19, // 56: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
20, // 57: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
23, // 58: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest
25, // 59: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest
26, // 60: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest
27, // 61: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest
29, // 62: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
31, // 63: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
33, // 64: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
35, // 65: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
37, // 66: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
38, // 67: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
39, // 68: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
41, // 69: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
43, // 70: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
44, // 71: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
6, // 72: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
8, // 73: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
4, // 74: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
4, // 75: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
4, // 76: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
58, // 77: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
16, // 78: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
13, // 79: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
17, // 80: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
17, // 81: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
21, // 82: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
24, // 83: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse
22, // 84: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
22, // 85: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
58, // 86: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty
30, // 87: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
32, // 88: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
58, // 89: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
36, // 90: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
34, // 91: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
34, // 92: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
58, // 93: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
42, // 94: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
40, // 95: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
58, // 96: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
72, // [72:97] is the sub-list for method output_type
47, // [47:72] is the sub-list for method input_type
47, // [47:47] is the sub-list for extension type_name
47, // [47:47] is the sub-list for extension extendee
0, // [0:47] is the sub-list for field type_name
}
func init() { file_api_v1_user_service_proto_init() }
@@ -3607,6 +3749,7 @@ func file_api_v1_user_service_proto_init() {
file_api_v1_user_service_proto_msgTypes[13].OneofWrappers = []any{
(*UserSetting_GeneralSetting_)(nil),
(*UserSetting_WebhooksSetting_)(nil),
(*UserSetting_TagsSetting_)(nil),
}
file_api_v1_user_service_proto_msgTypes[36].OneofWrappers = []any{
(*UserNotification_MemoComment)(nil),
@@ -3618,7 +3761,7 @@ func file_api_v1_user_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),
NumEnums: 4,
NumMessages: 47,
NumMessages: 50,
NumExtensions: 0,
NumServices: 1,
},
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: api/v1/user_service.proto
+30 -1
View File
@@ -2878,7 +2878,10 @@ components:
Each key is treated as an anchored regular expression (^pattern$),
so a single entry like "project/.*" matches all tags under that prefix.
Exact tag names are also valid (they are trivially valid regex patterns).
description: Tag metadata configuration.
description: |-
Tag metadata configuration.
Active tag metadata is stored in per-user tag settings.
This message remains for backward compatibility with existing clients and migrations.
InstanceSetting_TranscriptionConfig:
type: object
properties:
@@ -3835,6 +3838,8 @@ components:
$ref: '#/components/schemas/UserSetting_GeneralSetting'
webhooksSetting:
$ref: '#/components/schemas/UserSetting_WebhooksSetting'
tagsSetting:
$ref: '#/components/schemas/UserSetting_TagsSetting'
description: User settings message
UserSetting_GeneralSetting:
type: object
@@ -3852,6 +3857,30 @@ components:
This references a CSS file in the web/public/themes/ directory.
If not set, the default theme will be used.
description: General user settings configuration.
UserSetting_TagMetadata:
type: object
properties:
backgroundColor:
allOf:
- $ref: '#/components/schemas/Color'
description: |-
Optional background color for the tag label.
When unset, the default tag color is used.
blurContent:
type: boolean
description: Whether memos with this tag should have their content blurred.
description: Tag metadata for user-specific display rules.
UserSetting_TagsSetting:
type: object
properties:
tags:
type: object
additionalProperties:
$ref: '#/components/schemas/UserSetting_TagMetadata'
description: |-
Map of tag name pattern to tag metadata.
Each key is treated as an anchored regular expression (^pattern$).
description: User-specific tag metadata.
UserSetting_WebhooksSetting:
type: object
properties:
+2
View File
@@ -867,6 +867,8 @@ func (x *InstanceTagMetadata) GetBlurContent() bool {
return false
}
// Active tag metadata is stored in per-user tag settings.
// This message remains for backward compatibility with existing clients and migrations.
type InstanceTagsSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Map of tag name pattern to tag metadata.
+209 -65
View File
@@ -7,6 +7,7 @@
package store
import (
color "google.golang.org/genproto/googleapis/type/color"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
@@ -36,6 +37,8 @@ const (
UserSetting_REFRESH_TOKENS UserSetting_Key = 6
// Personal access tokens for the user.
UserSetting_PERSONAL_ACCESS_TOKENS UserSetting_Key = 7
// Per-user tag metadata.
UserSetting_TAGS UserSetting_Key = 8
)
// Enum value maps for UserSetting_Key.
@@ -47,6 +50,7 @@ var (
5: "WEBHOOKS",
6: "REFRESH_TOKENS",
7: "PERSONAL_ACCESS_TOKENS",
8: "TAGS",
}
UserSetting_Key_value = map[string]int32{
"KEY_UNSPECIFIED": 0,
@@ -55,6 +59,7 @@ var (
"WEBHOOKS": 5,
"REFRESH_TOKENS": 6,
"PERSONAL_ACCESS_TOKENS": 7,
"TAGS": 8,
}
)
@@ -96,6 +101,7 @@ type UserSetting struct {
// *UserSetting_Webhooks
// *UserSetting_RefreshTokens
// *UserSetting_PersonalAccessTokens
// *UserSetting_Tags
Value isUserSetting_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -197,6 +203,15 @@ func (x *UserSetting) GetPersonalAccessTokens() *PersonalAccessTokensUserSetting
return nil
}
func (x *UserSetting) GetTags() *TagsUserSetting {
if x != nil {
if x, ok := x.Value.(*UserSetting_Tags); ok {
return x.Tags
}
}
return nil
}
type isUserSetting_Value interface {
isUserSetting_Value()
}
@@ -221,6 +236,10 @@ type UserSetting_PersonalAccessTokens struct {
PersonalAccessTokens *PersonalAccessTokensUserSetting `protobuf:"bytes,9,opt,name=personal_access_tokens,json=personalAccessTokens,proto3,oneof"`
}
type UserSetting_Tags struct {
Tags *TagsUserSetting `protobuf:"bytes,10,opt,name=tags,proto3,oneof"`
}
func (*UserSetting_General) isUserSetting_Value() {}
func (*UserSetting_Shortcuts) isUserSetting_Value() {}
@@ -231,6 +250,8 @@ func (*UserSetting_RefreshTokens) isUserSetting_Value() {}
func (*UserSetting_PersonalAccessTokens) isUserSetting_Value() {}
func (*UserSetting_Tags) isUserSetting_Value() {}
type GeneralUserSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The user's locale.
@@ -295,6 +316,109 @@ func (x *GeneralUserSetting) GetTheme() string {
return ""
}
type UserTagMetadata struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Optional background color for the tag label.
// When unset, the default tag color is used.
BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"`
// Whether memos with this tag should have their content blurred.
BlurContent bool `protobuf:"varint,2,opt,name=blur_content,json=blurContent,proto3" json:"blur_content,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserTagMetadata) Reset() {
*x = UserTagMetadata{}
mi := &file_store_user_setting_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserTagMetadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserTagMetadata) ProtoMessage() {}
func (x *UserTagMetadata) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserTagMetadata.ProtoReflect.Descriptor instead.
func (*UserTagMetadata) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{2}
}
func (x *UserTagMetadata) GetBackgroundColor() *color.Color {
if x != nil {
return x.BackgroundColor
}
return nil
}
func (x *UserTagMetadata) GetBlurContent() bool {
if x != nil {
return x.BlurContent
}
return false
}
type TagsUserSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Map of tag name pattern to tag metadata.
// Each key is treated as an anchored regular expression (^pattern$),
// so a single entry like "project/.*" matches all tags under that prefix.
// Exact tag names are also valid.
Tags map[string]*UserTagMetadata `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TagsUserSetting) Reset() {
*x = TagsUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TagsUserSetting) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TagsUserSetting) ProtoMessage() {}
func (x *TagsUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TagsUserSetting.ProtoReflect.Descriptor instead.
func (*TagsUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{3}
}
func (x *TagsUserSetting) GetTags() map[string]*UserTagMetadata {
if x != nil {
return x.Tags
}
return nil
}
type RefreshTokensUserSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
RefreshTokens []*RefreshTokensUserSetting_RefreshToken `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"`
@@ -304,7 +428,7 @@ type RefreshTokensUserSetting struct {
func (x *RefreshTokensUserSetting) Reset() {
*x = RefreshTokensUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[2]
mi := &file_store_user_setting_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -316,7 +440,7 @@ func (x *RefreshTokensUserSetting) String() string {
func (*RefreshTokensUserSetting) ProtoMessage() {}
func (x *RefreshTokensUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[2]
mi := &file_store_user_setting_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -329,7 +453,7 @@ func (x *RefreshTokensUserSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use RefreshTokensUserSetting.ProtoReflect.Descriptor instead.
func (*RefreshTokensUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{2}
return file_store_user_setting_proto_rawDescGZIP(), []int{4}
}
func (x *RefreshTokensUserSetting) GetRefreshTokens() []*RefreshTokensUserSetting_RefreshToken {
@@ -348,7 +472,7 @@ type PersonalAccessTokensUserSetting struct {
func (x *PersonalAccessTokensUserSetting) Reset() {
*x = PersonalAccessTokensUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[3]
mi := &file_store_user_setting_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -360,7 +484,7 @@ func (x *PersonalAccessTokensUserSetting) String() string {
func (*PersonalAccessTokensUserSetting) ProtoMessage() {}
func (x *PersonalAccessTokensUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[3]
mi := &file_store_user_setting_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -373,7 +497,7 @@ func (x *PersonalAccessTokensUserSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use PersonalAccessTokensUserSetting.ProtoReflect.Descriptor instead.
func (*PersonalAccessTokensUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{3}
return file_store_user_setting_proto_rawDescGZIP(), []int{5}
}
func (x *PersonalAccessTokensUserSetting) GetTokens() []*PersonalAccessTokensUserSetting_PersonalAccessToken {
@@ -392,7 +516,7 @@ type ShortcutsUserSetting struct {
func (x *ShortcutsUserSetting) Reset() {
*x = ShortcutsUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[4]
mi := &file_store_user_setting_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -404,7 +528,7 @@ func (x *ShortcutsUserSetting) String() string {
func (*ShortcutsUserSetting) ProtoMessage() {}
func (x *ShortcutsUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[4]
mi := &file_store_user_setting_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -417,7 +541,7 @@ func (x *ShortcutsUserSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use ShortcutsUserSetting.ProtoReflect.Descriptor instead.
func (*ShortcutsUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{4}
return file_store_user_setting_proto_rawDescGZIP(), []int{6}
}
func (x *ShortcutsUserSetting) GetShortcuts() []*ShortcutsUserSetting_Shortcut {
@@ -436,7 +560,7 @@ type WebhooksUserSetting struct {
func (x *WebhooksUserSetting) Reset() {
*x = WebhooksUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[5]
mi := &file_store_user_setting_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -448,7 +572,7 @@ func (x *WebhooksUserSetting) String() string {
func (*WebhooksUserSetting) ProtoMessage() {}
func (x *WebhooksUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[5]
mi := &file_store_user_setting_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -461,7 +585,7 @@ func (x *WebhooksUserSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use WebhooksUserSetting.ProtoReflect.Descriptor instead.
func (*WebhooksUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{5}
return file_store_user_setting_proto_rawDescGZIP(), []int{7}
}
func (x *WebhooksUserSetting) GetWebhooks() []*WebhooksUserSetting_Webhook {
@@ -489,7 +613,7 @@ type RefreshTokensUserSetting_RefreshToken struct {
func (x *RefreshTokensUserSetting_RefreshToken) Reset() {
*x = RefreshTokensUserSetting_RefreshToken{}
mi := &file_store_user_setting_proto_msgTypes[6]
mi := &file_store_user_setting_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -501,7 +625,7 @@ func (x *RefreshTokensUserSetting_RefreshToken) String() string {
func (*RefreshTokensUserSetting_RefreshToken) ProtoMessage() {}
func (x *RefreshTokensUserSetting_RefreshToken) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[6]
mi := &file_store_user_setting_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -514,7 +638,7 @@ func (x *RefreshTokensUserSetting_RefreshToken) ProtoReflect() protoreflect.Mess
// Deprecated: Use RefreshTokensUserSetting_RefreshToken.ProtoReflect.Descriptor instead.
func (*RefreshTokensUserSetting_RefreshToken) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{2, 0}
return file_store_user_setting_proto_rawDescGZIP(), []int{4, 0}
}
func (x *RefreshTokensUserSetting_RefreshToken) GetTokenId() string {
@@ -570,7 +694,7 @@ type RefreshTokensUserSetting_ClientInfo struct {
func (x *RefreshTokensUserSetting_ClientInfo) Reset() {
*x = RefreshTokensUserSetting_ClientInfo{}
mi := &file_store_user_setting_proto_msgTypes[7]
mi := &file_store_user_setting_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -582,7 +706,7 @@ func (x *RefreshTokensUserSetting_ClientInfo) String() string {
func (*RefreshTokensUserSetting_ClientInfo) ProtoMessage() {}
func (x *RefreshTokensUserSetting_ClientInfo) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[7]
mi := &file_store_user_setting_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -595,7 +719,7 @@ func (x *RefreshTokensUserSetting_ClientInfo) ProtoReflect() protoreflect.Messag
// Deprecated: Use RefreshTokensUserSetting_ClientInfo.ProtoReflect.Descriptor instead.
func (*RefreshTokensUserSetting_ClientInfo) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{2, 1}
return file_store_user_setting_proto_rawDescGZIP(), []int{4, 1}
}
func (x *RefreshTokensUserSetting_ClientInfo) GetUserAgent() string {
@@ -653,7 +777,7 @@ type PersonalAccessTokensUserSetting_PersonalAccessToken struct {
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) Reset() {
*x = PersonalAccessTokensUserSetting_PersonalAccessToken{}
mi := &file_store_user_setting_proto_msgTypes[8]
mi := &file_store_user_setting_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -665,7 +789,7 @@ func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) String() string {
func (*PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoMessage() {}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[8]
mi := &file_store_user_setting_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -678,7 +802,7 @@ func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoReflect() pro
// Deprecated: Use PersonalAccessTokensUserSetting_PersonalAccessToken.ProtoReflect.Descriptor instead.
func (*PersonalAccessTokensUserSetting_PersonalAccessToken) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{3, 0}
return file_store_user_setting_proto_rawDescGZIP(), []int{5, 0}
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetTokenId() string {
@@ -734,7 +858,7 @@ type ShortcutsUserSetting_Shortcut struct {
func (x *ShortcutsUserSetting_Shortcut) Reset() {
*x = ShortcutsUserSetting_Shortcut{}
mi := &file_store_user_setting_proto_msgTypes[9]
mi := &file_store_user_setting_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -746,7 +870,7 @@ func (x *ShortcutsUserSetting_Shortcut) String() string {
func (*ShortcutsUserSetting_Shortcut) ProtoMessage() {}
func (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[9]
mi := &file_store_user_setting_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -759,7 +883,7 @@ func (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message {
// Deprecated: Use ShortcutsUserSetting_Shortcut.ProtoReflect.Descriptor instead.
func (*ShortcutsUserSetting_Shortcut) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{4, 0}
return file_store_user_setting_proto_rawDescGZIP(), []int{6, 0}
}
func (x *ShortcutsUserSetting_Shortcut) GetId() string {
@@ -797,7 +921,7 @@ type WebhooksUserSetting_Webhook struct {
func (x *WebhooksUserSetting_Webhook) Reset() {
*x = WebhooksUserSetting_Webhook{}
mi := &file_store_user_setting_proto_msgTypes[10]
mi := &file_store_user_setting_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -809,7 +933,7 @@ func (x *WebhooksUserSetting_Webhook) String() string {
func (*WebhooksUserSetting_Webhook) ProtoMessage() {}
func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[10]
mi := &file_store_user_setting_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -822,7 +946,7 @@ func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {
// Deprecated: Use WebhooksUserSetting_Webhook.ProtoReflect.Descriptor instead.
func (*WebhooksUserSetting_Webhook) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{5, 0}
return file_store_user_setting_proto_rawDescGZIP(), []int{7, 0}
}
func (x *WebhooksUserSetting_Webhook) GetId() string {
@@ -850,7 +974,7 @@ var File_store_user_setting_proto protoreflect.FileDescriptor
const file_store_user_setting_proto_rawDesc = "" +
"\n" +
"\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcb\x04\n" +
"\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17google/type/color.proto\"\x89\x05\n" +
"\vUserSetting\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x05R\x06userId\x12.\n" +
"\x03key\x18\x02 \x01(\x0e2\x1c.memos.store.UserSetting.KeyR\x03key\x12;\n" +
@@ -858,19 +982,30 @@ const file_store_user_setting_proto_rawDesc = "" +
"\tshortcuts\x18\x06 \x01(\v2!.memos.store.ShortcutsUserSettingH\x00R\tshortcuts\x12>\n" +
"\bwebhooks\x18\a \x01(\v2 .memos.store.WebhooksUserSettingH\x00R\bwebhooks\x12N\n" +
"\x0erefresh_tokens\x18\b \x01(\v2%.memos.store.RefreshTokensUserSettingH\x00R\rrefreshTokens\x12d\n" +
"\x16personal_access_tokens\x18\t \x01(\v2,.memos.store.PersonalAccessTokensUserSettingH\x00R\x14personalAccessTokens\"t\n" +
"\x16personal_access_tokens\x18\t \x01(\v2,.memos.store.PersonalAccessTokensUserSettingH\x00R\x14personalAccessTokens\x122\n" +
"\x04tags\x18\n" +
" \x01(\v2\x1c.memos.store.TagsUserSettingH\x00R\x04tags\"~\n" +
"\x03Key\x12\x13\n" +
"\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" +
"\aGENERAL\x10\x01\x12\r\n" +
"\tSHORTCUTS\x10\x04\x12\f\n" +
"\bWEBHOOKS\x10\x05\x12\x12\n" +
"\x0eREFRESH_TOKENS\x10\x06\x12\x1a\n" +
"\x16PERSONAL_ACCESS_TOKENS\x10\aB\a\n" +
"\x16PERSONAL_ACCESS_TOKENS\x10\a\x12\b\n" +
"\x04TAGS\x10\bB\a\n" +
"\x05value\"k\n" +
"\x12GeneralUserSetting\x12\x16\n" +
"\x06locale\x18\x01 \x01(\tR\x06locale\x12'\n" +
"\x0fmemo_visibility\x18\x02 \x01(\tR\x0ememoVisibility\x12\x14\n" +
"\x05theme\x18\x03 \x01(\tR\x05theme\"\xa4\x04\n" +
"\x05theme\x18\x03 \x01(\tR\x05theme\"s\n" +
"\x0fUserTagMetadata\x12=\n" +
"\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorR\x0fbackgroundColor\x12!\n" +
"\fblur_content\x18\x02 \x01(\bR\vblurContent\"\xa4\x01\n" +
"\x0fTagsUserSetting\x12:\n" +
"\x04tags\x18\x01 \x03(\v2&.memos.store.TagsUserSetting.TagsEntryR\x04tags\x1aU\n" +
"\tTagsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x122\n" +
"\x05value\x18\x02 \x01(\v2\x1c.memos.store.UserTagMetadataR\x05value:\x028\x01\"\xa4\x04\n" +
"\x18RefreshTokensUserSetting\x12Y\n" +
"\x0erefresh_tokens\x18\x01 \x03(\v22.memos.store.RefreshTokensUserSetting.RefreshTokenR\rrefreshTokens\x1a\x94\x02\n" +
"\fRefreshToken\x12\x19\n" +
@@ -932,44 +1067,52 @@ func file_store_user_setting_proto_rawDescGZIP() []byte {
}
var file_store_user_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_store_user_setting_proto_goTypes = []any{
(UserSetting_Key)(0), // 0: memos.store.UserSetting.Key
(*UserSetting)(nil), // 1: memos.store.UserSetting
(*GeneralUserSetting)(nil), // 2: memos.store.GeneralUserSetting
(*RefreshTokensUserSetting)(nil), // 3: memos.store.RefreshTokensUserSetting
(*PersonalAccessTokensUserSetting)(nil), // 4: memos.store.PersonalAccessTokensUserSetting
(*ShortcutsUserSetting)(nil), // 5: memos.store.ShortcutsUserSetting
(*WebhooksUserSetting)(nil), // 6: memos.store.WebhooksUserSetting
(*RefreshTokensUserSetting_RefreshToken)(nil), // 7: memos.store.RefreshTokensUserSetting.RefreshToken
(*RefreshTokensUserSetting_ClientInfo)(nil), // 8: memos.store.RefreshTokensUserSetting.ClientInfo
(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 9: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
(*ShortcutsUserSetting_Shortcut)(nil), // 10: memos.store.ShortcutsUserSetting.Shortcut
(*WebhooksUserSetting_Webhook)(nil), // 11: memos.store.WebhooksUserSetting.Webhook
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
(UserSetting_Key)(0), // 0: memos.store.UserSetting.Key
(*UserSetting)(nil), // 1: memos.store.UserSetting
(*GeneralUserSetting)(nil), // 2: memos.store.GeneralUserSetting
(*UserTagMetadata)(nil), // 3: memos.store.UserTagMetadata
(*TagsUserSetting)(nil), // 4: memos.store.TagsUserSetting
(*RefreshTokensUserSetting)(nil), // 5: memos.store.RefreshTokensUserSetting
(*PersonalAccessTokensUserSetting)(nil), // 6: memos.store.PersonalAccessTokensUserSetting
(*ShortcutsUserSetting)(nil), // 7: memos.store.ShortcutsUserSetting
(*WebhooksUserSetting)(nil), // 8: memos.store.WebhooksUserSetting
nil, // 9: memos.store.TagsUserSetting.TagsEntry
(*RefreshTokensUserSetting_RefreshToken)(nil), // 10: memos.store.RefreshTokensUserSetting.RefreshToken
(*RefreshTokensUserSetting_ClientInfo)(nil), // 11: memos.store.RefreshTokensUserSetting.ClientInfo
(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 12: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
(*ShortcutsUserSetting_Shortcut)(nil), // 13: memos.store.ShortcutsUserSetting.Shortcut
(*WebhooksUserSetting_Webhook)(nil), // 14: memos.store.WebhooksUserSetting.Webhook
(*color.Color)(nil), // 15: google.type.Color
(*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
}
var file_store_user_setting_proto_depIdxs = []int32{
0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key
2, // 1: memos.store.UserSetting.general:type_name -> memos.store.GeneralUserSetting
5, // 2: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting
6, // 3: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting
3, // 4: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting
4, // 5: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting
7, // 6: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken
9, // 7: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
10, // 8: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut
11, // 9: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
12, // 10: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp
12, // 11: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp
8, // 12: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo
12, // 13: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
12, // 14: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
12, // 15: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
16, // [16:16] is the sub-list for method output_type
16, // [16:16] is the sub-list for method input_type
16, // [16:16] is the sub-list for extension type_name
16, // [16:16] is the sub-list for extension extendee
0, // [0:16] is the sub-list for field type_name
7, // 2: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting
8, // 3: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting
5, // 4: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting
6, // 5: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting
4, // 6: memos.store.UserSetting.tags:type_name -> memos.store.TagsUserSetting
15, // 7: memos.store.UserTagMetadata.background_color:type_name -> google.type.Color
9, // 8: memos.store.TagsUserSetting.tags:type_name -> memos.store.TagsUserSetting.TagsEntry
10, // 9: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken
12, // 10: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
13, // 11: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut
14, // 12: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
3, // 13: memos.store.TagsUserSetting.TagsEntry.value:type_name -> memos.store.UserTagMetadata
16, // 14: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp
16, // 15: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp
11, // 16: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo
16, // 17: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
16, // 18: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
16, // 19: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
20, // [20:20] is the sub-list for method output_type
20, // [20:20] is the sub-list for method input_type
20, // [20:20] is the sub-list for extension type_name
20, // [20:20] is the sub-list for extension extendee
0, // [0:20] is the sub-list for field type_name
}
func init() { file_store_user_setting_proto_init() }
@@ -983,6 +1126,7 @@ func file_store_user_setting_proto_init() {
(*UserSetting_Webhooks)(nil),
(*UserSetting_RefreshTokens)(nil),
(*UserSetting_PersonalAccessTokens)(nil),
(*UserSetting_Tags)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -990,7 +1134,7 @@ func file_store_user_setting_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)),
NumEnums: 1,
NumMessages: 11,
NumMessages: 14,
NumExtensions: 0,
NumServices: 0,
},
+2
View File
@@ -121,6 +121,8 @@ message InstanceTagMetadata {
bool blur_content = 2;
}
// Active tag metadata is stored in per-user tag settings.
// This message remains for backward compatibility with existing clients and migrations.
message InstanceTagsSetting {
// Map of tag name pattern to tag metadata.
// Each key is treated as an anchored regular expression (^pattern$),
+20
View File
@@ -3,6 +3,7 @@ syntax = "proto3";
package memos.store;
import "google/protobuf/timestamp.proto";
import "google/type/color.proto";
option go_package = "gen/store";
@@ -19,6 +20,8 @@ message UserSetting {
REFRESH_TOKENS = 6;
// Personal access tokens for the user.
PERSONAL_ACCESS_TOKENS = 7;
// Per-user tag metadata.
TAGS = 8;
}
int32 user_id = 1;
@@ -30,6 +33,7 @@ message UserSetting {
WebhooksUserSetting webhooks = 7;
RefreshTokensUserSetting refresh_tokens = 8;
PersonalAccessTokensUserSetting personal_access_tokens = 9;
TagsUserSetting tags = 10;
}
}
@@ -43,6 +47,22 @@ message GeneralUserSetting {
string theme = 3;
}
message UserTagMetadata {
// Optional background color for the tag label.
// When unset, the default tag color is used.
google.type.Color background_color = 1;
// Whether memos with this tag should have their content blurred.
bool blur_content = 2;
}
message TagsUserSetting {
// Map of tag name pattern to tag metadata.
// Each key is treated as an anchored regular expression (^pattern$),
// so a single entry like "project/.*" matches all tags under that prefix.
// Exact tag names are also valid.
map<string, UserTagMetadata> tags = 1;
}
message RefreshTokensUserSetting {
message RefreshToken {
// Unique identifier (matches 'tid' claim in JWT)
@@ -5,6 +5,8 @@ import (
"testing"
"github.com/stretchr/testify/require"
colorpb "google.golang.org/genproto/googleapis/type/color"
"google.golang.org/protobuf/types/known/fieldmaskpb"
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
@@ -56,3 +58,149 @@ func TestListUserSettingsOmitsInternalStoreSettings(t *testing.T) {
require.Equal(t, "users/locale-user/settings/GENERAL", resp.Settings[0].Name)
require.Equal(t, "ja", resp.Settings[0].GetGeneralSetting().Locale)
}
func TestUserTagSettings(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "tag-user")
require.NoError(t, err)
userCtx := ts.CreateUserContext(ctx, user.ID)
tagsSetting := &apiv1.UserSetting{
Name: "users/tag-user/settings/TAGS",
Value: &apiv1.UserSetting_TagsSetting_{
TagsSetting: &apiv1.UserSetting_TagsSetting{
Tags: map[string]*apiv1.UserSetting_TagMetadata{
"bug": {
BackgroundColor: &colorpb.Color{
Red: 0.9,
Green: 0.1,
Blue: 0.1,
},
},
"private/.*": {
BlurContent: true,
},
},
},
},
}
updated, err := ts.Service.UpdateUserSetting(userCtx, &apiv1.UpdateUserSettingRequest{
Setting: tagsSetting,
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
})
require.NoError(t, err)
require.Equal(t, "users/tag-user/settings/TAGS", updated.Name)
require.Contains(t, updated.GetTagsSetting().GetTags(), "bug")
require.True(t, updated.GetTagsSetting().GetTags()["private/.*"].BlurContent)
got, err := ts.Service.GetUserSetting(userCtx, &apiv1.GetUserSettingRequest{Name: "users/tag-user/settings/TAGS"})
require.NoError(t, err)
require.Contains(t, got.GetTagsSetting().GetTags(), "bug")
resp, err := ts.Service.ListUserSettings(userCtx, &apiv1.ListUserSettingsRequest{
Parent: apiv1server.BuildUserName(user.Username),
})
require.NoError(t, err)
var foundTags bool
for _, setting := range resp.Settings {
if setting.GetTagsSetting() != nil {
foundTags = true
require.Equal(t, "users/tag-user/settings/TAGS", setting.Name)
}
}
require.True(t, foundTags)
}
func TestUserTagSettingsRejectInvalidInput(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "invalid-tag-user")
require.NoError(t, err)
userCtx := ts.CreateUserContext(ctx, user.ID)
_, err = ts.Service.UpdateUserSetting(userCtx, &apiv1.UpdateUserSettingRequest{
Setting: &apiv1.UserSetting{
Name: "users/invalid-tag-user/settings/TAGS",
Value: &apiv1.UserSetting_TagsSetting_{
TagsSetting: &apiv1.UserSetting_TagsSetting{
Tags: map[string]*apiv1.UserSetting_TagMetadata{
"(": {},
},
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid")
_, err = ts.Service.UpdateUserSetting(userCtx, &apiv1.UpdateUserSettingRequest{
Setting: &apiv1.UserSetting{
Name: "users/invalid-tag-user/settings/TAGS",
Value: &apiv1.UserSetting_TagsSetting_{
TagsSetting: &apiv1.UserSetting_TagsSetting{
Tags: map[string]*apiv1.UserSetting_TagMetadata{
"bug": {
BackgroundColor: &colorpb.Color{Red: 1.2},
},
},
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid")
_, err = ts.Service.UpdateUserSetting(userCtx, &apiv1.UpdateUserSettingRequest{
Setting: &apiv1.UserSetting{
Name: "users/invalid-tag-user/settings/TAGS",
Value: &apiv1.UserSetting_TagsSetting_{
TagsSetting: &apiv1.UserSetting_TagsSetting{
Tags: map[string]*apiv1.UserSetting_TagMetadata{
"bug": {},
},
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"locale"}},
})
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported update mask path")
}
func TestUserTagSettingsPermissionDeniedForOtherUser(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
_, err := ts.CreateRegularUser(ctx, "tag-owner")
require.NoError(t, err)
other, err := ts.CreateRegularUser(ctx, "tag-other")
require.NoError(t, err)
otherCtx := ts.CreateUserContext(ctx, other.ID)
_, err = ts.Service.GetUserSetting(otherCtx, &apiv1.GetUserSettingRequest{Name: "users/tag-owner/settings/TAGS"})
require.Error(t, err)
require.Contains(t, err.Error(), "permission denied")
_, err = ts.Service.UpdateUserSetting(otherCtx, &apiv1.UpdateUserSettingRequest{
Setting: &apiv1.UserSetting{
Name: "users/tag-owner/settings/TAGS",
Value: &apiv1.UserSetting_TagsSetting_{
TagsSetting: &apiv1.UserSetting_TagsSetting{
Tags: map[string]*apiv1.UserSetting_TagMetadata{"bug": {}},
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
})
require.Error(t, err)
require.Contains(t, err.Error(), "permission denied")
}
+106
View File
@@ -37,6 +37,29 @@ func validatePassword(password string) error {
return nil
}
func validateUserTagsSetting(setting *v1pb.UserSetting_TagsSetting) error {
if setting == nil {
return errors.New("tags setting is required")
}
for tag, metadata := range setting.Tags {
if strings.TrimSpace(tag) == "" {
return errors.New("tag key cannot be empty")
}
if _, err := regexp.Compile(tag); err != nil {
return errors.Wrapf(err, "tag key %q is not a valid regex pattern", tag)
}
if metadata == nil {
return errors.Errorf("tag metadata is required for %q", tag)
}
if metadata.GetBackgroundColor() != nil {
if err := validateInstanceColor(metadata.GetBackgroundColor()); err != nil {
return errors.Wrapf(err, "background_color for %q", tag)
}
}
}
return nil
}
func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
@@ -626,6 +649,33 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
GeneralSetting: updatedGeneral,
},
}
case storepb.UserSetting_TAGS:
var shouldUpdateTags bool
for _, field := range request.UpdateMask.Paths {
switch field {
case "tags":
shouldUpdateTags = true
default:
return nil, status.Errorf(codes.InvalidArgument, "unsupported update mask path for tags setting: %s", field)
}
}
if !shouldUpdateTags {
return nil, status.Errorf(codes.InvalidArgument, "update mask must include tags")
}
incomingTags := request.Setting.GetTagsSetting()
if incomingTags == nil {
return nil, status.Errorf(codes.InvalidArgument, "tags setting is required")
}
if err := validateUserTagsSetting(incomingTags); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user tags setting: %v", err)
}
updatedSetting = &v1pb.UserSetting{
Name: request.Setting.Name,
Value: &v1pb.UserSetting_TagsSetting_{
TagsSetting: incomingTags,
},
}
default:
return nil, status.Errorf(codes.InvalidArgument, "setting type %s should not be updated via UpdateUserSetting", storeKey.String())
}
@@ -1286,6 +1336,8 @@ func convertSettingKeyToStore(key string) (storepb.UserSetting_Key, error) {
return storepb.UserSetting_GENERAL, nil
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]:
return storepb.UserSetting_WEBHOOKS, nil
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_TAGS)]:
return storepb.UserSetting_TAGS, nil
default:
return storepb.UserSetting_KEY_UNSPECIFIED, errors.Errorf("unknown setting key: %s", key)
}
@@ -1300,11 +1352,49 @@ func convertSettingKeyFromStore(key storepb.UserSetting_Key) string {
return "SHORTCUTS" // Not defined in API proto
case storepb.UserSetting_WEBHOOKS:
return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]
case storepb.UserSetting_TAGS:
return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_TAGS)]
default:
return "unknown"
}
}
func convertUserTagsSettingFromStore(setting *storepb.TagsUserSetting) *v1pb.UserSetting_TagsSetting {
if setting == nil {
return &v1pb.UserSetting_TagsSetting{Tags: map[string]*v1pb.UserSetting_TagMetadata{}}
}
tags := make(map[string]*v1pb.UserSetting_TagMetadata, len(setting.Tags))
for tag, metadata := range setting.Tags {
if metadata == nil {
tags[tag] = &v1pb.UserSetting_TagMetadata{}
continue
}
tags[tag] = &v1pb.UserSetting_TagMetadata{
BackgroundColor: metadata.GetBackgroundColor(),
BlurContent: metadata.GetBlurContent(),
}
}
return &v1pb.UserSetting_TagsSetting{Tags: tags}
}
func convertUserTagsSettingToStore(setting *v1pb.UserSetting_TagsSetting) *storepb.TagsUserSetting {
if setting == nil {
return &storepb.TagsUserSetting{Tags: map[string]*storepb.UserTagMetadata{}}
}
tags := make(map[string]*storepb.UserTagMetadata, len(setting.Tags))
for tag, metadata := range setting.Tags {
if metadata == nil {
tags[tag] = &storepb.UserTagMetadata{}
continue
}
tags[tag] = &storepb.UserTagMetadata{
BackgroundColor: metadata.GetBackgroundColor(),
BlurContent: metadata.GetBlurContent(),
}
}
return &storepb.TagsUserSetting{Tags: tags}
}
// convertUserSettingFromStore converts store UserSetting to API UserSetting.
func convertUserSettingFromStore(storeSetting *storepb.UserSetting, user *store.User, key storepb.UserSetting_Key) *v1pb.UserSetting {
if storeSetting == nil {
@@ -1325,6 +1415,10 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, user *store.
Webhooks: []*v1pb.UserWebhook{},
},
}
case storepb.UserSetting_TAGS:
setting.Value = &v1pb.UserSetting_TagsSetting_{
TagsSetting: &v1pb.UserSetting_TagsSetting{Tags: map[string]*v1pb.UserSetting_TagMetadata{}},
}
default:
return nil
}
@@ -1370,6 +1464,10 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, user *store.
Webhooks: apiWebhooks,
},
}
case storepb.UserSetting_TAGS:
setting.Value = &v1pb.UserSetting_TagsSetting_{
TagsSetting: convertUserTagsSettingFromStore(storeSetting.GetTags()),
}
default:
return nil
}
@@ -1416,6 +1514,14 @@ func convertUserSettingToStore(apiSetting *v1pb.UserSetting, userID int32, key s
} else {
return nil, errors.Errorf("webhooks setting is required")
}
case storepb.UserSetting_TAGS:
if tags := apiSetting.GetTagsSetting(); tags != nil {
storeSetting.Value = &storepb.UserSetting_Tags{
Tags: convertUserTagsSettingToStore(tags),
}
} else {
return nil, errors.Errorf("tags setting is required")
}
default:
return nil, errors.Errorf("unsupported setting key: %v", key)
}
@@ -0,0 +1,10 @@
INSERT IGNORE INTO `user_setting` (`user_id`, `key`, `value`)
SELECT
`user`.`id`,
'TAGS',
`system_setting`.`value`
FROM `user`
JOIN `system_setting` ON `system_setting`.`name` = 'TAGS'
WHERE `system_setting`.`value` IS NOT NULL
AND `system_setting`.`value` != ''
AND `system_setting`.`value` != '{}';
@@ -0,0 +1,11 @@
INSERT INTO user_setting (user_id, key, value)
SELECT
"user".id,
'TAGS',
system_setting.value
FROM "user"
JOIN system_setting ON system_setting.name = 'TAGS'
WHERE system_setting.value IS NOT NULL
AND system_setting.value != ''
AND system_setting.value != '{}'
ON CONFLICT (user_id, key) DO NOTHING;
@@ -0,0 +1,10 @@
INSERT OR IGNORE INTO user_setting (user_id, key, value)
SELECT
user.id,
'TAGS',
system_setting.value
FROM user
JOIN system_setting ON system_setting.name = 'TAGS'
WHERE system_setting.value IS NOT NULL
AND system_setting.value != ''
AND system_setting.value != '{}';
+98
View File
@@ -2,12 +2,16 @@ package test
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
colorpb "google.golang.org/genproto/googleapis/type/color"
"google.golang.org/protobuf/encoding/protojson"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
@@ -110,6 +114,100 @@ func TestMigrationMultipleReRuns(t *testing.T) {
require.Equal(t, initialVersion, finalVersion, "version should remain unchanged after multiple re-runs")
}
// TestMigrationCopiesInstanceTagsToUserSettings verifies instance tag metadata is copied into user settings.
func TestMigrationCopiesInstanceTagsToUserSettings(t *testing.T) {
if getDriverFromEnv() != "sqlite" {
t.Skip("skipping focused migration fixture for non-sqlite driver")
}
ctx := context.Background()
dsn := fmt.Sprintf("%s/memos_tag_migration.db", t.TempDir())
db, err := sql.Open("sqlite", dsn)
require.NoError(t, err)
_, err = db.ExecContext(ctx, `
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'USER'
);
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT
);
`)
require.NoError(t, err)
basicSettingBytes, err := protojson.Marshal(&storepb.InstanceBasicSetting{SchemaVersion: "0.29.1"})
require.NoError(t, err)
tagsSettingBytes, err := protojson.Marshal(&storepb.InstanceTagsSetting{
Tags: map[string]*storepb.InstanceTagMetadata{
"bug": {
BackgroundColor: &colorpb.Color{Red: 0.9, Green: 0.1, Blue: 0.1},
},
"private/.*": {
BlurContent: true,
},
},
})
require.NoError(t, err)
existingUserTagsBytes, err := protojson.Marshal(&storepb.TagsUserSetting{
Tags: map[string]*storepb.UserTagMetadata{
"existing": {
BlurContent: true,
},
},
})
require.NoError(t, err)
_, err = db.ExecContext(ctx, "INSERT INTO system_setting (name, value) VALUES ('BASIC', ?), ('TAGS', ?)", string(basicSettingBytes), string(tagsSettingBytes))
require.NoError(t, err)
_, err = db.ExecContext(ctx, "INSERT INTO user (id, username, role) VALUES (1, 'tag-owner', 'USER'), (2, 'keeps-existing', 'USER')")
require.NoError(t, err)
_, err = db.ExecContext(ctx, "INSERT INTO user_setting (user_id, key, value) VALUES (2, 'TAGS', ?)", string(existingUserTagsBytes))
require.NoError(t, err)
require.NoError(t, db.Close())
ts := NewTestingStoreWithDSN(ctx, t, "sqlite", dsn)
require.NoError(t, ts.Migrate(ctx))
defer ts.Close()
copiedUserID := int32(1)
copied, err := ts.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &copiedUserID,
Key: storepb.UserSetting_TAGS,
})
require.NoError(t, err)
require.Contains(t, copied.GetTags().GetTags(), "bug")
bugMetadata := copied.GetTags().GetTags()["bug"]
require.NotNil(t, bugMetadata.GetBackgroundColor())
require.InDelta(t, 0.9, bugMetadata.GetBackgroundColor().GetRed(), 1e-6)
require.InDelta(t, 0.1, bugMetadata.GetBackgroundColor().GetGreen(), 1e-6)
require.InDelta(t, 0.1, bugMetadata.GetBackgroundColor().GetBlue(), 1e-6)
require.True(t, copied.GetTags().GetTags()["private/.*"].GetBlurContent())
existingUserID := int32(2)
existing, err := ts.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &existingUserID,
Key: storepb.UserSetting_TAGS,
})
require.NoError(t, err)
require.Contains(t, existing.GetTags().GetTags(), "existing")
require.NotContains(t, existing.GetTags().GetTags(), "bug")
}
// TestMigrationFromStableVersion verifies that upgrading from a stable Memos version
// to the current version works correctly. This is the critical upgrade path test.
//
+13
View File
@@ -435,6 +435,12 @@ func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) {
return nil, err
}
userSetting.Value = &storepb.UserSetting_General{General: generalUserSetting}
case storepb.UserSetting_TAGS:
tagsUserSetting := &storepb.TagsUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), tagsUserSetting); err != nil {
return nil, errors.Wrap(err, "unmarshal tags user setting")
}
userSetting.Value = &storepb.UserSetting_Tags{Tags: tagsUserSetting}
case storepb.UserSetting_REFRESH_TOKENS:
refreshTokensUserSetting := &storepb.RefreshTokensUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), refreshTokensUserSetting); err != nil {
@@ -480,6 +486,13 @@ func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, er
return nil, err
}
raw.Value = string(value)
case storepb.UserSetting_TAGS:
tagsUserSetting := userSetting.GetTags()
value, err := protojson.Marshal(tagsUserSetting)
if err != nil {
return nil, errors.Wrap(err, "marshal tags user setting")
}
raw.Value = string(value)
case storepb.UserSetting_REFRESH_TOKENS:
refreshTokensUserSetting := userSetting.GetRefreshTokens()
value, err := protojson.Marshal(refreshTokensUserSetting)
+5 -4
View File
@@ -1,6 +1,6 @@
import type { Element } from "hast";
import { useLocation } from "react-router-dom";
import { useInstance } from "@/contexts/InstanceContext";
import { useAuth } from "@/contexts/AuthContext";
import { type MemoFilter, stringifyFilters, useMemoFilterContext } from "@/contexts/MemoFilterContext";
import useNavigateTo from "@/hooks/useNavigateTo";
import { colorToHex } from "@/lib/color";
@@ -20,14 +20,15 @@ export const Tag: React.FC<TagProps> = ({ "data-tag": dataTag, children, classNa
const location = useLocation();
const navigateTo = useNavigateTo();
const { getFiltersByFactor, removeFilter, addFilter } = useMemoFilterContext();
const { tagsSetting } = useInstance();
const { userTagsSetting } = useAuth();
const tag = dataTag || "";
// Custom color from admin tag metadata. Dynamic hex values must use inline styles
// Custom color from user tag metadata. Dynamic hex values must use inline styles
// because Tailwind can't scan dynamically constructed class names.
// Text uses a darkened variant (40% color + black) for contrast on light backgrounds.
const bgHex = colorToHex(findTagMetadata(tag, tagsSetting)?.backgroundColor);
const metadata = userTagsSetting ? findTagMetadata(tag, userTagsSetting) : undefined;
const bgHex = colorToHex(metadata?.backgroundColor);
const tagStyle: React.CSSProperties | undefined = bgHex
? {
borderColor: bgHex,
+4 -4
View File
@@ -1,6 +1,6 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { useInstance } from "@/contexts/InstanceContext";
import { useAuth } from "@/contexts/AuthContext";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useUser } from "@/hooks/useUserQueries";
import { findTagMetadata } from "@/lib/tag";
@@ -23,15 +23,15 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const [cardWidth, setCardWidth] = useState(0);
const currentUser = useCurrentUser();
const { tagsSetting } = useInstance();
const { userTagsSetting } = useAuth();
const creator = useUser(memoData.creator).data;
const isArchived = memoData.state === State.ARCHIVED;
const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser);
const parentPage = parentPageProp || "/";
// Blur content when any tag has blur_content enabled in the instance tag settings.
// Blur content when any tag has blur_content enabled in the current user's tag settings.
const [showBlurredContent, setShowBlurredContent] = useState(false);
const blurred = memoData.tags?.some((tag) => findTagMetadata(tag, tagsSetting)?.blurContent) ?? false;
const blurred = memoData.tags?.some((tag) => userTagsSetting && findTagMetadata(tag, userTagsSetting)?.blurContent) ?? false;
const toggleBlurVisibility = useCallback(() => setShowBlurredContent((prev) => !prev), []);
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
+24 -19
View File
@@ -7,23 +7,23 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { useInstance } from "@/contexts/InstanceContext";
import { useTagCounts } from "@/hooks/useUserQueries";
import { useAuth } from "@/contexts/AuthContext";
import { buildUserSettingName } from "@/helpers/resource-names";
import { useTagCounts, useUpdateUserSetting } from "@/hooks/useUserQueries";
import { colorToHex } from "@/lib/color";
import { isValidTagPattern } from "@/lib/tag";
import { cn } from "@/lib/utils";
import {
InstanceSetting_Key,
InstanceSetting_TagMetadataSchema,
InstanceSetting_TagsSettingSchema,
InstanceSettingSchema,
} from "@/types/proto/api/v1/instance_service_pb";
UserSetting_Key,
UserSetting_TagMetadataSchema,
UserSetting_TagsSettingSchema,
UserSettingSchema,
} from "@/types/proto/api/v1/user_service_pb";
import { ColorSchema } from "@/types/proto/google/type/color_pb";
import { useTranslate } from "@/utils/i18n";
import SettingGroup from "./SettingGroup";
import { SettingList, SettingPanel } from "./SettingList";
import SettingSection from "./SettingSection";
import useInstanceSettingUpdater, { buildInstanceSettingName } from "./useInstanceSettingUpdater";
const DEFAULT_TAG_COLOR = "#ffffff";
@@ -50,9 +50,10 @@ const toLocalTagMeta = (meta: {
const TagsSection = () => {
const t = useTranslate();
const saveInstanceSetting = useInstanceSettingUpdater();
const { tagsSetting: originalSetting } = useInstance();
const { data: tagCounts = {} } = useTagCounts(false);
const { currentUser, userTagsSetting, refetchSettings } = useAuth();
const { mutateAsync: updateUserSetting } = useUpdateUserSetting();
const { data: tagCounts = {} } = useTagCounts(true);
const originalSetting = useMemo(() => userTagsSetting ?? create(UserSetting_TagsSettingSchema, {}), [userTagsSetting]);
// Local state: map of tagName → { color, blur } for editing.
const [localTags, setLocalTags] = useState<Record<string, LocalTagMeta>>(() =>
@@ -130,24 +131,28 @@ const TagsSection = () => {
const tags = Object.fromEntries(
Object.entries(localTags).map(([name, meta]) => [
name,
create(InstanceSetting_TagMetadataSchema, {
create(UserSetting_TagMetadataSchema, {
blurContent: meta.blur,
...(meta.color ? { backgroundColor: hexToColor(meta.color) } : {}),
}),
]),
);
await saveInstanceSetting({
key: InstanceSetting_Key.TAGS,
setting: create(InstanceSettingSchema, {
name: buildInstanceSettingName(InstanceSetting_Key.TAGS),
if (!currentUser) {
return;
}
await updateUserSetting({
setting: create(UserSettingSchema, {
name: buildUserSettingName(currentUser.name, UserSetting_Key.TAGS),
value: {
case: "tagsSetting",
value: create(InstanceSetting_TagsSettingSchema, { tags }),
value: create(UserSetting_TagsSettingSchema, { tags }),
},
}),
errorContext: "Update tags setting",
updateMask: ["tags"],
});
await refetchSettings();
};
return (
@@ -273,7 +278,7 @@ const TagsSection = () => {
</SettingGroup>
<div className="w-full flex justify-end">
<Button disabled={!hasChanges} onClick={handleSave}>
<Button disabled={!hasChanges || !currentUser} onClick={handleSave}>
{t("common.save")}
</Button>
</div>
@@ -98,11 +98,10 @@ export const SETTINGS_SECTIONS: SettingSectionDefinition[] = [
},
{
key: "tags",
scope: "admin",
scope: "basic",
labelKey: "setting.tags.label",
icon: TagsIcon,
component: TagsSection,
preloadSettingKeys: [InstanceSetting_Key.TAGS],
},
{
key: "storage",
+14 -1
View File
@@ -4,12 +4,18 @@ import { clearAccessToken, getAccessToken } from "@/auth-state";
import { authServiceClient, refreshAccessToken, shortcutServiceClient, userServiceClient } from "@/connect";
import { userKeys } from "@/hooks/useUserQueries";
import type { Shortcut } from "@/types/proto/api/v1/shortcut_service_pb";
import type { User, UserSetting_GeneralSetting, UserSetting_WebhooksSetting } from "@/types/proto/api/v1/user_service_pb";
import type {
User,
UserSetting_GeneralSetting,
UserSetting_TagsSetting,
UserSetting_WebhooksSetting,
} from "@/types/proto/api/v1/user_service_pb";
interface AuthState {
currentUser: User | undefined;
userGeneralSetting: UserSetting_GeneralSetting | undefined;
userWebhooksSetting: UserSetting_WebhooksSetting | undefined;
userTagsSetting: UserSetting_TagsSetting | undefined;
shortcuts: Shortcut[];
isInitialized: boolean;
isLoading: boolean;
@@ -30,6 +36,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
currentUser: undefined,
userGeneralSetting: undefined,
userWebhooksSetting: undefined,
userTagsSetting: undefined,
shortcuts: [],
isInitialized: false,
isLoading: true,
@@ -43,10 +50,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const generalSetting = settings.find((s) => s.value.case === "generalSetting");
const webhooksSetting = settings.find((s) => s.value.case === "webhooksSetting");
const tagsSetting = settings.find((s) => s.value.case === "tagsSetting");
return {
userGeneralSetting: generalSetting?.value.case === "generalSetting" ? generalSetting.value.value : undefined,
userWebhooksSetting: webhooksSetting?.value.case === "webhooksSetting" ? webhooksSetting.value.value : undefined,
userTagsSetting: tagsSetting?.value.case === "tagsSetting" ? tagsSetting.value.value : undefined,
shortcuts,
};
}, []);
@@ -73,6 +82,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
currentUser: undefined,
userGeneralSetting: undefined,
userWebhooksSetting: undefined,
userTagsSetting: undefined,
shortcuts: [],
isInitialized: true,
isLoading: false,
@@ -89,6 +99,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
currentUser: undefined,
userGeneralSetting: undefined,
userWebhooksSetting: undefined,
userTagsSetting: undefined,
shortcuts: [],
isInitialized: true,
isLoading: false,
@@ -115,6 +126,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
currentUser: undefined,
userGeneralSetting: undefined,
userWebhooksSetting: undefined,
userTagsSetting: undefined,
shortcuts: [],
isInitialized: true,
isLoading: false,
@@ -133,6 +145,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
currentUser: undefined,
userGeneralSetting: undefined,
userWebhooksSetting: undefined,
userTagsSetting: undefined,
shortcuts: [],
isInitialized: true,
isLoading: false,
+1 -18
View File
@@ -16,8 +16,6 @@ import {
InstanceSetting_NotificationSettingSchema,
InstanceSetting_StorageSetting,
InstanceSetting_StorageSettingSchema,
InstanceSetting_TagsSetting,
InstanceSetting_TagsSettingSchema,
} from "@/types/proto/api/v1/instance_service_pb";
const instanceSettingNamePrefix = "instance/settings/";
@@ -42,7 +40,6 @@ interface InstanceContextValue extends InstanceState {
generalSetting: InstanceSetting_GeneralSetting;
memoRelatedSetting: InstanceSetting_MemoRelatedSetting;
storageSetting: InstanceSetting_StorageSetting;
tagsSetting: InstanceSetting_TagsSetting;
notificationSetting: InstanceSetting_NotificationSetting;
aiSetting: InstanceSetting_AISetting;
initialize: () => Promise<void>;
@@ -89,14 +86,6 @@ export function InstanceProvider({ children }: { children: ReactNode }) {
return create(InstanceSetting_StorageSettingSchema, {});
}, [state.settings]);
const tagsSetting = useMemo((): InstanceSetting_TagsSetting => {
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}TAGS`);
if (setting?.value.case === "tagsSetting") {
return setting.value.value;
}
return create(InstanceSetting_TagsSettingSchema, {});
}, [state.settings]);
const notificationSetting = useMemo((): InstanceSetting_NotificationSetting => {
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}NOTIFICATION`);
if (setting?.value.case === "notificationSetting") {
@@ -119,11 +108,7 @@ export function InstanceProvider({ children }: { children: ReactNode }) {
const profile = await instanceServiceClient.getInstanceProfile({});
const settingsResponse = await instanceServiceClient.batchGetInstanceSettings({
names: [
buildInstanceSettingName(InstanceSetting_Key.GENERAL),
buildInstanceSettingName(InstanceSetting_Key.MEMO_RELATED),
buildInstanceSettingName(InstanceSetting_Key.TAGS),
],
names: [buildInstanceSettingName(InstanceSetting_Key.GENERAL), buildInstanceSettingName(InstanceSetting_Key.MEMO_RELATED)],
});
for (const setting of settingsResponse.settings) {
fetchedSettingsRef.current.add(setting.name);
@@ -204,7 +189,6 @@ export function InstanceProvider({ children }: { children: ReactNode }) {
generalSetting,
memoRelatedSetting,
storageSetting,
tagsSetting,
notificationSetting,
aiSetting,
initialize,
@@ -217,7 +201,6 @@ export function InstanceProvider({ children }: { children: ReactNode }) {
generalSetting,
memoRelatedSetting,
storageSetting,
tagsSetting,
notificationSetting,
aiSetting,
initialize,
+2 -2
View File
@@ -1,4 +1,4 @@
import type { InstanceSetting_TagMetadata, InstanceSetting_TagsSetting } from "@/types/proto/api/v1/instance_service_pb";
import type { UserSetting_TagMetadata, UserSetting_TagsSetting } from "@/types/proto/api/v1/user_service_pb";
// Cache compiled regexes to avoid re-compiling on every tag render.
const compiledPatternCache = new Map<string, RegExp | null>();
@@ -25,7 +25,7 @@ const getCompiledPattern = (pattern: string): RegExp | null => {
* 1. Exact key match (O(1) fast path, backward-compatible).
* 2. Iterate all keys and test as anchored regex first match wins.
*/
export const findTagMetadata = (tag: string, tagsSetting: InstanceSetting_TagsSetting): InstanceSetting_TagMetadata | undefined => {
export const findTagMetadata = (tag: string, tagsSetting: UserSetting_TagsSetting): UserSetting_TagMetadata | undefined => {
// Fast path: exact match.
if (tagsSetting.tags[tag]) {
return tagsSetting.tags[tag];
@@ -444,6 +444,8 @@ export const InstanceSetting_TagMetadataSchema: GenMessage<InstanceSetting_TagMe
/**
* Tag metadata configuration.
* Active tag metadata is stored in per-user tag settings.
* This message remains for backward compatibility with existing clients and migrations.
*
* @generated from message memos.api.v1.InstanceSetting.TagsSetting
*/
File diff suppressed because one or more lines are too long