diff --git a/proto/api/v1/user_service.proto b/proto/api/v1/user_service.proto index 5ae0b166..bd6f9b2c 100644 --- a/proto/api/v1/user_service.proto +++ b/proto/api/v1/user_service.proto @@ -598,6 +598,19 @@ message MemoView { // The CEL filter expression for the memo view, using the same grammar as the // ListMemos `filter` argument. Reuse it by passing this value to ListMemos. string filter = 3 [(google.api.field_behavior) = REQUIRED]; + + // Optional. Display icon. Omit when updating with the "icon" mask to reset + // to the default view icon. + Icon icon = 4 [(google.api.field_behavior) = OPTIONAL]; + + message Icon { + oneof value { + // A Unicode emoji sequence. + string emoji = 1; + // A canonical Lucide icon name, such as "leaf". + string lucide = 2; + } + } } message ListMemoViewsRequest { diff --git a/proto/gen/api/v1/user_service.pb.go b/proto/gen/api/v1/user_service.pb.go index db61a40f..c6a9aa3d 100644 --- a/proto/gen/api/v1/user_service.pb.go +++ b/proto/gen/api/v1/user_service.pb.go @@ -1497,7 +1497,10 @@ type MemoView struct { Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` // The CEL filter expression for the memo view, using the same grammar as the // ListMemos `filter` argument. Reuse it by passing this value to ListMemos. - Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + // Optional. Display icon. Omit when updating with the "icon" mask to reset + // to the default view icon. + Icon *MemoView_Icon `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1553,6 +1556,13 @@ func (x *MemoView) GetFilter() string { return "" } +func (x *MemoView) GetIcon() *MemoView_Icon { + if x != nil { + return x.Icon + } + return nil +} + type ListMemoViewsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The parent resource where memo views are listed. @@ -3658,6 +3668,90 @@ func (x *UserSetting_WebhooksSetting) GetWebhooks() []*UserWebhook { return nil } +type MemoView_Icon struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *MemoView_Icon_Emoji + // *MemoView_Icon_Lucide + Value isMemoView_Icon_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoView_Icon) Reset() { + *x = MemoView_Icon{} + mi := &file_api_v1_user_service_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoView_Icon) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoView_Icon) ProtoMessage() {} + +func (x *MemoView_Icon) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[57] + 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 MemoView_Icon.ProtoReflect.Descriptor instead. +func (*MemoView_Icon) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{18, 0} +} + +func (x *MemoView_Icon) GetValue() isMemoView_Icon_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *MemoView_Icon) GetEmoji() string { + if x != nil { + if x, ok := x.Value.(*MemoView_Icon_Emoji); ok { + return x.Emoji + } + } + return "" +} + +func (x *MemoView_Icon) GetLucide() string { + if x != nil { + if x, ok := x.Value.(*MemoView_Icon_Lucide); ok { + return x.Lucide + } + } + return "" +} + +type isMemoView_Icon_Value interface { + isMemoView_Icon_Value() +} + +type MemoView_Icon_Emoji struct { + // A Unicode emoji sequence. + Emoji string `protobuf:"bytes,1,opt,name=emoji,proto3,oneof"` +} + +type MemoView_Icon_Lucide struct { + // A canonical Lucide icon name, such as "leaf". + Lucide string `protobuf:"bytes,2,opt,name=lucide,proto3,oneof"` +} + +func (*MemoView_Icon_Emoji) isMemoView_Icon_Value() {} + +func (*MemoView_Icon_Lucide) isMemoView_Icon_Value() {} + type UserNotification_MemoCommentPayload struct { state protoimpl.MessageState `protogen:"open.v1"` // The memo name of comment. @@ -3676,7 +3770,7 @@ type UserNotification_MemoCommentPayload struct { func (x *UserNotification_MemoCommentPayload) Reset() { *x = UserNotification_MemoCommentPayload{} - mi := &file_api_v1_user_service_proto_msgTypes[57] + mi := &file_api_v1_user_service_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3688,7 +3782,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[57] + mi := &file_api_v1_user_service_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3750,7 +3844,7 @@ type UserNotification_MemoMentionPayload struct { func (x *UserNotification_MemoMentionPayload) Reset() { *x = UserNotification_MemoMentionPayload{} - mi := &file_api_v1_user_service_proto_msgTypes[58] + mi := &file_api_v1_user_service_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3762,7 +3856,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[58] + mi := &file_api_v1_user_service_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3823,7 +3917,7 @@ type UserNotification_SpaceInvitationPayload struct { func (x *UserNotification_SpaceInvitationPayload) Reset() { *x = UserNotification_SpaceInvitationPayload{} - mi := &file_api_v1_user_service_proto_msgTypes[59] + mi := &file_api_v1_user_service_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3835,7 +3929,7 @@ func (x *UserNotification_SpaceInvitationPayload) String() string { func (*UserNotification_SpaceInvitationPayload) ProtoMessage() {} func (x *UserNotification_SpaceInvitationPayload) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_user_service_proto_msgTypes[59] + mi := &file_api_v1_user_service_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4012,11 +4106,16 @@ const file_api_v1_user_service_proto_rawDesc = "" + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"y\n" + "\x18ListUserSettingsResponse\x125\n" + "\bsettings\x18\x01 \x03(\v2\x19.memos.api.v1.UserSettingR\bsettings\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x9f\x01\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x98\x02\n" + "\bMemoView\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x19\n" + "\x05title\x18\x02 \x01(\tB\x03\xe0A\x02R\x05title\x12\x1b\n" + - "\x06filter\x18\x03 \x01(\tB\x03\xe0A\x02R\x06filter:B\xeaA?\n" + + "\x06filter\x18\x03 \x01(\tB\x03\xe0A\x02R\x06filter\x124\n" + + "\x04icon\x18\x04 \x01(\v2\x1b.memos.api.v1.MemoView.IconB\x03\xe0A\x01R\x04icon\x1aA\n" + + "\x04Icon\x12\x16\n" + + "\x05emoji\x18\x01 \x01(\tH\x00R\x05emoji\x12\x18\n" + + "\x06lucide\x18\x02 \x01(\tH\x00R\x06lucideB\a\n" + + "\x05value:B\xeaA?\n" + "\x15memos.api.v1/MemoView\x12\x19users/{user}/views/{view}*\x05views2\x04view\"M\n" + "\x14ListMemoViewsRequest\x125\n" + "\x06parent\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\x12\x15memos.api.v1/MemoViewR\x06parent\"N\n" + @@ -4235,7 +4334,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte { } var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 60) +var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 61) 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 @@ -4299,140 +4398,142 @@ var file_api_v1_user_service_proto_goTypes = []any{ (*UserSetting_TagsSetting)(nil), // 59: memos.api.v1.UserSetting.TagsSetting (*UserSetting_WebhooksSetting)(nil), // 60: memos.api.v1.UserSetting.WebhooksSetting nil, // 61: memos.api.v1.UserSetting.TagsSetting.TagsEntry - (*UserNotification_MemoCommentPayload)(nil), // 62: memos.api.v1.UserNotification.MemoCommentPayload - (*UserNotification_MemoMentionPayload)(nil), // 63: memos.api.v1.UserNotification.MemoMentionPayload - (*UserNotification_SpaceInvitationPayload)(nil), // 64: memos.api.v1.UserNotification.SpaceInvitationPayload - (State)(0), // 65: memos.api.v1.State - (*timestamppb.Timestamp)(nil), // 66: google.protobuf.Timestamp - (*fieldmaskpb.FieldMask)(nil), // 67: google.protobuf.FieldMask - (*color.Color)(nil), // 68: google.type.Color - (*Space)(nil), // 69: memos.api.v1.Space - (SpaceMember_Role)(0), // 70: memos.api.v1.SpaceMember.Role - (*emptypb.Empty)(nil), // 71: google.protobuf.Empty + (*MemoView_Icon)(nil), // 62: memos.api.v1.MemoView.Icon + (*UserNotification_MemoCommentPayload)(nil), // 63: memos.api.v1.UserNotification.MemoCommentPayload + (*UserNotification_MemoMentionPayload)(nil), // 64: memos.api.v1.UserNotification.MemoMentionPayload + (*UserNotification_SpaceInvitationPayload)(nil), // 65: memos.api.v1.UserNotification.SpaceInvitationPayload + (State)(0), // 66: memos.api.v1.State + (*timestamppb.Timestamp)(nil), // 67: google.protobuf.Timestamp + (*fieldmaskpb.FieldMask)(nil), // 68: google.protobuf.FieldMask + (*color.Color)(nil), // 69: google.type.Color + (*Space)(nil), // 70: memos.api.v1.Space + (SpaceMember_Role)(0), // 71: memos.api.v1.SpaceMember.Role + (*emptypb.Empty)(nil), // 72: 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 - 65, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State - 66, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp - 66, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp + 66, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State + 67, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp + 67, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp 5, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User 5, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User - 67, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask + 68, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask 5, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User 5, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User - 67, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask + 68, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask 56, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats 55, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry - 66, // 12: memos.api.v1.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp - 66, // 13: memos.api.v1.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp - 65, // 14: memos.api.v1.ListAllUserStatsRequest.state:type_name -> memos.api.v1.State + 67, // 12: memos.api.v1.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp + 67, // 13: memos.api.v1.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp + 66, // 14: memos.api.v1.ListAllUserStatsRequest.state:type_name -> memos.api.v1.State 14, // 15: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats 57, // 16: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting 60, // 17: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting 59, // 18: memos.api.v1.UserSetting.tags_setting:type_name -> memos.api.v1.UserSetting.TagsSetting 18, // 19: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting - 67, // 20: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask + 68, // 20: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask 18, // 21: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting - 23, // 22: memos.api.v1.ListMemoViewsResponse.memo_views:type_name -> memos.api.v1.MemoView - 23, // 23: memos.api.v1.CreateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView - 23, // 24: memos.api.v1.UpdateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView - 67, // 25: memos.api.v1.UpdateMemoViewRequest.update_mask:type_name -> google.protobuf.FieldMask - 30, // 26: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity - 66, // 27: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp - 66, // 28: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp - 66, // 29: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp - 36, // 30: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken - 36, // 31: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken - 66, // 32: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp - 66, // 33: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp - 42, // 34: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook - 42, // 35: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook - 42, // 36: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook - 67, // 37: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask - 5, // 38: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User - 2, // 39: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status - 66, // 40: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp - 3, // 41: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type - 62, // 42: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload - 63, // 43: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload - 64, // 44: memos.api.v1.UserNotification.space_invitation:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload - 50, // 45: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification - 50, // 46: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification - 67, // 47: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask - 68, // 48: memos.api.v1.UserSetting.TagMetadata.background_color:type_name -> google.type.Color - 61, // 49: memos.api.v1.UserSetting.TagsSetting.tags:type_name -> memos.api.v1.UserSetting.TagsSetting.TagsEntry - 42, // 50: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook - 58, // 51: memos.api.v1.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.UserSetting.TagMetadata - 69, // 52: memos.api.v1.UserNotification.SpaceInvitationPayload.space:type_name -> memos.api.v1.Space - 70, // 53: memos.api.v1.UserNotification.SpaceInvitationPayload.role:type_name -> memos.api.v1.SpaceMember.Role - 4, // 54: memos.api.v1.UserNotification.SpaceInvitationPayload.state:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload.State - 6, // 55: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest - 8, // 56: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest - 10, // 57: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest - 11, // 58: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest - 12, // 59: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest - 13, // 60: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest - 16, // 61: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest - 15, // 62: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest - 19, // 63: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest - 20, // 64: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest - 21, // 65: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest - 24, // 66: memos.api.v1.UserService.ListMemoViews:input_type -> memos.api.v1.ListMemoViewsRequest - 26, // 67: memos.api.v1.UserService.GetMemoView:input_type -> memos.api.v1.GetMemoViewRequest - 27, // 68: memos.api.v1.UserService.CreateMemoView:input_type -> memos.api.v1.CreateMemoViewRequest - 28, // 69: memos.api.v1.UserService.UpdateMemoView:input_type -> memos.api.v1.UpdateMemoViewRequest - 29, // 70: memos.api.v1.UserService.DeleteMemoView:input_type -> memos.api.v1.DeleteMemoViewRequest - 31, // 71: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest - 33, // 72: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest - 34, // 73: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest - 35, // 74: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest - 37, // 75: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest - 39, // 76: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest - 41, // 77: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest - 43, // 78: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest - 45, // 79: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest - 46, // 80: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest - 47, // 81: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest - 48, // 82: memos.api.v1.UserService.GetUserWebhookSigningSecret:input_type -> memos.api.v1.GetUserWebhookSigningSecretRequest - 51, // 83: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest - 53, // 84: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest - 54, // 85: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest - 7, // 86: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse - 9, // 87: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse - 5, // 88: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User - 5, // 89: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User - 5, // 90: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User - 71, // 91: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty - 17, // 92: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse - 14, // 93: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats - 18, // 94: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting - 18, // 95: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting - 22, // 96: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse - 25, // 97: memos.api.v1.UserService.ListMemoViews:output_type -> memos.api.v1.ListMemoViewsResponse - 23, // 98: memos.api.v1.UserService.GetMemoView:output_type -> memos.api.v1.MemoView - 23, // 99: memos.api.v1.UserService.CreateMemoView:output_type -> memos.api.v1.MemoView - 23, // 100: memos.api.v1.UserService.UpdateMemoView:output_type -> memos.api.v1.MemoView - 71, // 101: memos.api.v1.UserService.DeleteMemoView:output_type -> google.protobuf.Empty - 32, // 102: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse - 30, // 103: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity - 30, // 104: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity - 71, // 105: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty - 38, // 106: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse - 40, // 107: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse - 71, // 108: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty - 44, // 109: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse - 42, // 110: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook - 42, // 111: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook - 71, // 112: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty - 49, // 113: memos.api.v1.UserService.GetUserWebhookSigningSecret:output_type -> memos.api.v1.GetUserWebhookSigningSecretResponse - 52, // 114: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse - 50, // 115: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification - 71, // 116: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty - 86, // [86:117] is the sub-list for method output_type - 55, // [55:86] is the sub-list for method input_type - 55, // [55:55] is the sub-list for extension type_name - 55, // [55:55] is the sub-list for extension extendee - 0, // [0:55] is the sub-list for field type_name + 62, // 22: memos.api.v1.MemoView.icon:type_name -> memos.api.v1.MemoView.Icon + 23, // 23: memos.api.v1.ListMemoViewsResponse.memo_views:type_name -> memos.api.v1.MemoView + 23, // 24: memos.api.v1.CreateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView + 23, // 25: memos.api.v1.UpdateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView + 68, // 26: memos.api.v1.UpdateMemoViewRequest.update_mask:type_name -> google.protobuf.FieldMask + 30, // 27: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity + 67, // 28: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp + 67, // 29: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp + 67, // 30: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp + 36, // 31: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken + 36, // 32: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken + 67, // 33: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp + 67, // 34: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp + 42, // 35: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook + 42, // 36: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook + 42, // 37: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook + 68, // 38: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask + 5, // 39: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User + 2, // 40: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status + 67, // 41: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp + 3, // 42: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type + 63, // 43: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload + 64, // 44: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload + 65, // 45: memos.api.v1.UserNotification.space_invitation:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload + 50, // 46: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification + 50, // 47: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification + 68, // 48: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask + 69, // 49: memos.api.v1.UserSetting.TagMetadata.background_color:type_name -> google.type.Color + 61, // 50: memos.api.v1.UserSetting.TagsSetting.tags:type_name -> memos.api.v1.UserSetting.TagsSetting.TagsEntry + 42, // 51: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook + 58, // 52: memos.api.v1.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.UserSetting.TagMetadata + 70, // 53: memos.api.v1.UserNotification.SpaceInvitationPayload.space:type_name -> memos.api.v1.Space + 71, // 54: memos.api.v1.UserNotification.SpaceInvitationPayload.role:type_name -> memos.api.v1.SpaceMember.Role + 4, // 55: memos.api.v1.UserNotification.SpaceInvitationPayload.state:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload.State + 6, // 56: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest + 8, // 57: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest + 10, // 58: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest + 11, // 59: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest + 12, // 60: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest + 13, // 61: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest + 16, // 62: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest + 15, // 63: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest + 19, // 64: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest + 20, // 65: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest + 21, // 66: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest + 24, // 67: memos.api.v1.UserService.ListMemoViews:input_type -> memos.api.v1.ListMemoViewsRequest + 26, // 68: memos.api.v1.UserService.GetMemoView:input_type -> memos.api.v1.GetMemoViewRequest + 27, // 69: memos.api.v1.UserService.CreateMemoView:input_type -> memos.api.v1.CreateMemoViewRequest + 28, // 70: memos.api.v1.UserService.UpdateMemoView:input_type -> memos.api.v1.UpdateMemoViewRequest + 29, // 71: memos.api.v1.UserService.DeleteMemoView:input_type -> memos.api.v1.DeleteMemoViewRequest + 31, // 72: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest + 33, // 73: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest + 34, // 74: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest + 35, // 75: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest + 37, // 76: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest + 39, // 77: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest + 41, // 78: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest + 43, // 79: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest + 45, // 80: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest + 46, // 81: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest + 47, // 82: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest + 48, // 83: memos.api.v1.UserService.GetUserWebhookSigningSecret:input_type -> memos.api.v1.GetUserWebhookSigningSecretRequest + 51, // 84: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest + 53, // 85: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest + 54, // 86: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest + 7, // 87: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse + 9, // 88: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse + 5, // 89: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User + 5, // 90: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User + 5, // 91: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User + 72, // 92: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty + 17, // 93: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse + 14, // 94: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats + 18, // 95: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting + 18, // 96: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting + 22, // 97: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse + 25, // 98: memos.api.v1.UserService.ListMemoViews:output_type -> memos.api.v1.ListMemoViewsResponse + 23, // 99: memos.api.v1.UserService.GetMemoView:output_type -> memos.api.v1.MemoView + 23, // 100: memos.api.v1.UserService.CreateMemoView:output_type -> memos.api.v1.MemoView + 23, // 101: memos.api.v1.UserService.UpdateMemoView:output_type -> memos.api.v1.MemoView + 72, // 102: memos.api.v1.UserService.DeleteMemoView:output_type -> google.protobuf.Empty + 32, // 103: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse + 30, // 104: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity + 30, // 105: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity + 72, // 106: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty + 38, // 107: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse + 40, // 108: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse + 72, // 109: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty + 44, // 110: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse + 42, // 111: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook + 42, // 112: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook + 72, // 113: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty + 49, // 114: memos.api.v1.UserService.GetUserWebhookSigningSecret:output_type -> memos.api.v1.GetUserWebhookSigningSecretResponse + 52, // 115: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse + 50, // 116: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification + 72, // 117: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty + 87, // [87:118] is the sub-list for method output_type + 56, // [56:87] is the sub-list for method input_type + 56, // [56:56] is the sub-list for extension type_name + 56, // [56:56] is the sub-list for extension extendee + 0, // [0:56] is the sub-list for field type_name } func init() { file_api_v1_user_service_proto_init() } @@ -4452,13 +4553,17 @@ func file_api_v1_user_service_proto_init() { (*UserNotification_MemoMention)(nil), (*UserNotification_SpaceInvitation)(nil), } + file_api_v1_user_service_proto_msgTypes[57].OneofWrappers = []any{ + (*MemoView_Icon_Emoji)(nil), + (*MemoView_Icon_Lucide)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ 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: 5, - NumMessages: 60, + NumMessages: 61, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index b3efeb1c..391ca9ea 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -4119,7 +4119,22 @@ components: description: |- The CEL filter expression for the memo view, using the same grammar as the ListMemos `filter` argument. Reuse it by passing this value to ListMemos. + icon: + allOf: + - $ref: '#/components/schemas/MemoView_Icon' + description: |- + Optional. Display icon. Omit when updating with the "icon" mask to reset + to the default view icon. description: Saved memo view messages + MemoView_Icon: + type: object + properties: + emoji: + type: string + description: A Unicode emoji sequence. + lucide: + type: string + description: A canonical Lucide icon name, such as "leaf". Memo_Property: type: object properties: diff --git a/proto/gen/store/user_setting.pb.go b/proto/gen/store/user_setting.pb.go index 0d777a9f..8a09555f 100644 --- a/proto/gen/store/user_setting.pb.go +++ b/proto/gen/store/user_setting.pb.go @@ -857,10 +857,11 @@ func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetLastUsedAt() *t } type MemoViewsUserSetting_MemoView struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + Icon *MemoViewsUserSetting_MemoView_Icon `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -916,6 +917,97 @@ func (x *MemoViewsUserSetting_MemoView) GetFilter() string { return "" } +func (x *MemoViewsUserSetting_MemoView) GetIcon() *MemoViewsUserSetting_MemoView_Icon { + if x != nil { + return x.Icon + } + return nil +} + +type MemoViewsUserSetting_MemoView_Icon struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *MemoViewsUserSetting_MemoView_Icon_Emoji + // *MemoViewsUserSetting_MemoView_Icon_Lucide + Value isMemoViewsUserSetting_MemoView_Icon_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoViewsUserSetting_MemoView_Icon) Reset() { + *x = MemoViewsUserSetting_MemoView_Icon{} + mi := &file_store_user_setting_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoViewsUserSetting_MemoView_Icon) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoViewsUserSetting_MemoView_Icon) ProtoMessage() {} + +func (x *MemoViewsUserSetting_MemoView_Icon) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[13] + 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 MemoViewsUserSetting_MemoView_Icon.ProtoReflect.Descriptor instead. +func (*MemoViewsUserSetting_MemoView_Icon) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{6, 0, 0} +} + +func (x *MemoViewsUserSetting_MemoView_Icon) GetValue() isMemoViewsUserSetting_MemoView_Icon_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *MemoViewsUserSetting_MemoView_Icon) GetEmoji() string { + if x != nil { + if x, ok := x.Value.(*MemoViewsUserSetting_MemoView_Icon_Emoji); ok { + return x.Emoji + } + } + return "" +} + +func (x *MemoViewsUserSetting_MemoView_Icon) GetLucide() string { + if x != nil { + if x, ok := x.Value.(*MemoViewsUserSetting_MemoView_Icon_Lucide); ok { + return x.Lucide + } + } + return "" +} + +type isMemoViewsUserSetting_MemoView_Icon_Value interface { + isMemoViewsUserSetting_MemoView_Icon_Value() +} + +type MemoViewsUserSetting_MemoView_Icon_Emoji struct { + // A Unicode emoji sequence. + Emoji string `protobuf:"bytes,1,opt,name=emoji,proto3,oneof"` +} + +type MemoViewsUserSetting_MemoView_Icon_Lucide struct { + // A canonical Lucide icon name, such as "leaf". + Lucide string `protobuf:"bytes,2,opt,name=lucide,proto3,oneof"` +} + +func (*MemoViewsUserSetting_MemoView_Icon_Emoji) isMemoViewsUserSetting_MemoView_Icon_Value() {} + +func (*MemoViewsUserSetting_MemoView_Icon_Lucide) isMemoViewsUserSetting_MemoView_Icon_Value() {} + type WebhooksUserSetting_Webhook struct { state protoimpl.MessageState `protogen:"open.v1"` // Unique identifier for the webhook @@ -932,7 +1024,7 @@ type WebhooksUserSetting_Webhook struct { func (x *WebhooksUserSetting_Webhook) Reset() { *x = WebhooksUserSetting_Webhook{} - mi := &file_store_user_setting_proto_msgTypes[13] + mi := &file_store_user_setting_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -944,7 +1036,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[13] + mi := &file_store_user_setting_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1060,14 +1152,19 @@ const file_store_user_setting_proto_rawDesc = "" + "\n" + "created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" + "\flast_used_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "lastUsedAt\"\xab\x01\n" + + "lastUsedAt\"\xb4\x02\n" + "\x14MemoViewsUserSetting\x12I\n" + "\n" + - "memo_views\x18\x01 \x03(\v2*.memos.store.MemoViewsUserSetting.MemoViewR\tmemoViews\x1aH\n" + + "memo_views\x18\x01 \x03(\v2*.memos.store.MemoViewsUserSetting.MemoViewR\tmemoViews\x1a\xd0\x01\n" + "\bMemoView\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" + - "\x06filter\x18\x03 \x01(\tR\x06filter\"\xc5\x01\n" + + "\x06filter\x18\x03 \x01(\tR\x06filter\x12C\n" + + "\x04icon\x18\x04 \x01(\v2/.memos.store.MemoViewsUserSetting.MemoView.IconR\x04icon\x1aA\n" + + "\x04Icon\x12\x16\n" + + "\x05emoji\x18\x01 \x01(\tH\x00R\x05emoji\x12\x18\n" + + "\x06lucide\x18\x02 \x01(\tH\x00R\x06lucideB\a\n" + + "\x05value\"\xc5\x01\n" + "\x13WebhooksUserSetting\x12D\n" + "\bwebhooks\x18\x01 \x03(\v2(.memos.store.WebhooksUserSetting.WebhookR\bwebhooks\x1ah\n" + "\aWebhook\x12\x0e\n" + @@ -1090,7 +1187,7 @@ 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, 14) +var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_store_user_setting_proto_goTypes = []any{ (UserSetting_Key)(0), // 0: memos.store.UserSetting.Key (*UserSetting)(nil), // 1: memos.store.UserSetting @@ -1106,9 +1203,10 @@ var file_store_user_setting_proto_goTypes = []any{ (*RefreshTokensUserSetting_ClientInfo)(nil), // 11: memos.store.RefreshTokensUserSetting.ClientInfo (*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 12: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken (*MemoViewsUserSetting_MemoView)(nil), // 13: memos.store.MemoViewsUserSetting.MemoView - (*WebhooksUserSetting_Webhook)(nil), // 14: memos.store.WebhooksUserSetting.Webhook - (*color.Color)(nil), // 15: google.type.Color - (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp + (*MemoViewsUserSetting_MemoView_Icon)(nil), // 14: memos.store.MemoViewsUserSetting.MemoView.Icon + (*WebhooksUserSetting_Webhook)(nil), // 15: memos.store.WebhooksUserSetting.Webhook + (*color.Color)(nil), // 16: google.type.Color + (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp } var file_store_user_setting_proto_depIdxs = []int32{ 0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key @@ -1118,24 +1216,25 @@ var file_store_user_setting_proto_depIdxs = []int32{ 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 + 16, // 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.MemoViewsUserSetting.memo_views:type_name -> memos.store.MemoViewsUserSetting.MemoView - 14, // 12: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook + 15, // 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 + 17, // 14: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp + 17, // 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 + 17, // 17: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp + 17, // 18: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp + 17, // 19: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp + 14, // 20: memos.store.MemoViewsUserSetting.MemoView.icon:type_name -> memos.store.MemoViewsUserSetting.MemoView.Icon + 21, // [21:21] is the sub-list for method output_type + 21, // [21:21] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_store_user_setting_proto_init() } @@ -1151,13 +1250,17 @@ func file_store_user_setting_proto_init() { (*UserSetting_PersonalAccessTokens)(nil), (*UserSetting_Tags)(nil), } + file_store_user_setting_proto_msgTypes[13].OneofWrappers = []any{ + (*MemoViewsUserSetting_MemoView_Icon_Emoji)(nil), + (*MemoViewsUserSetting_MemoView_Icon_Lucide)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ 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: 14, + NumMessages: 15, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/store/user_setting.proto b/proto/store/user_setting.proto index 3244f3b8..d94d9951 100644 --- a/proto/store/user_setting.proto +++ b/proto/store/user_setting.proto @@ -118,6 +118,16 @@ message MemoViewsUserSetting { string id = 1; string title = 2; string filter = 3; + Icon icon = 4; + + message Icon { + oneof value { + // A Unicode emoji sequence. + string emoji = 1; + // A canonical Lucide icon name, such as "leaf". + string lucide = 2; + } + } } repeated MemoView memo_views = 1; } diff --git a/server/router/api/v1/memo_view_icon.go b/server/router/api/v1/memo_view_icon.go new file mode 100644 index 00000000..bdbee272 --- /dev/null +++ b/server/router/api/v1/memo_view_icon.go @@ -0,0 +1,35 @@ +package v1 + +import ( + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" +) + +func convertMemoViewIconFromStore(icon *storepb.MemoViewsUserSetting_MemoView_Icon) *v1pb.MemoView_Icon { + if icon == nil { + return nil + } + switch value := icon.Value.(type) { + case *storepb.MemoViewsUserSetting_MemoView_Icon_Emoji: + return &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Emoji{Emoji: value.Emoji}} + case *storepb.MemoViewsUserSetting_MemoView_Icon_Lucide: + return &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Lucide{Lucide: value.Lucide}} + default: + return nil + } +} + +func convertMemoViewIconToStore(icon *v1pb.MemoView_Icon) *storepb.MemoViewsUserSetting_MemoView_Icon { + if icon == nil { + return nil + } + switch value := icon.Value.(type) { + case *v1pb.MemoView_Icon_Emoji: + return &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Emoji{Emoji: value.Emoji}} + case *v1pb.MemoView_Icon_Lucide: + return &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Lucide{Lucide: value.Lucide}} + default: + // Preserve an empty icon so validation rejects it instead of treating it as a reset. + return &storepb.MemoViewsUserSetting_MemoView_Icon{} + } +} diff --git a/server/router/api/v1/test/memo_view_icon_test.go b/server/router/api/v1/test/memo_view_icon_test.go new file mode 100644 index 00000000..e7d8e8b3 --- /dev/null +++ b/server/router/api/v1/test/memo_view_icon_test.go @@ -0,0 +1,99 @@ +package test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +func TestMemoViewIconLifecycle(t *testing.T) { + ctx := context.Background() + ts := NewTestService(t) + defer ts.Cleanup() + owner, err := ts.CreateRegularUser(ctx, "icon-owner") + require.NoError(t, err) + other, err := ts.CreateRegularUser(ctx, "icon-other") + require.NoError(t, err) + ownerCtx := ts.CreateUserContext(ctx, owner.ID) + parent := "users/" + owner.Username + emoji := &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Emoji{Emoji: "👩🏽‍🌾"}} + request := &v1pb.CreateMemoViewRequest{ + Parent: parent, + MemoView: &v1pb.MemoView{Title: "Garden", Filter: "pinned", Icon: emoji}, + ValidateOnly: true, + } + validated, err := ts.Service.CreateMemoView(ownerCtx, request) + require.NoError(t, err) + require.True(t, proto.Equal(emoji, validated.Icon)) + listed, err := ts.Service.ListMemoViews(ownerCtx, &v1pb.ListMemoViewsRequest{Parent: parent}) + require.NoError(t, err) + require.Empty(t, listed.MemoViews) + + request.ValidateOnly = false + view, err := ts.Service.CreateMemoView(ownerCtx, request) + require.NoError(t, err) + require.True(t, proto.Equal(emoji, view.Icon)) + view, err = ts.Service.UpdateMemoView(ownerCtx, &v1pb.UpdateMemoViewRequest{ + MemoView: &v1pb.MemoView{Name: view.Name, Title: "Renamed", Filter: "has_link"}, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"title", "filter"}}, + }) + require.NoError(t, err) + require.True(t, proto.Equal(emoji, view.Icon), "other field updates preserve the icon") + + lucide := &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Lucide{Lucide: "future-icon"}} + update := &v1pb.UpdateMemoViewRequest{ + MemoView: &v1pb.MemoView{Name: view.Name, Icon: lucide}, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"icon"}}, + } + _, err = ts.Service.UpdateMemoView(ts.CreateUserContext(ctx, other.ID), update) + require.Equal(t, codes.PermissionDenied, status.Code(err)) + view, err = ts.Service.UpdateMemoView(ownerCtx, update) + require.NoError(t, err) + require.True(t, proto.Equal(lucide, view.Icon)) + require.Equal(t, "Renamed", view.Title) + require.Equal(t, "has_link", view.Filter) + fetched, err := ts.Service.GetMemoView(ownerCtx, &v1pb.GetMemoViewRequest{Name: view.Name}) + require.NoError(t, err) + require.True(t, proto.Equal(lucide, fetched.Icon)) + listed, err = ts.Service.ListMemoViews(ownerCtx, &v1pb.ListMemoViewsRequest{Parent: parent}) + require.NoError(t, err) + require.Len(t, listed.MemoViews, 1) + require.True(t, proto.Equal(lucide, listed.MemoViews[0].Icon)) + + for _, invalid := range []*v1pb.MemoView_Icon{ + {}, + {Value: &v1pb.MemoView_Icon_Emoji{Emoji: "🌱🌱"}}, + {Value: &v1pb.MemoView_Icon_Emoji{Emoji: ""}}, + {Value: &v1pb.MemoView_Icon_Lucide{Lucide: "BookOpen"}}, + {Value: &v1pb.MemoView_Icon_Lucide{Lucide: ""}}, + } { + request.MemoView.Icon = invalid + request.ValidateOnly = true + _, err = ts.Service.CreateMemoView(ownerCtx, request) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + request.ValidateOnly = false + _, err = ts.Service.CreateMemoView(ownerCtx, request) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + update.MemoView.Icon = invalid + _, err = ts.Service.UpdateMemoView(ownerCtx, update) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + } + fetched, err = ts.Service.GetMemoView(ownerCtx, &v1pb.GetMemoViewRequest{Name: view.Name}) + require.NoError(t, err) + require.True(t, proto.Equal(lucide, fetched.Icon), "invalid updates leave storage unchanged") + + update.MemoView.Icon = nil + view, err = ts.Service.UpdateMemoView(ownerCtx, update) + require.NoError(t, err) + require.Nil(t, view.Icon) + fetched, err = ts.Service.GetMemoView(ownerCtx, &v1pb.GetMemoViewRequest{Name: view.Name}) + require.NoError(t, err) + require.Nil(t, fetched.Icon) +} diff --git a/server/router/api/v1/user_service_memo_views.go b/server/router/api/v1/user_service_memo_views.go index fe7bb5b7..9a50e698 100644 --- a/server/router/api/v1/user_service_memo_views.go +++ b/server/router/api/v1/user_service_memo_views.go @@ -51,6 +51,7 @@ func convertMemoViewFromStore(username string, memoView *storepb.MemoViewsUserSe Name: constructMemoViewName(username, memoView.GetId()), Title: memoView.GetTitle(), Filter: memoView.GetFilter(), + Icon: convertMemoViewIconFromStore(memoView.GetIcon()), } } @@ -138,6 +139,7 @@ func (s *APIV1Service) CreateMemoView(ctx context.Context, request *v1pb.CreateM Id: util.GenUUID(), Title: request.GetMemoView().GetTitle(), Filter: request.GetMemoView().GetFilter(), + Icon: convertMemoViewIconToStore(request.GetMemoView().GetIcon()), } if newMemoView.Title == "" { return nil, status.Errorf(codes.InvalidArgument, "title is required") @@ -145,6 +147,9 @@ func (s *APIV1Service) CreateMemoView(ctx context.Context, request *v1pb.CreateM if err := s.validateFilter(ctx, newMemoView.Filter); err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) } + if err := store.ValidateMemoViewIcon(newMemoView.Icon); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid icon: %v", err) + } if request.ValidateOnly { return convertMemoViewFromStore(user.Username, newMemoView), nil } @@ -170,6 +175,7 @@ func (s *APIV1Service) UpdateMemoView(ctx context.Context, request *v1pb.UpdateM } var title, filterValue *string + var icon **storepb.MemoViewsUserSetting_MemoView_Icon for _, field := range request.UpdateMask.Paths { switch field { case "title": @@ -184,12 +190,18 @@ func (s *APIV1Service) UpdateMemoView(ctx context.Context, request *v1pb.UpdateM } value := request.GetMemoView().GetFilter() filterValue = &value + case "icon": + value := convertMemoViewIconToStore(request.GetMemoView().GetIcon()) + if err := store.ValidateMemoViewIcon(value); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid icon: %v", err) + } + icon = &value default: return nil, status.Errorf(codes.InvalidArgument, "unsupported update mask path: %s", field) } } - updatedMemoView, err := s.Store.UpdateUserMemoView(ctx, user.ID, memoViewID, title, filterValue) + updatedMemoView, err := s.Store.UpdateUserMemoView(ctx, user.ID, memoViewID, title, filterValue, icon) if err != nil { return nil, status.Errorf(codes.Internal, "failed to update memo view: %v", err) } diff --git a/store/custom_icon.go b/store/custom_icon.go new file mode 100644 index 00000000..24661305 --- /dev/null +++ b/store/custom_icon.go @@ -0,0 +1,33 @@ +package store + +import ( + "strings" + + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/markdown/parser" +) + +func validateEmojiIcon(emoji string) error { + if !parser.IsEmoji(emoji) { + return errors.New("icon must contain one fully qualified emoji") + } + return nil +} + +func validateLucideIcon(name string) error { + if len(name) == 0 || len(name) > 128 { + return errors.New("icon name must contain 1 to 128 bytes") + } + for _, part := range strings.Split(name, "-") { + if part == "" { + return errors.New("icon name must use lowercase kebab-case") + } + for _, char := range part { + if (char < 'a' || char > 'z') && (char < '0' || char > '9') { + return errors.New("icon name must use lowercase kebab-case") + } + } + } + return nil +} diff --git a/store/memo_view_icon.go b/store/memo_view_icon.go new file mode 100644 index 00000000..0d9e77f1 --- /dev/null +++ b/store/memo_view_icon.go @@ -0,0 +1,22 @@ +package store + +import ( + "github.com/pkg/errors" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +// ValidateMemoViewIcon validates optional display metadata for a saved view. +func ValidateMemoViewIcon(icon *storepb.MemoViewsUserSetting_MemoView_Icon) error { + if icon == nil { + return nil + } + switch value := icon.Value.(type) { + case *storepb.MemoViewsUserSetting_MemoView_Icon_Emoji: + return validateEmojiIcon(value.Emoji) + case *storepb.MemoViewsUserSetting_MemoView_Icon_Lucide: + return validateLucideIcon(value.Lucide) + default: + return errors.New("view icon must specify emoji or lucide") + } +} diff --git a/store/space_icon.go b/store/space_icon.go index 0b30a5e6..225b40a3 100644 --- a/store/space_icon.go +++ b/store/space_icon.go @@ -1,11 +1,8 @@ package store import ( - "strings" - "github.com/pkg/errors" - "github.com/usememos/memos/internal/markdown/parser" storepb "github.com/usememos/memos/proto/gen/store" ) @@ -17,25 +14,10 @@ func ValidateSpaceIcon(icon *storepb.SpacePayload_Icon) error { } switch value := icon.Value.(type) { case *storepb.SpacePayload_Icon_Emoji: - if !parser.IsEmoji(value.Emoji) { - return errors.New("space icon must contain one fully qualified emoji") - } + return validateEmojiIcon(value.Emoji) case *storepb.SpacePayload_Icon_Lucide: - if len(value.Lucide) == 0 || len(value.Lucide) > 128 { - return errors.New("space icon name must contain 1 to 128 bytes") - } - for _, part := range strings.Split(value.Lucide, "-") { - if part == "" { - return errors.New("space icon name must use lowercase kebab-case") - } - for _, char := range part { - if (char < 'a' || char > 'z') && (char < '0' || char > '9') { - return errors.New("space icon name must use lowercase kebab-case") - } - } - } + return validateLucideIcon(value.Lucide) default: return errors.New("space icon must specify emoji or lucide") } - return nil } diff --git a/store/test/user_setting_test.go b/store/test/user_setting_test.go index 840ddd37..0de61da8 100644 --- a/store/test/user_setting_test.go +++ b/store/test/user_setting_test.go @@ -939,21 +939,27 @@ func TestUserMemoViewConcurrentPartialUpdates(t *testing.T) { Id: "work", Title: "Original title", Filter: `tag in ["original"]`, + Icon: &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Emoji{Emoji: "🌱"}}, })) updatedTitle := "Updated title" updatedFilter := `tag in ["updated"]` var wg sync.WaitGroup - errs := make([]error, 2) + errs := make([]error, 3) start := make(chan struct{}) wg.Go(func() { <-start - _, errs[0] = ts.UpdateUserMemoView(ctx, user.ID, "work", &updatedTitle, nil) + _, errs[0] = ts.UpdateUserMemoView(ctx, user.ID, "work", &updatedTitle, nil, nil) }) wg.Go(func() { <-start - _, errs[1] = ts.UpdateUserMemoView(ctx, user.ID, "work", nil, &updatedFilter) + _, errs[1] = ts.UpdateUserMemoView(ctx, user.ID, "work", nil, &updatedFilter, nil) + }) + wg.Go(func() { + <-start + icon := &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Lucide{Lucide: "leaf"}} + _, errs[2] = ts.UpdateUserMemoView(ctx, user.ID, "work", nil, nil, &icon) }) close(start) wg.Wait() @@ -961,11 +967,14 @@ func TestUserMemoViewConcurrentPartialUpdates(t *testing.T) { require.NoError(t, err) } + _, err = ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_MEMO_VIEWS}) + require.NoError(t, err) memoViews, err := ts.GetUserMemoViews(ctx, user.ID) require.NoError(t, err) require.Len(t, memoViews, 1) require.Equal(t, updatedTitle, memoViews[0].GetTitle()) require.Equal(t, updatedFilter, memoViews[0].GetFilter()) + require.Equal(t, "leaf", memoViews[0].GetIcon().GetLucide()) } func TestGetUserMemoViewsReturnsCopies(t *testing.T) { @@ -980,17 +989,20 @@ func TestGetUserMemoViewsReturnsCopies(t *testing.T) { Id: "work", Title: "Original title", Filter: `tag in ["original"]`, + Icon: &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Emoji{Emoji: "🌱"}}, })) firstRead, err := ts.GetUserMemoViews(ctx, user.ID) require.NoError(t, err) require.Len(t, firstRead, 1) firstRead[0].Title = "Mutated title" + firstRead[0].Icon.Value = &storepb.MemoViewsUserSetting_MemoView_Icon_Lucide{Lucide: "leaf"} secondRead, err := ts.GetUserMemoViews(ctx, user.ID) require.NoError(t, err) require.Len(t, secondRead, 1) require.Equal(t, "Original title", secondRead[0].GetTitle()) + require.Equal(t, "🌱", secondRead[0].GetIcon().GetEmoji()) } func TestUserSettingMemoViewsPartialUpdate(t *testing.T) { diff --git a/store/user_setting.go b/store/user_setting.go index 46a95964..dd703157 100644 --- a/store/user_setting.go +++ b/store/user_setting.go @@ -495,6 +495,7 @@ func (s *Store) AddUserMemoView(ctx context.Context, userID int32, memoView *sto } // UpdateUserMemoView applies the non-nil field updates to the memo view carrying the same ID. +// A non-nil icon update containing nil resets the icon. // It returns nil when no matching memo view is found. func (s *Store) UpdateUserMemoView( ctx context.Context, @@ -502,6 +503,7 @@ func (s *Store) UpdateUserMemoView( memoViewID string, title *string, filter *string, + icon **storepb.MemoViewsUserSetting_MemoView_Icon, ) (*storepb.MemoViewsUserSetting_MemoView, error) { s.memoViewMu.Lock() defer s.memoViewMu.Unlock() @@ -519,17 +521,16 @@ func (s *Store) UpdateUserMemoView( continue } - updatedMemoView = &storepb.MemoViewsUserSetting_MemoView{ - Id: item.GetId(), - Title: item.GetTitle(), - Filter: item.GetFilter(), - } + updatedMemoView = proto.CloneOf(item) if title != nil { updatedMemoView.Title = *title } if filter != nil { updatedMemoView.Filter = *filter } + if icon != nil { + updatedMemoView.Icon = proto.CloneOf(*icon) + } memoViews = append(memoViews, updatedMemoView) } if updatedMemoView == nil { diff --git a/web/src/components/AppSidebar/ViewsSection.tsx b/web/src/components/AppSidebar/ViewsSection.tsx index 7a773466..b65575c3 100644 --- a/web/src/components/AppSidebar/ViewsSection.tsx +++ b/web/src/components/AppSidebar/ViewsSection.tsx @@ -1,10 +1,11 @@ import { useQueryClient } from "@tanstack/react-query"; -import { MoreHorizontalIcon, ParenthesesIcon, PlusIcon, SquareCheckIcon } from "lucide-react"; +import { MoreHorizontalIcon, PlusIcon, SquareCheckIcon } from "lucide-react"; import { useState } from "react"; import toast from "react-hot-toast"; import { useLocation, useNavigate } from "react-router-dom"; import ConfirmDialog from "@/components/ConfirmDialog"; import MemoDisplaySettingMenu from "@/components/MemoDisplaySettingMenu"; +import MemoViewIcon from "@/components/MemoViewIcon"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { userServiceClient } from "@/connect"; @@ -20,9 +21,10 @@ import type { MemoView } from "@/types/proto/api/v1/user_service_pb"; import { useTranslate } from "@/utils/i18n"; import SidebarRow, { SIDEBAR_ROW_BOX_CLASSES, + SIDEBAR_ROW_ICON_CLASSES, SIDEBAR_ROW_LABEL_CLASSES, SIDEBAR_ROW_SLOT_BUTTON_CLASSES, - SidebarRowIconSlot, + SIDEBAR_ROW_SLOT_CLASSES, sidebarRowStateAttributes, sidebarRowStateClasses, } from "./SidebarRow"; @@ -102,7 +104,9 @@ const ViewsSection = ({ manageActive = false }: { manageActive?: boolean }) => { className={cn(SIDEBAR_ROW_BOX_CLASSES, "group/view", sidebarRowStateClasses(state))} > diff --git a/web/src/components/CustomIcon.tsx b/web/src/components/CustomIcon.tsx new file mode 100644 index 00000000..f055a4ee --- /dev/null +++ b/web/src/components/CustomIcon.tsx @@ -0,0 +1,24 @@ +import { AstroidIcon, type LucideIcon } from "lucide-react"; +import type { CustomIconValue } from "@/lib/custom-icons"; +import { getCustomSymbol } from "@/lib/custom-icons"; +import { cn } from "@/lib/utils"; + +interface Props { + icon?: CustomIconValue; + className?: string; + fallback?: LucideIcon; +} + +function CustomIcon({ icon, className, fallback = AstroidIcon }: Props) { + if (icon?.value.case === "emoji" && icon.value.value) { + return ( + + {icon.value.value} + + ); + } + const IconComponent = getCustomSymbol(icon?.value.case === "lucide" ? icon.value.value : "", fallback); + return ; +} + +export default CustomIcon; diff --git a/web/src/components/CustomIconPicker.tsx b/web/src/components/CustomIconPicker.tsx new file mode 100644 index 00000000..3f702a19 --- /dev/null +++ b/web/src/components/CustomIconPicker.tsx @@ -0,0 +1,192 @@ +import { AstroidIcon, type LucideIcon, RotateCcwIcon, SearchIcon } from "lucide-react"; +import { type KeyboardEvent, useId, useRef, useState } from "react"; +import CustomIcon from "@/components/CustomIcon"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { CustomIconValue } from "@/lib/custom-icons"; +import { CUSTOM_SYMBOLS, customIconsEqual } from "@/lib/custom-icons"; +import { SPACE_EMOJI } from "@/lib/space-emoji"; +import { cn } from "@/lib/utils"; +import { useTranslate } from "@/utils/i18n"; +import { FULLY_QUALIFIED_EMOJI } from "@/utils/tag-unicode-data"; + +const emojiSet = new Set(FULLY_QUALIFIED_EMOJI); +const symbolLabels: Record = { + astroid: "Space", + "book-open": "Book", + "chart-no-axes-combined": "Chart", + "check-check": "Checklist", + "flask-conical": "Science", + "flower-2": "Flower", + "graduation-cap": "Education", + "map-pin": "Location", + "notebook-pen": "Notebook", +}; +const symbols = Object.keys(CUSTOM_SYMBOLS).map((name) => ({ + label: symbolLabels[name] ?? name.replaceAll("-", " ").replace(/^./, (char) => char.toUpperCase()), + icon: { value: { case: "lucide", value: name } } satisfies CustomIconValue, +})); +const emojis = SPACE_EMOJI.map(([emoji, label]) => ({ label, icon: { value: { case: "emoji", value: emoji } } satisfies CustomIconValue })); + +interface Props { + value?: CustomIconValue; + onChange: (icon: CustomIconValue | undefined) => void; + disabled?: boolean; + label: string; + fallback?: LucideIcon; +} + +function CustomIconPicker({ value, onChange, disabled, label, fallback = AstroidIcon }: Props) { + const t = useTranslate(); + const id = useId(); + const [open, setOpen] = useState(false); + const [tab, setTab] = useState("icons"); + const [query, setQuery] = useState(""); + const [focusedIndex, setFocusedIndex] = useState(0); + const searchRef = useRef(null); + const gridRef = useRef(null); + const search = query.trim().toLowerCase(); + const options: { label: string; icon: CustomIconValue }[] = (tab === "icons" ? symbols : emojis).filter( + (option) => option.label.toLowerCase().includes(search) || option.icon.value.value?.includes(search), + ); + const customEmoji = query.trim() || (value?.value.case === "emoji" ? value.value.value : ""); + if (tab === "emoji" && emojiSet.has(customEmoji) && !options.some((option) => option.icon.value.value === customEmoji)) { + options.unshift({ + label: t("space.icon.use-emoji", { emoji: customEmoji }), + icon: { value: { case: "emoji", value: customEmoji } }, + }); + } + + const select = (icon?: CustomIconValue) => { + onChange(icon); + setOpen(false); + }; + const changeTab = (next: string) => { + setTab(next); + setQuery(""); + setFocusedIndex(0); + }; + const moveFocus = (event: KeyboardEvent, index: number) => { + const offsets: Record = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -8, ArrowDown: 8 }; + let next = index; + if (event.key in offsets) next = Math.max(0, Math.min(options.length - 1, index + offsets[event.key])); + else if (event.key === "Home") next = 0; + else if (event.key === "End") next = options.length - 1; + else return; + event.preventDefault(); + setFocusedIndex(next); + gridRef.current?.querySelectorAll("button")[next]?.focus(); + }; + + return ( + { + if (next) { + changeTab(value?.value.case === "emoji" ? "emoji" : "icons"); + } + setOpen(next); + }} + > + + } + > + + + + + + {(["icons", "emoji"] as const).map((name) => ( + { + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { + event.preventDefault(); + const next = name === "icons" ? "emoji" : "icons"; + changeTab(next); + document.getElementById(`${id}-${next}`)?.focus(); + } + }} + > + {t(`space.icon.${name}`)} + + ))} + + +
+
+ + { + setQuery(event.target.value); + setFocusedIndex(0); + }} + onKeyDown={(event) => { + if (event.nativeEvent.isComposing || event.keyCode === 229) return; + if (event.key === "Enter") { + event.preventDefault(); + if (options[0]) select(options[0].icon); + } else if (event.key === "ArrowDown" && options.length) { + event.preventDefault(); + setFocusedIndex(0); + gridRef.current?.querySelector("button")?.focus(); + } + }} + /> +
+
+ {options.length ? ( +
+ {options.map((option, index) => ( + + ))} +
+ ) : ( +

+ {t("space.icon.no-results")} +

+ )} +
+ {tab === "emoji" &&

{t("space.icon.emoji-help")}

} +
+
+ +
+
+
+ ); +} + +export default CustomIconPicker; diff --git a/web/src/components/MemoFilters.tsx b/web/src/components/MemoFilters.tsx index 896c1ea4..e9f0975c 100644 --- a/web/src/components/MemoFilters.tsx +++ b/web/src/components/MemoFilters.tsx @@ -10,12 +10,13 @@ import { LinkIcon, type LucideIcon, MapPinIcon, - ParenthesesIcon, SearchIcon, SquareCheckIcon, XIcon, } from "lucide-react"; +import type { ReactNode } from "react"; import { useLocation } from "react-router-dom"; +import MemoViewIcon from "@/components/MemoViewIcon"; import { Button } from "@/components/ui/button"; import { useAppSidebar } from "@/contexts/AppSidebarContext"; import { type FilterFactor, getMemoFilterKey, type MemoFilter, useMemoFilterContext } from "@/contexts/MemoFilterContext"; @@ -83,6 +84,7 @@ const FILTER_CONFIGS: Record = { interface FilterChipProps { icon?: LucideIcon; + customIcon?: ReactNode; label: string; onRemove: () => void; /** When set, the label is a button that edits the filter; the label is also shown in full as a tooltip. */ @@ -90,10 +92,10 @@ interface FilterChipProps { } /** One chip for anything narrowing the list, so a view, a tag and a day are announced the same way. */ -const FilterChip = ({ icon: Icon, label, onRemove, onEdit }: FilterChipProps) => { +const FilterChip = ({ icon: Icon, customIcon, label, onRemove, onEdit }: FilterChipProps) => { const body = ( <> - {Icon && } + {customIcon ?? (Icon && )} {label} ); @@ -151,8 +153,13 @@ const MemoFilters = ({ className }: { className?: string }) => { const viewChip = (() => { if (!viewApplies) return null; if (memoView === BUILTIN_TASKS_VIEW_ID) return { icon: SquareCheckIcon, label: t("common.tasks") }; - const title = memoViews.find((item) => getMemoViewId(item.name) === memoView)?.title; - return title ? { icon: ParenthesesIcon, label: title } : null; + const view = memoViews.find((item) => getMemoViewId(item.name) === memoView); + return view?.title + ? { + customIcon: , + label: view.title, + } + : null; })(); if (filters.length === 0 && !viewChip) { @@ -161,7 +168,9 @@ const MemoFilters = ({ className }: { className?: string }) => { return (
- {viewChip && setMemoView(undefined)} />} + {viewChip && ( + setMemoView(undefined)} /> + )} {filters.map((filter) => ( ( + +); + +export default MemoViewIcon; diff --git a/web/src/components/Settings/SpacesSection.tsx b/web/src/components/Settings/SpacesSection.tsx index 0f8ad299..1dde6557 100644 --- a/web/src/components/Settings/SpacesSection.tsx +++ b/web/src/components/Settings/SpacesSection.tsx @@ -42,10 +42,10 @@ import { useUserSpaceInvitations, } from "@/hooks/useSpaceQueries"; import { useUsersByUsernames } from "@/hooks/useUserQueries"; +import { customIconsEqual } from "@/lib/custom-icons"; import { handleError } from "@/lib/error"; import { extractUsernameFromName } from "@/lib/resource-names"; import { extractSpaceUidFromName } from "@/lib/space-display"; -import { spaceIconsEqual } from "@/lib/space-icons"; import { ROUTES } from "@/router/routes"; import { type Space, type SpaceInvitation, type SpaceMember, SpaceMember_Role } from "@/types/proto/api/v1/space_service_pb"; import type { User } from "@/types/proto/api/v1/user_service_pb"; @@ -370,7 +370,7 @@ const SpaceDetail = ({ space, viewerName, onBack }: SpaceDetailProps) => { { value: String(SpaceMember_Role.USER), label: t("setting.spaces.space-user") }, { value: String(SpaceMember_Role.ADMIN), label: t("setting.spaces.space-admin") }, ]; - const iconChanged = !spaceIconsEqual(icon, space.icon); + const iconChanged = !customIconsEqual(icon, space.icon); const detailsChanged = title.trim() !== space.title || description.trim() !== space.description || iconChanged; const spaceUid = extractSpaceUidFromName(space.name); const disambiguatedSpaceTitle = `${space.title} (${spaceUid})`; diff --git a/web/src/components/SpaceIcon.tsx b/web/src/components/SpaceIcon.tsx index ff18413a..cc3f321a 100644 --- a/web/src/components/SpaceIcon.tsx +++ b/web/src/components/SpaceIcon.tsx @@ -1,22 +1,6 @@ -import { getSpaceSymbol } from "@/lib/space-icons"; -import { cn } from "@/lib/utils"; +import CustomIcon from "@/components/CustomIcon"; import type { Space_Icon } from "@/types/proto/api/v1/space_service_pb"; -interface Props { - icon?: Space_Icon; - className?: string; -} - -function SpaceIcon({ icon, className }: Props) { - if (icon?.value.case === "emoji" && icon.value.value) { - return ( - - {icon.value.value} - - ); - } - const IconComponent = getSpaceSymbol(icon?.value.case === "lucide" ? icon.value.value : "astroid"); - return ; -} +const SpaceIcon = ({ icon, className }: { icon?: Space_Icon; className?: string }) => ; export default SpaceIcon; diff --git a/web/src/components/SpaceIconPicker.tsx b/web/src/components/SpaceIconPicker.tsx index e2cd0974..57b2c8fe 100644 --- a/web/src/components/SpaceIconPicker.tsx +++ b/web/src/components/SpaceIconPicker.tsx @@ -1,36 +1,7 @@ import { create } from "@bufbuild/protobuf"; -import { RotateCcwIcon, SearchIcon } from "lucide-react"; -import { type KeyboardEvent, useId, useRef, useState } from "react"; -import SpaceIcon from "@/components/SpaceIcon"; -import SpaceMark from "@/components/SpaceMark"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { SPACE_EMOJI } from "@/lib/space-emoji"; -import { SPACE_SYMBOLS, spaceIconsEqual } from "@/lib/space-icons"; -import { cn } from "@/lib/utils"; +import CustomIconPicker from "@/components/CustomIconPicker"; import { type Space_Icon, Space_IconSchema } from "@/types/proto/api/v1/space_service_pb"; import { useTranslate } from "@/utils/i18n"; -import { FULLY_QUALIFIED_EMOJI } from "@/utils/tag-unicode-data"; - -const emojiSet = new Set(FULLY_QUALIFIED_EMOJI); -const symbolLabels: Record = { - astroid: "Space", - "book-open": "Book", - "chart-no-axes-combined": "Chart", - "check-check": "Checklist", - "flask-conical": "Science", - "flower-2": "Flower", - "graduation-cap": "Education", - "map-pin": "Location", - "notebook-pen": "Notebook", -}; -const symbols = Object.keys(SPACE_SYMBOLS).map((name) => ({ - label: symbolLabels[name] ?? name.replaceAll("-", " ").replace(/^./, (char) => char.toUpperCase()), - icon: create(Space_IconSchema, { value: { case: "lucide", value: name } }), -})); -const emojis = SPACE_EMOJI.map(([emoji, label]) => ({ label, icon: create(Space_IconSchema, { value: { case: "emoji", value: emoji } }) })); interface Props { value?: Space_Icon; @@ -38,168 +9,16 @@ interface Props { disabled?: boolean; } -function SpaceIconPicker({ value, onChange, disabled }: Props) { +const SpaceIconPicker = ({ value, onChange, disabled }: Props) => { const t = useTranslate(); - const id = useId(); - const [open, setOpen] = useState(false); - const [tab, setTab] = useState("icons"); - const [query, setQuery] = useState(""); - const [focusedIndex, setFocusedIndex] = useState(0); - const searchRef = useRef(null); - const gridRef = useRef(null); - const search = query.trim().toLowerCase(); - const options = (tab === "icons" ? symbols : emojis).filter( - (option) => option.label.toLowerCase().includes(search) || option.icon.value.value?.includes(search), - ); - const customEmoji = query.trim() || (value?.value.case === "emoji" ? value.value.value : ""); - if (tab === "emoji" && emojiSet.has(customEmoji) && !options.some((option) => option.icon.value.value === customEmoji)) { - options.unshift({ - label: t("space.icon.use-emoji", { emoji: customEmoji }), - icon: create(Space_IconSchema, { value: { case: "emoji", value: customEmoji } }), - }); - } - - const select = (icon?: Space_Icon) => { - onChange(icon); - setOpen(false); - }; - const changeTab = (next: string) => { - setTab(next); - setQuery(""); - setFocusedIndex(0); - }; - const moveFocus = (event: KeyboardEvent, index: number) => { - const offsets: Record = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -8, ArrowDown: 8 }; - let next = index; - if (event.key in offsets) next = Math.max(0, Math.min(options.length - 1, index + offsets[event.key])); - else if (event.key === "Home") next = 0; - else if (event.key === "End") next = options.length - 1; - else return; - event.preventDefault(); - setFocusedIndex(next); - gridRef.current?.querySelectorAll("button")[next]?.focus(); - }; - return ( - { - if (next) { - changeTab(value?.value.case === "emoji" ? "emoji" : "icons"); - } - setOpen(next); - }} - > - - } - > - - - - - - {(["icons", "emoji"] as const).map((name) => ( - { - if (event.key === "ArrowLeft" || event.key === "ArrowRight") { - event.preventDefault(); - const next = name === "icons" ? "emoji" : "icons"; - changeTab(next); - document.getElementById(`${id}-${next}`)?.focus(); - } - }} - > - {t(`space.icon.${name}`)} - - ))} - - -
-
- - { - setQuery(event.target.value); - setFocusedIndex(0); - }} - onKeyDown={(event) => { - if (event.nativeEvent.isComposing || event.keyCode === 229) return; - if (event.key === "Enter") { - event.preventDefault(); - if (options[0]) select(options[0].icon); - } else if (event.key === "ArrowDown" && options.length) { - event.preventDefault(); - setFocusedIndex(0); - gridRef.current?.querySelector("button")?.focus(); - } - }} - /> -
-
- {options.length ? ( -
- {options.map((option, index) => ( - - ))} -
- ) : ( -

- {t("space.icon.no-results")} -

- )} -
- {tab === "emoji" &&

{t("space.icon.emoji-help")}

} -
-
- -
-
-
+ onChange(icon ? create(Space_IconSchema, icon) : undefined)} + disabled={disabled} + label={t("space.icon.change")} + /> ); -} +}; export default SpaceIconPicker; diff --git a/web/src/lib/space-icons.ts b/web/src/lib/custom-icons.ts similarity index 72% rename from web/src/lib/space-icons.ts rename to web/src/lib/custom-icons.ts index 23ee9d7c..eb5dfedf 100644 --- a/web/src/lib/space-icons.ts +++ b/web/src/lib/custom-icons.ts @@ -19,6 +19,7 @@ import { HouseIcon, LeafIcon, LightbulbIcon, + type LucideIcon, MapPinIcon, MountainIcon, MusicIcon, @@ -38,10 +39,13 @@ import { WrenchIcon, ZapIcon, } from "lucide-react"; -import type { Space_Icon } from "@/types/proto/api/v1/space_service_pb"; +// Shared display shape; each resource retains its own protobuf message type. +export interface CustomIconValue { + value: { case: "emoji" | "lucide"; value: string } | { case: undefined; value?: undefined }; +} // Keep the catalog explicit so adding a picker does not bundle the entire icon library. -export const SPACE_SYMBOLS = { +export const CUSTOM_SYMBOLS = { astroid: AstroidIcon, "book-open": BookOpenIcon, briefcase: BriefcaseIcon, @@ -82,7 +86,8 @@ export const SPACE_SYMBOLS = { zap: ZapIcon, }; -export const getSpaceSymbol = (name: string) => - Object.hasOwn(SPACE_SYMBOLS, name) ? SPACE_SYMBOLS[name as keyof typeof SPACE_SYMBOLS] : AstroidIcon; +export const getCustomSymbol = (name: string, fallback: LucideIcon = AstroidIcon) => + Object.hasOwn(CUSTOM_SYMBOLS, name) ? CUSTOM_SYMBOLS[name as keyof typeof CUSTOM_SYMBOLS] : fallback; -export const spaceIconsEqual = (a?: Space_Icon, b?: Space_Icon) => a?.value.case === b?.value.case && a?.value.value === b?.value.value; +export const customIconsEqual = (a?: CustomIconValue, b?: CustomIconValue) => + a?.value.case === b?.value.case && a?.value.value === b?.value.value; diff --git a/web/src/locales/az.json b/web/src/locales/az.json index c0bb1989..680f45be 100644 --- a/web/src/locales/az.json +++ b/web/src/locales/az.json @@ -854,6 +854,7 @@ "uploads-privacy-title": "Yükləmələr və məxfilik" }, "memo-view": { + "change-icon": "Görünüş ikonunu dəyiş", "delete-confirm": "`{{title}}` görünüşünü silmək istədiyinizə əminsiniz?", "delete-success": "`{{title}}` görünüşü silindi" }, diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 592c70fe..e2786193 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -854,6 +854,7 @@ "uploads-privacy-title": "Uploads & privacy" }, "memo-view": { + "change-icon": "Change view icon", "delete-confirm": "Are you sure you want to delete view `{{title}}`?", "delete-success": "View `{{title}}` deleted successfully" }, diff --git a/web/src/pages/MemoViews.tsx b/web/src/pages/MemoViews.tsx index 2da747b7..47bb5adb 100644 --- a/web/src/pages/MemoViews.tsx +++ b/web/src/pages/MemoViews.tsx @@ -10,6 +10,7 @@ import { FilterIcon, MapPinIcon, MoreVerticalIcon, + ParenthesesIcon, PencilIcon, PinIcon, PlusIcon, @@ -24,6 +25,8 @@ import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { useLocation, useNavigate } from "react-router-dom"; import ConfirmDialog from "@/components/ConfirmDialog"; +import CustomIconPicker from "@/components/CustomIconPicker"; +import MemoViewIcon from "@/components/MemoViewIcon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; @@ -38,7 +41,7 @@ import { useMemoViews, userKeys } from "@/hooks/useUserQueries"; import { handleError } from "@/lib/error"; import { getMemoViewId } from "@/lib/memo-views"; import { cn } from "@/lib/utils"; -import { MemoView, MemoViewSchema } from "@/types/proto/api/v1/user_service_pb"; +import { MemoView, MemoView_IconSchema, MemoViewSchema } from "@/types/proto/api/v1/user_service_pb"; import { useTranslate } from "@/utils/i18n"; const memoViewExamples = [ @@ -287,6 +290,7 @@ const MemoViews = () => { name: state.memoView.name, title: state.memoView.title, filter: state.memoView.filter, + icon: state.memoView.icon, }), ); setIsCreateFormOpen(true); @@ -308,6 +312,7 @@ const MemoViews = () => { name: draft.name, title: draft.title || example.title, filter: example.filter, + icon: draft.icon, }), ); setIsCreateFormOpen(true); @@ -329,6 +334,7 @@ const MemoViews = () => { name: memoView.name, title: memoView.title, filter: memoView.filter, + icon: memoView.icon, }), ); setIsCreateFormOpen(true); @@ -348,7 +354,7 @@ const MemoViews = () => { validateState.setLoading(); await userServiceClient.createMemoView({ parent: user.name, - memoView: { name: "", title: draft.title, filter: draft.filter }, + memoView: { name: "", title: draft.title, filter: draft.filter, icon: draft.icon }, validateOnly: true, }); validateState.setFinish(); @@ -377,7 +383,7 @@ const MemoViews = () => { createState.setLoading(); await userServiceClient.createMemoView({ parent: user.name, - memoView: { name: "", title: draft.title, filter: draft.filter }, + memoView: { name: "", title: draft.title, filter: draft.filter, icon: draft.icon }, }); await queryClient.invalidateQueries({ queryKey: userKeys.memoViews(user.name) }); createState.setFinish(); @@ -402,7 +408,7 @@ const MemoViews = () => { updateState.setLoading(); await userServiceClient.updateMemoView({ memoView: draft, - updateMask: create(FieldMaskSchema, { paths: ["title", "filter"] }), + updateMask: create(FieldMaskSchema, { paths: ["title", "filter", "icon"] }), }); await queryClient.invalidateQueries({ queryKey: userKeys.memoViews(user?.name) }); updateState.setFinish(); @@ -491,15 +497,21 @@ const MemoViews = () => {
- setDraftState({ title: event.target.value })} - /> -

- Prefix the title with an emoji if you want it to appear in the sidebar. -

+
+ setDraftState({ icon: icon ? create(MemoView_IconSchema, icon) : undefined })} + label={t("setting.memo-view.change-icon")} + fallback={ParenthesesIcon} + disabled={isSaving} + /> + setDraftState({ title: event.target.value })} + /> +
@@ -554,7 +566,10 @@ const MemoViews = () => { className="grid gap-3 bg-background px-4 py-3 sm:grid-cols-[minmax(10rem,14rem)_minmax(0,1fr)_2rem]" >
-
{memoView.title}
+
+ + {memoView.title} +
{getMemoViewId(memoView.name)}
diff --git a/web/src/types/proto/api/v1/user_service_pb.ts b/web/src/types/proto/api/v1/user_service_pb.ts
index a5274517..b8b2adf6 100644
--- a/web/src/types/proto/api/v1/user_service_pb.ts
+++ b/web/src/types/proto/api/v1/user_service_pb.ts
@@ -22,7 +22,7 @@ import type { Message } from "@bufbuild/protobuf";
  * Describes the file api/v1/user_service.proto.
  */
 export const file_api_v1_user_service: GenFile = /*@__PURE__*/
-  fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJPChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSIpChRCYXRjaEdldFVzZXJzUmVxdWVzdBIRCgl1c2VybmFtZXMYASADKAkiOgoVQmF0Y2hHZXRVc2Vyc1Jlc3BvbnNlEiEKBXVzZXJzGAEgAygLMhIubWVtb3MuYXBpLnYxLlVzZXIibQoOR2V0VXNlclJlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIyCglyZWFkX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQEiiAEKEUNyZWF0ZVVzZXJSZXF1ZXN0EigKBHVzZXIYASABKAsyEi5tZW1vcy5hcGkudjEuVXNlckIG4EEC4EEEEhQKB3VzZXJfaWQYAiABKAlCA+BBARIaCg12YWxpZGF0ZV9vbmx5GAMgASgIQgPgQQESFwoKcmVxdWVzdF9pZBgEIAEoCUID4EEBIowBChFVcGRhdGVVc2VyUmVxdWVzdBIlCgR1c2VyGAEgASgLMhIubWVtb3MuYXBpLnYxLlVzZXJCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAhIaCg1hbGxvd19taXNzaW5nGAMgASgIQgPgQQEiUAoRRGVsZXRlVXNlclJlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchISCgVmb3JjZRgCIAEoCEID4EEBItIECglVc2VyU3RhdHMSEQoEbmFtZRgBIAEoCUID4EEIEj4KD21lbW9fdHlwZV9zdGF0cxgDIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuTWVtb1R5cGVTdGF0cxI4Cgl0YWdfY291bnQYBCADKAsyJS5tZW1vcy5hcGkudjEuVXNlclN0YXRzLlRhZ0NvdW50RW50cnkSOwoXbWVtb19jcmVhdGVkX3RpbWVzdGFtcHMYByADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjsKF21lbW9fdXBkYXRlZF90aW1lc3RhbXBzGAggAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIsCgxwaW5uZWRfbWVtb3MYBSADKAlCFvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SGAoQdG90YWxfbWVtb19jb3VudBgGIAEoBRovCg1UYWdDb3VudEVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEaXwoNTWVtb1R5cGVTdGF0cxISCgpsaW5rX2NvdW50GAEgASgFEhIKCmNvZGVfY291bnQYAiABKAUSEgoKdG9kb19jb3VudBgDIAEoBRISCgp1bmRvX2NvdW50GAQgASgFOkXqQUIKFm1lbW9zLmFwaS52MS9Vc2VyU3RhdHMSEnVzZXJzL3t1c2VyfS9zdGF0cyoJdXNlclN0YXRzMgl1c2VyU3RhdHNKBAgCEANSF21lbW9fZGlzcGxheV90aW1lc3RhbXBzImwKE0dldFVzZXJTdGF0c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchITCgZmaWx0ZXIYAiABKAlCA+BBAUoECAMQBFIFc3BhY2VSCnVuYXNzaWduZWQidgoXTGlzdEFsbFVzZXJTdGF0c1JlcXVlc3QSJwoFc3RhdGUYASABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBARITCgZmaWx0ZXIYAiABKAlCA+BBAUoECAMQBEoECAQQBVIFc3BhY2VSCnVuYXNzaWduZWQiQgoYTGlzdEFsbFVzZXJTdGF0c1Jlc3BvbnNlEiYKBXN0YXRzGAEgAygLMhcubWVtb3MuYXBpLnYxLlVzZXJTdGF0cyLQBgoLVXNlclNldHRpbmcSEQoEbmFtZRgBIAEoCUID4EEIEkMKD2dlbmVyYWxfc2V0dGluZxgCIAEoCzIoLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5HZW5lcmFsU2V0dGluZ0gAEkUKEHdlYmhvb2tzX3NldHRpbmcYBSABKAsyKS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcuV2ViaG9va3NTZXR0aW5nSAASPQoMdGFnc19zZXR0aW5nGAYgASgLMiUubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLlRhZ3NTZXR0aW5nSAAaeQoOR2VuZXJhbFNldHRpbmcSEwoGbG9jYWxlGAEgASgJQgPgQQESHAoPbWVtb192aXNpYmlsaXR5GAMgASgJQgPgQQESEgoFdGhlbWUYBCABKAlCA+BBARIgChNzYXZlX21lZGlhX21ldGFkYXRhGAUgASgIQgPgQQEaWwoLVGFnTWV0YWRhdGESMQoQYmFja2dyb3VuZF9jb2xvchgBIAEoCzISLmdvb2dsZS50eXBlLkNvbG9yQgPgQQESGQoMYmx1cl9jb250ZW50GAIgASgIQgPgQQEapQEKC1RhZ3NTZXR0aW5nEkIKBHRhZ3MYASADKAsyLy5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcuVGFnc1NldHRpbmcuVGFnc0VudHJ5QgPgQQEaUgoJVGFnc0VudHJ5EgsKA2tleRgBIAEoCRI0CgV2YWx1ZRgCIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5UYWdNZXRhZGF0YToCOAEaPgoPV2ViaG9va3NTZXR0aW5nEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIj8KA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESDAoIV0VCSE9PS1MQBBIICgRUQUdTEAU6WepBVgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nEh91c2Vycy97dXNlcn0vc2V0dGluZ3Mve3NldHRpbmd9Kgx1c2VyU2V0dGluZ3MyC3VzZXJTZXR0aW5nQgcKBXZhbHVlIkcKFUdldFVzZXJTZXR0aW5nUmVxdWVzdBIuCgRuYW1lGAEgASgJQiDgQQL6QRoKGG1lbW9zLmFwaS52MS9Vc2VyU2V0dGluZyKBAQoYVXBkYXRlVXNlclNldHRpbmdSZXF1ZXN0Ei8KB3NldHRpbmcYASABKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmdCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAiJ1ChdMaXN0VXNlclNldHRpbmdzUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBImAKGExpc3RVc2VyU2V0dGluZ3NSZXNwb25zZRIrCghzZXR0aW5ncxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZxIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkiigEKCE1lbW9WaWV3EhEKBG5hbWUYASABKAlCA+BBCBISCgV0aXRsZRgCIAEoCUID4EECEhMKBmZpbHRlchgDIAEoCUID4EECOkLqQT8KFW1lbW9zLmFwaS52MS9NZW1vVmlldxIZdXNlcnMve3VzZXJ9L3ZpZXdzL3t2aWV3fSoFdmlld3MyBHZpZXciRQoUTGlzdE1lbW9WaWV3c1JlcXVlc3QSLQoGcGFyZW50GAEgASgJQh3gQQL6QRcSFW1lbW9zLmFwaS52MS9NZW1vVmlldyJDChVMaXN0TWVtb1ZpZXdzUmVzcG9uc2USKgoKbWVtb192aWV3cxgBIAMoCzIWLm1lbW9zLmFwaS52MS5NZW1vVmlldyJBChJHZXRNZW1vVmlld1JlcXVlc3QSKwoEbmFtZRgBIAEoCUId4EEC+kEXChVtZW1vcy5hcGkudjEvTWVtb1ZpZXcikgEKFUNyZWF0ZU1lbW9WaWV3UmVxdWVzdBItCgZwYXJlbnQYASABKAlCHeBBAvpBFxIVbWVtb3MuYXBpLnYxL01lbW9WaWV3Ei4KCW1lbW9fdmlldxgCIAEoCzIWLm1lbW9zLmFwaS52MS5NZW1vVmlld0ID4EECEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBASJ9ChVVcGRhdGVNZW1vVmlld1JlcXVlc3QSLgoJbWVtb192aWV3GAEgASgLMhYubWVtb3MuYXBpLnYxLk1lbW9WaWV3QgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQEiRAoVRGVsZXRlTWVtb1ZpZXdSZXF1ZXN0EisKBG5hbWUYASABKAlCHeBBAvpBFwoVbWVtb3MuYXBpLnYxL01lbW9WaWV3IuoBCg5MaW5rZWRJZGVudGl0eRIRCgRuYW1lGAEgASgJQgPgQQgSNwoIaWRwX25hbWUYAiABKAlCJeBBA/pBHwodbWVtb3MuYXBpLnYxL0lkZW50aXR5UHJvdmlkZXISFwoKZXh0ZXJuX3VpZBgDIAEoCUID4EEDOnPqQXAKG21lbW9zLmFwaS52MS9MaW5rZWRJZGVudGl0eRIvdXNlcnMve3VzZXJ9L2xpbmtlZElkZW50aXRpZXMve2xpbmtlZF9pZGVudGl0eX0qEGxpbmtlZElkZW50aXRpZXMyDmxpbmtlZElkZW50aXR5IkgKG0xpc3RMaW5rZWRJZGVudGl0aWVzUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXIiVwocTGlzdExpbmtlZElkZW50aXRpZXNSZXNwb25zZRI3ChFsaW5rZWRfaWRlbnRpdGllcxgBIAMoCzIcLm1lbW9zLmFwaS52MS5MaW5rZWRJZGVudGl0eSLLAQobQ3JlYXRlTGlua2VkSWRlbnRpdHlSZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchI3CghpZHBfbmFtZRgCIAEoCUIl4EEC+kEfCh1tZW1vcy5hcGkudjEvSWRlbnRpdHlQcm92aWRlchIRCgRjb2RlGAMgASgJQgPgQQISGQoMcmVkaXJlY3RfdXJpGAQgASgJQgPgQQISGgoNY29kZV92ZXJpZmllchgFIAEoCUID4EEBIk0KGEdldExpbmtlZElkZW50aXR5UmVxdWVzdBIxCgRuYW1lGAEgASgJQiPgQQL6QR0KG21lbW9zLmFwaS52MS9MaW5rZWRJZGVudGl0eSJQChtEZWxldGVMaW5rZWRJZGVudGl0eVJlcXVlc3QSMQoEbmFtZRgBIAEoCUIj4EEC+kEdChttZW1vcy5hcGkudjEvTGlua2VkSWRlbnRpdHki8gIKE1BlcnNvbmFsQWNjZXNzVG9rZW4SEQoEbmFtZRgBIAEoCUID4EEIEhgKC2Rlc2NyaXB0aW9uGAIgASgJQgPgQQESMwoKY3JlYXRlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxIzCgpleHBpcmVzX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEBEjUKDGxhc3RfdXNlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAzqMAepBiAEKIG1lbW9zLmFwaS52MS9QZXJzb25hbEFjY2Vzc1Rva2VuEjl1c2Vycy97dXNlcn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMve3BlcnNvbmFsX2FjY2Vzc190b2tlbn0qFHBlcnNvbmFsQWNjZXNzVG9rZW5zMhNwZXJzb25hbEFjY2Vzc1Rva2VuIn0KH0xpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASJ+CiBMaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXNwb25zZRJBChZwZXJzb25hbF9hY2Nlc3NfdG9rZW5zGAEgAygLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIoUBCiBDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISGAoLZGVzY3JpcHRpb24YAiABKAlCA+BBARIcCg9leHBpcmVzX2luX2RheXMYAyABKAVCA+BBASJ0CiFDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2USQAoVcGVyc29uYWxfYWNjZXNzX3Rva2VuGAEgASgLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SDQoFdG9rZW4YAiABKAkiWgogRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QSNgoEbmFtZRgBIAEoCUIo4EEC+kEiCiBtZW1vcy5hcGkudjEvUGVyc29uYWxBY2Nlc3NUb2tlbiLIAgoLVXNlcldlYmhvb2sSEQoEbmFtZRgBIAEoCUID4EEIEgsKA3VybBgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNAoLdXBkYXRlX3RpbWUYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSGwoOc2lnbmluZ19zZWNyZXQYBiABKAlCA+BBBBIfChJzaWduaW5nX3NlY3JldF9zZXQYByABKAhCA+BBAzpZ6kFWChhtZW1vcy5hcGkudjEvVXNlcldlYmhvb2sSH3VzZXJzL3t1c2VyfS93ZWJob29rcy97d2ViaG9va30qDHVzZXJXZWJob29rczILdXNlcldlYmhvb2siSwoXTGlzdFVzZXJXZWJob29rc1JlcXVlc3QSMAoGcGFyZW50GAEgASgJQiDgQQL6QRoSGG1lbW9zLmFwaS52MS9Vc2VyV2ViaG9vayJHChhMaXN0VXNlcldlYmhvb2tzUmVzcG9uc2USKwoId2ViaG9va3MYASADKAsyGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2sifQoYQ3JlYXRlVXNlcldlYmhvb2tSZXF1ZXN0EjAKBnBhcmVudBgBIAEoCUIg4EEC+kEaEhhtZW1vcy5hcGkudjEvVXNlcldlYmhvb2sSLwoHd2ViaG9vaxgCIAEoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9va0ID4EECInwKGFVwZGF0ZVVzZXJXZWJob29rUmVxdWVzdBIvCgd3ZWJob29rGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rQgPgQQISLwoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrIkoKGERlbGV0ZVVzZXJXZWJob29rUmVxdWVzdBIuCgRuYW1lGAEgASgJQiDgQQL6QRoKGG1lbW9zLmFwaS52MS9Vc2VyV2ViaG9vayJUCiJHZXRVc2VyV2ViaG9va1NpZ25pbmdTZWNyZXRSZXF1ZXN0Ei4KBG5hbWUYASABKAlCIOBBAvpBGgoYbWVtb3MuYXBpLnYxL1VzZXJXZWJob29rIj0KI0dldFVzZXJXZWJob29rU2lnbmluZ1NlY3JldFJlc3BvbnNlEhYKDnNpZ25pbmdfc2VjcmV0GAEgASgJIsEKChBVc2VyTm90aWZpY2F0aW9uEhQKBG5hbWUYASABKAlCBuBBA+BBCBIpCgZzZW5kZXIYAiABKAlCGeBBA/pBEwoRbWVtb3MuYXBpLnYxL1VzZXISLAoLc2VuZGVyX3VzZXIYCCABKAsyEi5tZW1vcy5hcGkudjEuVXNlckID4EEDEjoKBnN0YXR1cxgDIAEoDjIlLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLlN0YXR1c0ID4EEBEjQKC2NyZWF0ZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjYKBHR5cGUYBSABKA4yIy5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5UeXBlQgPgQQMSTgoMbWVtb19jb21tZW50GAYgASgLMjEubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uTWVtb0NvbW1lbnRQYXlsb2FkQgPgQQNIABJOCgxtZW1vX21lbnRpb24YByABKAsyMS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5NZW1vTWVudGlvblBheWxvYWRCA+BBA0gAElYKEHNwYWNlX2ludml0YXRpb24YCSABKAsyNS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5TcGFjZUludml0YXRpb25QYXlsb2FkQgPgQQNIABpsChJNZW1vQ29tbWVudFBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkSFAoMbWVtb19zbmlwcGV0GAMgASgJEhwKFHJlbGF0ZWRfbWVtb19zbmlwcGV0GAQgASgJGmwKEk1lbW9NZW50aW9uUGF5bG9hZBIMCgRtZW1vGAEgASgJEhQKDHJlbGF0ZWRfbWVtbxgCIAEoCRIUCgxtZW1vX3NuaXBwZXQYAyABKAkSHAoUcmVsYXRlZF9tZW1vX3NuaXBwZXQYBCABKAkargIKFlNwYWNlSW52aXRhdGlvblBheWxvYWQSOwoQc3BhY2VfaW52aXRhdGlvbhgBIAEoCUIh+kEeChxtZW1vcy5hcGkudjEvU3BhY2VJbnZpdGF0aW9uEiIKBXNwYWNlGAIgASgLMhMubWVtb3MuYXBpLnYxLlNwYWNlEiwKBHJvbGUYAyABKA4yHi5tZW1vcy5hcGkudjEuU3BhY2VNZW1iZXIuUm9sZRJKCgVzdGF0ZRgEIAEoDjI7Lm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLlNwYWNlSW52aXRhdGlvblBheWxvYWQuU3RhdGUiOQoFU3RhdGUSFQoRU1RBVEVfVU5TUEVDSUZJRUQQABILCgdQRU5ESU5HEAESDAoIQUNDRVBURUQQAiI6CgZTdGF0dXMSFgoSU1RBVFVTX1VOU1BFQ0lGSUVEEAASCgoGVU5SRUFEEAESDAoIQVJDSElWRUQQAiJWCgRUeXBlEhQKEFRZUEVfVU5TUEVDSUZJRUQQABIQCgxNRU1PX0NPTU1FTlQQARIQCgxNRU1PX01FTlRJT04QAhIUChBTUEFDRV9JTlZJVEFUSU9OEAM6cOpBbQodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24SKXVzZXJzL3t1c2VyfS9ub3RpZmljYXRpb25zL3tub3RpZmljYXRpb259GgRuYW1lKg1ub3RpZmljYXRpb25zMgxub3RpZmljYXRpb25CCQoHcGF5bG9hZCKPAQocTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBEhMKBmZpbHRlchgEIAEoCUID4EEBIm8KHUxpc3RVc2VyTm90aWZpY2F0aW9uc1Jlc3BvbnNlEjUKDW5vdGlmaWNhdGlvbnMYASADKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikAEKHVVwZGF0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjkKDG5vdGlmaWNhdGlvbhgBIAEoCzIeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIiVAodRGVsZXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QSMwoEbmFtZRgBIAEoCUIl4EEC+kEfCh1tZW1vcy5hcGkudjEvVXNlck5vdGlmaWNhdGlvbjKPJAoLVXNlclNlcnZpY2USYwoJTGlzdFVzZXJzEh4ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1JlcXVlc3QaHy5tZW1vcy5hcGkudjEuTGlzdFVzZXJzUmVzcG9uc2UiFYLT5JMCDxINL2FwaS92MS91c2VycxJ7Cg1CYXRjaEdldFVzZXJzEiIubWVtb3MuYXBpLnYxLkJhdGNoR2V0VXNlcnNSZXF1ZXN0GiMubWVtb3MuYXBpLnYxLkJhdGNoR2V0VXNlcnNSZXNwb25zZSIhgtPkkwIbOgEqIhYvYXBpL3YxL3VzZXJzOmJhdGNoR2V0EmIKB0dldFVzZXISHC5tZW1vcy5hcGkudjEuR2V0VXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciIl2kEEbmFtZYLT5JMCGBIWL2FwaS92MS97bmFtZT11c2Vycy8qfRJtCgpDcmVhdGVVc2VyEh8ubWVtb3MuYXBpLnYxLkNyZWF0ZVVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiKtpBDHVzZXIsdXNlcl9pZILT5JMCFToEdXNlciINL2FwaS92MS91c2VycxJ/CgpVcGRhdGVVc2VyEh8ubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiPNpBEHVzZXIsdXBkYXRlX21hc2uC0+STAiM6BHVzZXIyGy9hcGkvdjEve3VzZXIubmFtZT11c2Vycy8qfRJsCgpEZWxldGVVc2VyEh8ubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IiXaQQRuYW1lgtPkkwIYKhYvYXBpL3YxL3tuYW1lPXVzZXJzLyp9En4KEExpc3RBbGxVc2VyU3RhdHMSJS5tZW1vcy5hcGkudjEuTGlzdEFsbFVzZXJTdGF0c1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdEFsbFVzZXJTdGF0c1Jlc3BvbnNlIhuC0+STAhUSEy9hcGkvdjEvdXNlcnM6c3RhdHMSegoMR2V0VXNlclN0YXRzEiEubWVtb3MuYXBpLnYxLkdldFVzZXJTdGF0c1JlcXVlc3QaFy5tZW1vcy5hcGkudjEuVXNlclN0YXRzIi7aQQRuYW1lgtPkkwIhEh8vYXBpL3YxL3tuYW1lPXVzZXJzLyp9OmdldFN0YXRzEoIBCg5HZXRVc2VyU2V0dGluZxIjLm1lbW9zLmFwaS52MS5HZXRVc2VyU2V0dGluZ1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmciMNpBBG5hbWWC0+STAiMSIS9hcGkvdjEve25hbWU9dXNlcnMvKi9zZXR0aW5ncy8qfRKoAQoRVXBkYXRlVXNlclNldHRpbmcSJi5tZW1vcy5hcGkudjEuVXBkYXRlVXNlclNldHRpbmdSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nIlDaQRNzZXR0aW5nLHVwZGF0ZV9tYXNrgtPkkwI0OgdzZXR0aW5nMikvYXBpL3YxL3tzZXR0aW5nLm5hbWU9dXNlcnMvKi9zZXR0aW5ncy8qfRKVAQoQTGlzdFVzZXJTZXR0aW5ncxIlLm1lbW9zLmFwaS52MS5MaXN0VXNlclNldHRpbmdzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0VXNlclNldHRpbmdzUmVzcG9uc2UiMtpBBnBhcmVudILT5JMCIxIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3NldHRpbmdzEokBCg1MaXN0TWVtb1ZpZXdzEiIubWVtb3MuYXBpLnYxLkxpc3RNZW1vVmlld3NSZXF1ZXN0GiMubWVtb3MuYXBpLnYxLkxpc3RNZW1vVmlld3NSZXNwb25zZSIv2kEGcGFyZW50gtPkkwIgEh4vYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vdmlld3MSdgoLR2V0TWVtb1ZpZXcSIC5tZW1vcy5hcGkudjEuR2V0TWVtb1ZpZXdSZXF1ZXN0GhYubWVtb3MuYXBpLnYxLk1lbW9WaWV3Ii3aQQRuYW1lgtPkkwIgEh4vYXBpL3YxL3tuYW1lPXVzZXJzLyovdmlld3MvKn0SkwEKDkNyZWF0ZU1lbW9WaWV3EiMubWVtb3MuYXBpLnYxLkNyZWF0ZU1lbW9WaWV3UmVxdWVzdBoWLm1lbW9zLmFwaS52MS5NZW1vVmlldyJE2kEQcGFyZW50LG1lbW9fdmlld4LT5JMCKzoJbWVtb192aWV3Ih4vYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vdmlld3MSogEKDlVwZGF0ZU1lbW9WaWV3EiMubWVtb3MuYXBpLnYxLlVwZGF0ZU1lbW9WaWV3UmVxdWVzdBoWLm1lbW9zLmFwaS52MS5NZW1vVmlldyJT2kEVbWVtb192aWV3LHVwZGF0ZV9tYXNrgtPkkwI1OgltZW1vX3ZpZXcyKC9hcGkvdjEve21lbW9fdmlldy5uYW1lPXVzZXJzLyovdmlld3MvKn0SfAoORGVsZXRlTWVtb1ZpZXcSIy5tZW1vcy5hcGkudjEuRGVsZXRlTWVtb1ZpZXdSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Ii3aQQRuYW1lgtPkkwIgKh4vYXBpL3YxL3tuYW1lPXVzZXJzLyovdmlld3MvKn0SqQEKFExpc3RMaW5rZWRJZGVudGl0aWVzEikubWVtb3MuYXBpLnYxLkxpc3RMaW5rZWRJZGVudGl0aWVzUmVxdWVzdBoqLm1lbW9zLmFwaS52MS5MaXN0TGlua2VkSWRlbnRpdGllc1Jlc3BvbnNlIjraQQZwYXJlbnSC0+STAisSKS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9saW5rZWRJZGVudGl0aWVzEqcBChRDcmVhdGVMaW5rZWRJZGVudGl0eRIpLm1lbW9zLmFwaS52MS5DcmVhdGVMaW5rZWRJZGVudGl0eVJlcXVlc3QaHC5tZW1vcy5hcGkudjEuTGlua2VkSWRlbnRpdHkiRtpBD3BhcmVudCxpZHBfbmFtZYLT5JMCLjoBKiIpL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L2xpbmtlZElkZW50aXRpZXMSkwEKEUdldExpbmtlZElkZW50aXR5EiYubWVtb3MuYXBpLnYxLkdldExpbmtlZElkZW50aXR5UmVxdWVzdBocLm1lbW9zLmFwaS52MS5MaW5rZWRJZGVudGl0eSI42kEEbmFtZYLT5JMCKxIpL2FwaS92MS97bmFtZT11c2Vycy8qL2xpbmtlZElkZW50aXRpZXMvKn0SkwEKFERlbGV0ZUxpbmtlZElkZW50aXR5EikubWVtb3MuYXBpLnYxLkRlbGV0ZUxpbmtlZElkZW50aXR5UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSI42kEEbmFtZYLT5JMCKyopL2FwaS92MS97bmFtZT11c2Vycy8qL2xpbmtlZElkZW50aXRpZXMvKn0SuQEKGExpc3RQZXJzb25hbEFjY2Vzc1Rva2VucxItLm1lbW9zLmFwaS52MS5MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXF1ZXN0Gi4ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1Jlc3BvbnNlIj7aQQZwYXJlbnSC0+STAi8SLS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9wZXJzb25hbEFjY2Vzc1Rva2VucxK2AQoZQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlbhIuLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBovLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2UiOILT5JMCMjoBKiItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zEqEBChlEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkRlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjzaQQRuYW1lgtPkkwIvKi0vYXBpL3YxL3tuYW1lPXVzZXJzLyovcGVyc29uYWxBY2Nlc3NUb2tlbnMvKn0SlQEKEExpc3RVc2VyV2ViaG9va3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS93ZWJob29rcxKbAQoRQ3JlYXRlVXNlcldlYmhvb2sSJi5tZW1vcy5hcGkudjEuQ3JlYXRlVXNlcldlYmhvb2tSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIkPaQQ5wYXJlbnQsd2ViaG9va4LT5JMCLDoHd2ViaG9vayIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEqgBChFVcGRhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siUNpBE3dlYmhvb2ssdXBkYXRlX21hc2uC0+STAjQ6B3dlYmhvb2syKS9hcGkvdjEve3dlYmhvb2submFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EoUBChFEZWxldGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyV2ViaG9va1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiMNpBBG5hbWWC0+STAiMqIS9hcGkvdjEve25hbWU9dXNlcnMvKi93ZWJob29rcy8qfRLFAQobR2V0VXNlcldlYmhvb2tTaWduaW5nU2VjcmV0EjAubWVtb3MuYXBpLnYxLkdldFVzZXJXZWJob29rU2lnbmluZ1NlY3JldFJlcXVlc3QaMS5tZW1vcy5hcGkudjEuR2V0VXNlcldlYmhvb2tTaWduaW5nU2VjcmV0UmVzcG9uc2UiQdpBBG5hbWWC0+STAjQSMi9hcGkvdjEve25hbWU9dXNlcnMvKi93ZWJob29rcy8qfTpnZXRTaWduaW5nU2VjcmV0EqkBChVMaXN0VXNlck5vdGlmaWNhdGlvbnMSKi5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBorLm1lbW9zLmFwaS52MS5MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZSI32kEGcGFyZW50gtPkkwIoEiYvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vbm90aWZpY2F0aW9ucxLLAQoWVXBkYXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uImTaQRhub3RpZmljYXRpb24sdXBkYXRlX21hc2uC0+STAkM6DG5vdGlmaWNhdGlvbjIzL2FwaS92MS97bm90aWZpY2F0aW9uLm5hbWU9dXNlcnMvKi9ub3RpZmljYXRpb25zLyp9EpQBChZEZWxldGVVc2VyTm90aWZpY2F0aW9uEisubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjXaQQRuYW1lgtPkkwIoKiYvYXBpL3YxL3tuYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfUKoAQoQY29tLm1lbW9zLmFwaS52MUIQVXNlclNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z", [file_api_v1_common, file_api_v1_space_service, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp, file_google_type_color]);
+  fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJPChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSIpChRCYXRjaEdldFVzZXJzUmVxdWVzdBIRCgl1c2VybmFtZXMYASADKAkiOgoVQmF0Y2hHZXRVc2Vyc1Jlc3BvbnNlEiEKBXVzZXJzGAEgAygLMhIubWVtb3MuYXBpLnYxLlVzZXIibQoOR2V0VXNlclJlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIyCglyZWFkX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQEiiAEKEUNyZWF0ZVVzZXJSZXF1ZXN0EigKBHVzZXIYASABKAsyEi5tZW1vcy5hcGkudjEuVXNlckIG4EEC4EEEEhQKB3VzZXJfaWQYAiABKAlCA+BBARIaCg12YWxpZGF0ZV9vbmx5GAMgASgIQgPgQQESFwoKcmVxdWVzdF9pZBgEIAEoCUID4EEBIowBChFVcGRhdGVVc2VyUmVxdWVzdBIlCgR1c2VyGAEgASgLMhIubWVtb3MuYXBpLnYxLlVzZXJCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAhIaCg1hbGxvd19taXNzaW5nGAMgASgIQgPgQQEiUAoRRGVsZXRlVXNlclJlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchISCgVmb3JjZRgCIAEoCEID4EEBItIECglVc2VyU3RhdHMSEQoEbmFtZRgBIAEoCUID4EEIEj4KD21lbW9fdHlwZV9zdGF0cxgDIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuTWVtb1R5cGVTdGF0cxI4Cgl0YWdfY291bnQYBCADKAsyJS5tZW1vcy5hcGkudjEuVXNlclN0YXRzLlRhZ0NvdW50RW50cnkSOwoXbWVtb19jcmVhdGVkX3RpbWVzdGFtcHMYByADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjsKF21lbW9fdXBkYXRlZF90aW1lc3RhbXBzGAggAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIsCgxwaW5uZWRfbWVtb3MYBSADKAlCFvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SGAoQdG90YWxfbWVtb19jb3VudBgGIAEoBRovCg1UYWdDb3VudEVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEaXwoNTWVtb1R5cGVTdGF0cxISCgpsaW5rX2NvdW50GAEgASgFEhIKCmNvZGVfY291bnQYAiABKAUSEgoKdG9kb19jb3VudBgDIAEoBRISCgp1bmRvX2NvdW50GAQgASgFOkXqQUIKFm1lbW9zLmFwaS52MS9Vc2VyU3RhdHMSEnVzZXJzL3t1c2VyfS9zdGF0cyoJdXNlclN0YXRzMgl1c2VyU3RhdHNKBAgCEANSF21lbW9fZGlzcGxheV90aW1lc3RhbXBzImwKE0dldFVzZXJTdGF0c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchITCgZmaWx0ZXIYAiABKAlCA+BBAUoECAMQBFIFc3BhY2VSCnVuYXNzaWduZWQidgoXTGlzdEFsbFVzZXJTdGF0c1JlcXVlc3QSJwoFc3RhdGUYASABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBARITCgZmaWx0ZXIYAiABKAlCA+BBAUoECAMQBEoECAQQBVIFc3BhY2VSCnVuYXNzaWduZWQiQgoYTGlzdEFsbFVzZXJTdGF0c1Jlc3BvbnNlEiYKBXN0YXRzGAEgAygLMhcubWVtb3MuYXBpLnYxLlVzZXJTdGF0cyLQBgoLVXNlclNldHRpbmcSEQoEbmFtZRgBIAEoCUID4EEIEkMKD2dlbmVyYWxfc2V0dGluZxgCIAEoCzIoLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5HZW5lcmFsU2V0dGluZ0gAEkUKEHdlYmhvb2tzX3NldHRpbmcYBSABKAsyKS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcuV2ViaG9va3NTZXR0aW5nSAASPQoMdGFnc19zZXR0aW5nGAYgASgLMiUubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLlRhZ3NTZXR0aW5nSAAaeQoOR2VuZXJhbFNldHRpbmcSEwoGbG9jYWxlGAEgASgJQgPgQQESHAoPbWVtb192aXNpYmlsaXR5GAMgASgJQgPgQQESEgoFdGhlbWUYBCABKAlCA+BBARIgChNzYXZlX21lZGlhX21ldGFkYXRhGAUgASgIQgPgQQEaWwoLVGFnTWV0YWRhdGESMQoQYmFja2dyb3VuZF9jb2xvchgBIAEoCzISLmdvb2dsZS50eXBlLkNvbG9yQgPgQQESGQoMYmx1cl9jb250ZW50GAIgASgIQgPgQQEapQEKC1RhZ3NTZXR0aW5nEkIKBHRhZ3MYASADKAsyLy5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcuVGFnc1NldHRpbmcuVGFnc0VudHJ5QgPgQQEaUgoJVGFnc0VudHJ5EgsKA2tleRgBIAEoCRI0CgV2YWx1ZRgCIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5UYWdNZXRhZGF0YToCOAEaPgoPV2ViaG9va3NTZXR0aW5nEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIj8KA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESDAoIV0VCSE9PS1MQBBIICgRUQUdTEAU6WepBVgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nEh91c2Vycy97dXNlcn0vc2V0dGluZ3Mve3NldHRpbmd9Kgx1c2VyU2V0dGluZ3MyC3VzZXJTZXR0aW5nQgcKBXZhbHVlIkcKFUdldFVzZXJTZXR0aW5nUmVxdWVzdBIuCgRuYW1lGAEgASgJQiDgQQL6QRoKGG1lbW9zLmFwaS52MS9Vc2VyU2V0dGluZyKBAQoYVXBkYXRlVXNlclNldHRpbmdSZXF1ZXN0Ei8KB3NldHRpbmcYASABKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmdCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAiJ1ChdMaXN0VXNlclNldHRpbmdzUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBImAKGExpc3RVc2VyU2V0dGluZ3NSZXNwb25zZRIrCghzZXR0aW5ncxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZxIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAki7gEKCE1lbW9WaWV3EhEKBG5hbWUYASABKAlCA+BBCBISCgV0aXRsZRgCIAEoCUID4EECEhMKBmZpbHRlchgDIAEoCUID4EECEi4KBGljb24YBCABKAsyGy5tZW1vcy5hcGkudjEuTWVtb1ZpZXcuSWNvbkID4EEBGjIKBEljb24SDwoFZW1vamkYASABKAlIABIQCgZsdWNpZGUYAiABKAlIAEIHCgV2YWx1ZTpC6kE/ChVtZW1vcy5hcGkudjEvTWVtb1ZpZXcSGXVzZXJzL3t1c2VyfS92aWV3cy97dmlld30qBXZpZXdzMgR2aWV3IkUKFExpc3RNZW1vVmlld3NSZXF1ZXN0Ei0KBnBhcmVudBgBIAEoCUId4EEC+kEXEhVtZW1vcy5hcGkudjEvTWVtb1ZpZXciQwoVTGlzdE1lbW9WaWV3c1Jlc3BvbnNlEioKCm1lbW9fdmlld3MYASADKAsyFi5tZW1vcy5hcGkudjEuTWVtb1ZpZXciQQoSR2V0TWVtb1ZpZXdSZXF1ZXN0EisKBG5hbWUYASABKAlCHeBBAvpBFwoVbWVtb3MuYXBpLnYxL01lbW9WaWV3IpIBChVDcmVhdGVNZW1vVmlld1JlcXVlc3QSLQoGcGFyZW50GAEgASgJQh3gQQL6QRcSFW1lbW9zLmFwaS52MS9NZW1vVmlldxIuCgltZW1vX3ZpZXcYAiABKAsyFi5tZW1vcy5hcGkudjEuTWVtb1ZpZXdCA+BBAhIaCg12YWxpZGF0ZV9vbmx5GAMgASgIQgPgQQEifQoVVXBkYXRlTWVtb1ZpZXdSZXF1ZXN0Ei4KCW1lbW9fdmlldxgBIAEoCzIWLm1lbW9zLmFwaS52MS5NZW1vVmlld0ID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EEBIkQKFURlbGV0ZU1lbW9WaWV3UmVxdWVzdBIrCgRuYW1lGAEgASgJQh3gQQL6QRcKFW1lbW9zLmFwaS52MS9NZW1vVmlldyLqAQoOTGlua2VkSWRlbnRpdHkSEQoEbmFtZRgBIAEoCUID4EEIEjcKCGlkcF9uYW1lGAIgASgJQiXgQQP6QR8KHW1lbW9zLmFwaS52MS9JZGVudGl0eVByb3ZpZGVyEhcKCmV4dGVybl91aWQYAyABKAlCA+BBAzpz6kFwChttZW1vcy5hcGkudjEvTGlua2VkSWRlbnRpdHkSL3VzZXJzL3t1c2VyfS9saW5rZWRJZGVudGl0aWVzL3tsaW5rZWRfaWRlbnRpdHl9KhBsaW5rZWRJZGVudGl0aWVzMg5saW5rZWRJZGVudGl0eSJIChtMaXN0TGlua2VkSWRlbnRpdGllc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyIlcKHExpc3RMaW5rZWRJZGVudGl0aWVzUmVzcG9uc2USNwoRbGlua2VkX2lkZW50aXRpZXMYASADKAsyHC5tZW1vcy5hcGkudjEuTGlua2VkSWRlbnRpdHkiywEKG0NyZWF0ZUxpbmtlZElkZW50aXR5UmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISNwoIaWRwX25hbWUYAiABKAlCJeBBAvpBHwodbWVtb3MuYXBpLnYxL0lkZW50aXR5UHJvdmlkZXISEQoEY29kZRgDIAEoCUID4EECEhkKDHJlZGlyZWN0X3VyaRgEIAEoCUID4EECEhoKDWNvZGVfdmVyaWZpZXIYBSABKAlCA+BBASJNChhHZXRMaW5rZWRJZGVudGl0eVJlcXVlc3QSMQoEbmFtZRgBIAEoCUIj4EEC+kEdChttZW1vcy5hcGkudjEvTGlua2VkSWRlbnRpdHkiUAobRGVsZXRlTGlua2VkSWRlbnRpdHlSZXF1ZXN0EjEKBG5hbWUYASABKAlCI+BBAvpBHQobbWVtb3MuYXBpLnYxL0xpbmtlZElkZW50aXR5IvICChNQZXJzb25hbEFjY2Vzc1Rva2VuEhEKBG5hbWUYASABKAlCA+BBCBIYCgtkZXNjcmlwdGlvbhgCIAEoCUID4EEBEjMKCmNyZWF0ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSMwoKZXhwaXJlc19hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBARI1CgxsYXN0X3VzZWRfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQM6jAHqQYgBCiBtZW1vcy5hcGkudjEvUGVyc29uYWxBY2Nlc3NUb2tlbhI5dXNlcnMve3VzZXJ9L3BlcnNvbmFsQWNjZXNzVG9rZW5zL3twZXJzb25hbF9hY2Nlc3NfdG9rZW59KhRwZXJzb25hbEFjY2Vzc1Rva2VuczITcGVyc29uYWxBY2Nlc3NUb2tlbiJ9Ch9MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIWCglwYWdlX3NpemUYAiABKAVCA+BBARIXCgpwYWdlX3Rva2VuGAMgASgJQgPgQQEifgogTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2USQQoWcGVyc29uYWxfYWNjZXNzX3Rva2VucxgBIAMoCzIhLm1lbW9zLmFwaS52MS5QZXJzb25hbEFjY2Vzc1Rva2VuEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSKFAQogQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhgKC2Rlc2NyaXB0aW9uGAIgASgJQgPgQQESHAoPZXhwaXJlc19pbl9kYXlzGAMgASgFQgPgQQEidAohQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlc3BvbnNlEkAKFXBlcnNvbmFsX2FjY2Vzc190b2tlbhgBIAEoCzIhLm1lbW9zLmFwaS52MS5QZXJzb25hbEFjY2Vzc1Rva2VuEg0KBXRva2VuGAIgASgJIloKIERlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0EjYKBG5hbWUYASABKAlCKOBBAvpBIgogbWVtb3MuYXBpLnYxL1BlcnNvbmFsQWNjZXNzVG9rZW4iyAIKC1VzZXJXZWJob29rEhEKBG5hbWUYASABKAlCA+BBCBILCgN1cmwYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEjQKC2NyZWF0ZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjQKC3VwZGF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEhsKDnNpZ25pbmdfc2VjcmV0GAYgASgJQgPgQQQSHwoSc2lnbmluZ19zZWNyZXRfc2V0GAcgASgIQgPgQQM6WepBVgoYbWVtb3MuYXBpLnYxL1VzZXJXZWJob29rEh91c2Vycy97dXNlcn0vd2ViaG9va3Mve3dlYmhvb2t9Kgx1c2VyV2ViaG9va3MyC3VzZXJXZWJob29rIksKF0xpc3RVc2VyV2ViaG9va3NSZXF1ZXN0EjAKBnBhcmVudBgBIAEoCUIg4EEC+kEaEhhtZW1vcy5hcGkudjEvVXNlcldlYmhvb2siRwoYTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIn0KGENyZWF0ZVVzZXJXZWJob29rUmVxdWVzdBIwCgZwYXJlbnQYASABKAlCIOBBAvpBGhIYbWVtb3MuYXBpLnYxL1VzZXJXZWJob29rEi8KB3dlYmhvb2sYAiABKAsyGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2tCA+BBAiJ8ChhVcGRhdGVVc2VyV2ViaG9va1JlcXVlc3QSLwoHd2ViaG9vaxgBIAEoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9va0ID4EECEi8KC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFzayJKChhEZWxldGVVc2VyV2ViaG9va1JlcXVlc3QSLgoEbmFtZRgBIAEoCUIg4EEC+kEaChhtZW1vcy5hcGkudjEvVXNlcldlYmhvb2siVAoiR2V0VXNlcldlYmhvb2tTaWduaW5nU2VjcmV0UmVxdWVzdBIuCgRuYW1lGAEgASgJQiDgQQL6QRoKGG1lbW9zLmFwaS52MS9Vc2VyV2ViaG9vayI9CiNHZXRVc2VyV2ViaG9va1NpZ25pbmdTZWNyZXRSZXNwb25zZRIWCg5zaWduaW5nX3NlY3JldBgBIAEoCSLBCgoQVXNlck5vdGlmaWNhdGlvbhIUCgRuYW1lGAEgASgJQgbgQQPgQQgSKQoGc2VuZGVyGAIgASgJQhngQQP6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEiwKC3NlbmRlcl91c2VyGAggASgLMhIubWVtb3MuYXBpLnYxLlVzZXJCA+BBAxI6CgZzdGF0dXMYAyABKA4yJS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5TdGF0dXNCA+BBARI0CgtjcmVhdGVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI2CgR0eXBlGAUgASgOMiMubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uVHlwZUID4EEDEk4KDG1lbW9fY29tbWVudBgGIAEoCzIxLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLk1lbW9Db21tZW50UGF5bG9hZEID4EEDSAASTgoMbWVtb19tZW50aW9uGAcgASgLMjEubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uTWVtb01lbnRpb25QYXlsb2FkQgPgQQNIABJWChBzcGFjZV9pbnZpdGF0aW9uGAkgASgLMjUubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uU3BhY2VJbnZpdGF0aW9uUGF5bG9hZEID4EEDSAAabAoSTWVtb0NvbW1lbnRQYXlsb2FkEgwKBG1lbW8YASABKAkSFAoMcmVsYXRlZF9tZW1vGAIgASgJEhQKDG1lbW9fc25pcHBldBgDIAEoCRIcChRyZWxhdGVkX21lbW9fc25pcHBldBgEIAEoCRpsChJNZW1vTWVudGlvblBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkSFAoMbWVtb19zbmlwcGV0GAMgASgJEhwKFHJlbGF0ZWRfbWVtb19zbmlwcGV0GAQgASgJGq4CChZTcGFjZUludml0YXRpb25QYXlsb2FkEjsKEHNwYWNlX2ludml0YXRpb24YASABKAlCIfpBHgocbWVtb3MuYXBpLnYxL1NwYWNlSW52aXRhdGlvbhIiCgVzcGFjZRgCIAEoCzITLm1lbW9zLmFwaS52MS5TcGFjZRIsCgRyb2xlGAMgASgOMh4ubWVtb3MuYXBpLnYxLlNwYWNlTWVtYmVyLlJvbGUSSgoFc3RhdGUYBCABKA4yOy5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5TcGFjZUludml0YXRpb25QYXlsb2FkLlN0YXRlIjkKBVN0YXRlEhUKEVNUQVRFX1VOU1BFQ0lGSUVEEAASCwoHUEVORElORxABEgwKCEFDQ0VQVEVEEAIiOgoGU3RhdHVzEhYKElNUQVRVU19VTlNQRUNJRklFRBAAEgoKBlVOUkVBRBABEgwKCEFSQ0hJVkVEEAIiVgoEVHlwZRIUChBUWVBFX1VOU1BFQ0lGSUVEEAASEAoMTUVNT19DT01NRU5UEAESEAoMTUVNT19NRU5USU9OEAISFAoQU1BBQ0VfSU5WSVRBVElPThADOnDqQW0KHW1lbW9zLmFwaS52MS9Vc2VyTm90aWZpY2F0aW9uEil1c2Vycy97dXNlcn0vbm90aWZpY2F0aW9ucy97bm90aWZpY2F0aW9ufRoEbmFtZSoNbm90aWZpY2F0aW9uczIMbm90aWZpY2F0aW9uQgkKB3BheWxvYWQijwEKHExpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBARITCgZmaWx0ZXIYBCABKAlCA+BBASJvCh1MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZRI1Cg1ub3RpZmljYXRpb25zGAEgAygLMh4ubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpABCh1VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBI5Cgxub3RpZmljYXRpb24YASABKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbkID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECIlQKHURlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjMKBG5hbWUYASABKAlCJeBBAvpBHwodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24yjyQKC1VzZXJTZXJ2aWNlEmMKCUxpc3RVc2VycxIeLm1lbW9zLmFwaS52MS5MaXN0VXNlcnNSZXF1ZXN0Gh8ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1Jlc3BvbnNlIhWC0+STAg8SDS9hcGkvdjEvdXNlcnMSewoNQmF0Y2hHZXRVc2VycxIiLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVxdWVzdBojLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVzcG9uc2UiIYLT5JMCGzoBKiIWL2FwaS92MS91c2VyczpiYXRjaEdldBJiCgdHZXRVc2VyEhwubWVtb3MuYXBpLnYxLkdldFVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiJdpBBG5hbWWC0+STAhgSFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SbQoKQ3JlYXRlVXNlchIfLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIiraQQx1c2VyLHVzZXJfaWSC0+STAhU6BHVzZXIiDS9hcGkvdjEvdXNlcnMSfwoKVXBkYXRlVXNlchIfLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIjzaQRB1c2VyLHVwZGF0ZV9tYXNrgtPkkwIjOgR1c2VyMhsvYXBpL3YxL3t1c2VyLm5hbWU9dXNlcnMvKn0SbAoKRGVsZXRlVXNlchIfLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIl2kEEbmFtZYLT5JMCGCoWL2FwaS92MS97bmFtZT11c2Vycy8qfRJ+ChBMaXN0QWxsVXNlclN0YXRzEiUubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL3VzZXJzOnN0YXRzEnoKDEdldFVzZXJTdGF0cxIhLm1lbW9zLmFwaS52MS5HZXRVc2VyU3RhdHNSZXF1ZXN0GhcubWVtb3MuYXBpLnYxLlVzZXJTdGF0cyIu2kEEbmFtZYLT5JMCIRIfL2FwaS92MS97bmFtZT11c2Vycy8qfTpnZXRTdGF0cxKCAQoOR2V0VXNlclNldHRpbmcSIy5tZW1vcy5hcGkudjEuR2V0VXNlclNldHRpbmdSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nIjDaQQRuYW1lgtPkkwIjEiEvYXBpL3YxL3tuYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SqAEKEVVwZGF0ZVVzZXJTZXR0aW5nEiYubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyJQ2kETc2V0dGluZyx1cGRhdGVfbWFza4LT5JMCNDoHc2V0dGluZzIpL2FwaS92MS97c2V0dGluZy5uYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SlQEKEExpc3RVc2VyU2V0dGluZ3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9zZXR0aW5ncxKJAQoNTGlzdE1lbW9WaWV3cxIiLm1lbW9zLmFwaS52MS5MaXN0TWVtb1ZpZXdzUmVxdWVzdBojLm1lbW9zLmFwaS52MS5MaXN0TWVtb1ZpZXdzUmVzcG9uc2UiL9pBBnBhcmVudILT5JMCIBIeL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3ZpZXdzEnYKC0dldE1lbW9WaWV3EiAubWVtb3MuYXBpLnYxLkdldE1lbW9WaWV3UmVxdWVzdBoWLm1lbW9zLmFwaS52MS5NZW1vVmlldyIt2kEEbmFtZYLT5JMCIBIeL2FwaS92MS97bmFtZT11c2Vycy8qL3ZpZXdzLyp9EpMBCg5DcmVhdGVNZW1vVmlldxIjLm1lbW9zLmFwaS52MS5DcmVhdGVNZW1vVmlld1JlcXVlc3QaFi5tZW1vcy5hcGkudjEuTWVtb1ZpZXciRNpBEHBhcmVudCxtZW1vX3ZpZXeC0+STAis6CW1lbW9fdmlldyIeL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3ZpZXdzEqIBCg5VcGRhdGVNZW1vVmlldxIjLm1lbW9zLmFwaS52MS5VcGRhdGVNZW1vVmlld1JlcXVlc3QaFi5tZW1vcy5hcGkudjEuTWVtb1ZpZXciU9pBFW1lbW9fdmlldyx1cGRhdGVfbWFza4LT5JMCNToJbWVtb192aWV3MigvYXBpL3YxL3ttZW1vX3ZpZXcubmFtZT11c2Vycy8qL3ZpZXdzLyp9EnwKDkRlbGV0ZU1lbW9WaWV3EiMubWVtb3MuYXBpLnYxLkRlbGV0ZU1lbW9WaWV3UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIt2kEEbmFtZYLT5JMCICoeL2FwaS92MS97bmFtZT11c2Vycy8qL3ZpZXdzLyp9EqkBChRMaXN0TGlua2VkSWRlbnRpdGllcxIpLm1lbW9zLmFwaS52MS5MaXN0TGlua2VkSWRlbnRpdGllc1JlcXVlc3QaKi5tZW1vcy5hcGkudjEuTGlzdExpbmtlZElkZW50aXRpZXNSZXNwb25zZSI62kEGcGFyZW50gtPkkwIrEikvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vbGlua2VkSWRlbnRpdGllcxKnAQoUQ3JlYXRlTGlua2VkSWRlbnRpdHkSKS5tZW1vcy5hcGkudjEuQ3JlYXRlTGlua2VkSWRlbnRpdHlSZXF1ZXN0GhwubWVtb3MuYXBpLnYxLkxpbmtlZElkZW50aXR5IkbaQQ9wYXJlbnQsaWRwX25hbWWC0+STAi46ASoiKS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9saW5rZWRJZGVudGl0aWVzEpMBChFHZXRMaW5rZWRJZGVudGl0eRImLm1lbW9zLmFwaS52MS5HZXRMaW5rZWRJZGVudGl0eVJlcXVlc3QaHC5tZW1vcy5hcGkudjEuTGlua2VkSWRlbnRpdHkiONpBBG5hbWWC0+STAisSKS9hcGkvdjEve25hbWU9dXNlcnMvKi9saW5rZWRJZGVudGl0aWVzLyp9EpMBChREZWxldGVMaW5rZWRJZGVudGl0eRIpLm1lbW9zLmFwaS52MS5EZWxldGVMaW5rZWRJZGVudGl0eVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiONpBBG5hbWWC0+STAisqKS9hcGkvdjEve25hbWU9dXNlcnMvKi9saW5rZWRJZGVudGl0aWVzLyp9ErkBChhMaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnMSLS5tZW1vcy5hcGkudjEuTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVxdWVzdBouLm1lbW9zLmFwaS52MS5MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXNwb25zZSI+2kEGcGFyZW50gtPkkwIvEi0vYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMStgEKGUNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW4SLi5tZW1vcy5hcGkudjEuQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QaLy5tZW1vcy5hcGkudjEuQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlc3BvbnNlIjiC0+STAjI6ASoiLS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9wZXJzb25hbEFjY2Vzc1Rva2VucxKhAQoZRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlbhIuLm1lbW9zLmFwaS52MS5EZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSI82kEEbmFtZYLT5JMCLyotL2FwaS92MS97bmFtZT11c2Vycy8qL3BlcnNvbmFsQWNjZXNzVG9rZW5zLyp9EpUBChBMaXN0VXNlcldlYmhvb2tzEiUubWVtb3MuYXBpLnYxLkxpc3RVc2VyV2ViaG9va3NSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RVc2VyV2ViaG9va3NSZXNwb25zZSIy2kEGcGFyZW50gtPkkwIjEiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vd2ViaG9va3MSmwEKEUNyZWF0ZVVzZXJXZWJob29rEiYubWVtb3MuYXBpLnYxLkNyZWF0ZVVzZXJXZWJob29rUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayJD2kEOcGFyZW50LHdlYmhvb2uC0+STAiw6B3dlYmhvb2siIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS93ZWJob29rcxKoAQoRVXBkYXRlVXNlcldlYmhvb2sSJi5tZW1vcy5hcGkudjEuVXBkYXRlVXNlcldlYmhvb2tSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIlDaQRN3ZWJob29rLHVwZGF0ZV9tYXNrgtPkkwI0Ogd3ZWJob29rMikvYXBpL3YxL3t3ZWJob29rLm5hbWU9dXNlcnMvKi93ZWJob29rcy8qfRKFAQoRRGVsZXRlVXNlcldlYmhvb2sSJi5tZW1vcy5hcGkudjEuRGVsZXRlVXNlcldlYmhvb2tSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjDaQQRuYW1lgtPkkwIjKiEvYXBpL3YxL3tuYW1lPXVzZXJzLyovd2ViaG9va3MvKn0SxQEKG0dldFVzZXJXZWJob29rU2lnbmluZ1NlY3JldBIwLm1lbW9zLmFwaS52MS5HZXRVc2VyV2ViaG9va1NpZ25pbmdTZWNyZXRSZXF1ZXN0GjEubWVtb3MuYXBpLnYxLkdldFVzZXJXZWJob29rU2lnbmluZ1NlY3JldFJlc3BvbnNlIkHaQQRuYW1lgtPkkwI0EjIvYXBpL3YxL3tuYW1lPXVzZXJzLyovd2ViaG9va3MvKn06Z2V0U2lnbmluZ1NlY3JldBKpAQoVTGlzdFVzZXJOb3RpZmljYXRpb25zEioubWVtb3MuYXBpLnYxLkxpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QaKy5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVzcG9uc2UiN9pBBnBhcmVudILT5JMCKBImL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L25vdGlmaWNhdGlvbnMSywEKFlVwZGF0ZVVzZXJOb3RpZmljYXRpb24SKy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QaHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbiJk2kEYbm90aWZpY2F0aW9uLHVwZGF0ZV9tYXNrgtPkkwJDOgxub3RpZmljYXRpb24yMy9hcGkvdjEve25vdGlmaWNhdGlvbi5uYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfRKUAQoWRGVsZXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSI12kEEbmFtZYLT5JMCKComL2FwaS92MS97bmFtZT11c2Vycy8qL25vdGlmaWNhdGlvbnMvKn1CqAEKEGNvbS5tZW1vcy5hcGkudjFCEFVzZXJTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_api_v1_common, file_api_v1_space_service, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp, file_google_type_color]);
 
 /**
  * @generated from message memos.api.v1.User
@@ -903,6 +903,14 @@ export type MemoView = Message<"memos.api.v1.MemoView"> & {
    * @generated from field: string filter = 3;
    */
   filter: string;
+
+  /**
+   * Optional. Display icon. Omit when updating with the "icon" mask to reset
+   * to the default view icon.
+   *
+   * @generated from field: memos.api.v1.MemoView.Icon icon = 4;
+   */
+  icon?: MemoView_Icon | undefined;
 };
 
 /**
@@ -912,6 +920,39 @@ export type MemoView = Message<"memos.api.v1.MemoView"> & {
 export const MemoViewSchema: GenMessage = /*@__PURE__*/
   messageDesc(file_api_v1_user_service, 18);
 
+/**
+ * @generated from message memos.api.v1.MemoView.Icon
+ */
+export type MemoView_Icon = Message<"memos.api.v1.MemoView.Icon"> & {
+  /**
+   * @generated from oneof memos.api.v1.MemoView.Icon.value
+   */
+  value: {
+    /**
+     * A Unicode emoji sequence.
+     *
+     * @generated from field: string emoji = 1;
+     */
+    value: string;
+    case: "emoji";
+  } | {
+    /**
+     * A canonical Lucide icon name, such as "leaf".
+     *
+     * @generated from field: string lucide = 2;
+     */
+    value: string;
+    case: "lucide";
+  } | { case: undefined; value?: undefined };
+};
+
+/**
+ * Describes the message memos.api.v1.MemoView.Icon.
+ * Use `create(MemoView_IconSchema)` to create a new message.
+ */
+export const MemoView_IconSchema: GenMessage = /*@__PURE__*/
+  messageDesc(file_api_v1_user_service, 18, 0);
+
 /**
  * @generated from message memos.api.v1.ListMemoViewsRequest
  */
diff --git a/web/tests/memo-view-icons.test.tsx b/web/tests/memo-view-icons.test.tsx
new file mode 100644
index 00000000..35de5149
--- /dev/null
+++ b/web/tests/memo-view-icons.test.tsx
@@ -0,0 +1,112 @@
+import { create } from "@bufbuild/protobuf";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { MemoryRouter } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import MemoViewIcon from "@/components/MemoViewIcon";
+import MemoViews from "@/pages/MemoViews";
+import { MemoView_IconSchema, MemoViewSchema } from "@/types/proto/api/v1/user_service_pb";
+
+const mocks = vi.hoisted(() => ({ create: vi.fn(), update: vi.fn() }));
+vi.mock("@/connect", () => ({ userServiceClient: { createMemoView: mocks.create, updateMemoView: mocks.update } }));
+vi.mock("@/hooks/useCurrentUser", () => ({ default: () => ({ name: "users/steven" }) }));
+vi.mock("@/hooks/useUserQueries", () => ({ useMemoViews: () => ({ data: [] }), userKeys: { memoViews: () => ["views"] } }));
+vi.mock("@/contexts/MemoFilterContext", () => ({ useMemoFilterContext: () => ({ setMemoView: vi.fn() }) }));
+vi.mock("@/utils/i18n", () => ({ useTranslate: () => (key: string) => key }));
+
+const emoji = create(MemoView_IconSchema, { value: { case: "emoji", value: "🌱" } });
+const leaf = create(MemoView_IconSchema, { value: { case: "lucide", value: "leaf" } });
+const existingView = create(MemoViewSchema, { name: "users/steven/views/garden", title: "Garden", filter: "pinned", icon: emoji });
+const iconMask = expect.objectContaining({ paths: ["title", "filter", "icon"] });
+
+function renderPage(edit = false) {
+  return render(
+    
+      
+        
+      
+    ,
+  );
+}
+
+beforeEach(() => {
+  vi.clearAllMocks();
+  mocks.create.mockResolvedValue({});
+  mocks.update.mockResolvedValue({});
+});
+
+describe("memo view icons", () => {
+  it("renders emoji and symbols with a view-specific fallback", () => {
+    const view = render();
+    expect(view.container.querySelector(".lucide-parentheses")).not.toBeNull();
+    view.rerender();
+    expect(view.container).toHaveTextContent("🌱");
+    view.rerender();
+    expect(view.container.querySelector(".lucide-leaf")).not.toBeNull();
+    view.rerender();
+    expect(view.container.querySelector(".lucide-parentheses")).not.toBeNull();
+  });
+
+  it("creates and validates a view with a selected icon", async () => {
+    renderPage();
+    fireEvent.change(screen.getByLabelText("common.title"), { target: { value: "Garden" } });
+    fireEvent.change(screen.getByLabelText("common.filter"), { target: { value: "pinned" } });
+    fireEvent.click(screen.getByRole("button", { name: "setting.memo-view.change-icon" }));
+    const search = await screen.findByRole("textbox", { name: "space.icon.search-icons" });
+    fireEvent.change(search, { target: { value: "leaf" } });
+    fireEvent.keyDown(search, { key: "Enter" });
+    fireEvent.click(screen.getByRole("button", { name: "Validate" }));
+    await waitFor(() =>
+      expect(mocks.create).toHaveBeenCalledWith(
+        expect.objectContaining({
+          validateOnly: true,
+          memoView: expect.objectContaining({ icon: leaf }),
+        }),
+      ),
+    );
+    await waitFor(() => expect(screen.getByRole("button", { name: "common.save" })).toBeEnabled());
+    fireEvent.click(screen.getByRole("button", { name: "common.save" }));
+    await waitFor(() =>
+      expect(mocks.create).toHaveBeenLastCalledWith({
+        parent: "users/steven",
+        memoView: { name: "", title: "Garden", filter: "pinned", icon: leaf },
+      }),
+    );
+  });
+
+  it("preserves the icon when editing from the sidebar and applying an example", async () => {
+    renderPage(true);
+    expect(screen.getByRole("button", { name: "setting.memo-view.change-icon" })).toHaveTextContent("🌱");
+    fireEvent.click(screen.getByRole("button", { name: /Recent notes/ }));
+    fireEvent.click(screen.getByRole("button", { name: "common.save" }));
+    await waitFor(() =>
+      expect(mocks.update).toHaveBeenCalledWith(
+        expect.objectContaining({
+          memoView: expect.objectContaining({
+            name: existingView.name,
+            title: "Garden",
+            icon: emoji,
+            filter: 'created_ts >= now - duration("1h")',
+          }),
+          updateMask: iconMask,
+        }),
+      ),
+    );
+  });
+
+  it("resets an existing icon using the icon update mask", async () => {
+    renderPage(true);
+    fireEvent.click(screen.getByRole("button", { name: "setting.memo-view.change-icon" }));
+    fireEvent.click(await screen.findByRole("button", { name: "space.icon.reset" }));
+    expect(screen.getByRole("button", { name: "setting.memo-view.change-icon" }).querySelector(".lucide-parentheses")).not.toBeNull();
+    fireEvent.click(screen.getByRole("button", { name: "common.save" }));
+    await waitFor(() =>
+      expect(mocks.update).toHaveBeenCalledWith(
+        expect.objectContaining({
+          memoView: expect.objectContaining({ icon: undefined }),
+          updateMask: iconMask,
+        }),
+      ),
+    );
+  });
+});