Unverified Commit c268551a authored by boojack's avatar boojack Committed by GitHub

feat(memos): choose created or updated time for memos (#5894)

parent 3949a252
......@@ -158,8 +158,8 @@ message InstanceSetting {
// Memo-related instance settings and policies.
message MemoRelatedSetting {
// display_with_update_time orders and displays memo with update time.
bool display_with_update_time = 2;
reserved 2;
reserved "display_with_update_time";
// content_length_limit is the limit of content length. Unit is byte.
int32 content_length_limit = 3;
// enable_double_click_edit enables editing on double click.
......
......@@ -204,8 +204,8 @@ message Memo {
// If not set on creation, the server will set it to the current time.
google.protobuf.Timestamp update_time = 5 [(google.api.field_behavior) = OPTIONAL];
// The display timestamp of the memo.
google.protobuf.Timestamp display_time = 6 [(google.api.field_behavior) = OPTIONAL];
reserved 6;
reserved "display_time";
// Required. The content of the memo in Markdown format.
string content = 7 [(google.api.field_behavior) = REQUIRED];
......@@ -291,10 +291,10 @@ message ListMemosRequest {
State state = 3 [(google.api.field_behavior) = OPTIONAL];
// Optional. The order to sort results by.
// Default to "display_time desc".
// Default to "create_time desc".
// Supports comma-separated list of fields following AIP-132.
// Example: "pinned desc, display_time desc" or "create_time asc"
// Supported fields: pinned, display_time, create_time, update_time, name
// Example: "pinned desc, create_time desc" or "update_time asc"
// Supported fields: pinned, create_time, update_time, name
string order_by = 4 [(google.api.field_behavior) = OPTIONAL];
// Optional. Filter to apply to the list results.
......
......@@ -354,8 +354,8 @@ message UserStats {
// Format: users/{user}
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
// The timestamps when the memos were displayed.
repeated google.protobuf.Timestamp memo_display_timestamps = 2;
reserved 2;
reserved "memo_display_timestamps";
// The stats of memo types.
MemoTypeStats memo_type_stats = 3;
......@@ -363,6 +363,9 @@ message UserStats {
// The count of tags.
map<string, int32> tag_count = 4;
// The creation timestamps of the user's memos.
repeated google.protobuf.Timestamp memo_created_timestamps = 7;
// The pinned memos of the user.
repeated string pinned_memos = 5;
......
......@@ -756,8 +756,6 @@ func (x *InstanceSetting_StorageSetting) GetS3Config() *InstanceSetting_StorageS
// Memo-related instance settings and policies.
type InstanceSetting_MemoRelatedSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// display_with_update_time orders and displays memo with update time.
DisplayWithUpdateTime bool `protobuf:"varint,2,opt,name=display_with_update_time,json=displayWithUpdateTime,proto3" json:"display_with_update_time,omitempty"`
// content_length_limit is the limit of content length. Unit is byte.
ContentLengthLimit int32 `protobuf:"varint,3,opt,name=content_length_limit,json=contentLengthLimit,proto3" json:"content_length_limit,omitempty"`
// enable_double_click_edit enables editing on double click.
......@@ -798,13 +796,6 @@ func (*InstanceSetting_MemoRelatedSetting) Descriptor() ([]byte, []int) {
return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 2}
}
func (x *InstanceSetting_MemoRelatedSetting) GetDisplayWithUpdateTime() bool {
if x != nil {
return x.DisplayWithUpdateTime
}
return false
}
func (x *InstanceSetting_MemoRelatedSetting) GetContentLengthLimit() int32 {
if x != nil {
return x.ContentLengthLimit
......@@ -1392,7 +1383,7 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
"\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" +
"\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\x12(\n" +
"\x05admin\x18\a \x01(\v2\x12.memos.api.v1.UserR\x05admin\"\x1b\n" +
"\x19GetInstanceProfileRequest\"\xff\x19\n" +
"\x19GetInstanceProfileRequest\"\xe6\x19\n" +
"\x0fInstanceSetting\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" +
"\x0fgeneral_setting\x18\x02 \x01(\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12W\n" +
......@@ -1431,12 +1422,11 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
"\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" +
"\bDATABASE\x10\x01\x12\t\n" +
"\x05LOCAL\x10\x02\x12\x06\n" +
"\x02S3\x10\x03\x1a\xd6\x01\n" +
"\x12MemoRelatedSetting\x127\n" +
"\x18display_with_update_time\x18\x02 \x01(\bR\x15displayWithUpdateTime\x120\n" +
"\x02S3\x10\x03\x1a\xbd\x01\n" +
"\x12MemoRelatedSetting\x120\n" +
"\x14content_length_limit\x18\x03 \x01(\x05R\x12contentLengthLimit\x127\n" +
"\x18enable_double_click_edit\x18\x04 \x01(\bR\x15enableDoubleClickEdit\x12\x1c\n" +
"\treactions\x18\a \x03(\tR\treactions\x1ao\n" +
"\treactions\x18\a \x03(\tR\treactionsJ\x04\b\x02\x10\x03R\x18display_with_update_time\x1ao\n" +
"\vTagMetadata\x12=\n" +
"\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorR\x0fbackgroundColor\x12!\n" +
"\fblur_content\x18\x02 \x01(\bR\vblurContent\x1a\xba\x01\n" +
......
This diff is collapsed.
......@@ -849,12 +849,12 @@ type UserStats struct {
// The resource name of the user whose stats these are.
// Format: users/{user}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// The timestamps when the memos were displayed.
MemoDisplayTimestamps []*timestamppb.Timestamp `protobuf:"bytes,2,rep,name=memo_display_timestamps,json=memoDisplayTimestamps,proto3" json:"memo_display_timestamps,omitempty"`
// The stats of memo types.
MemoTypeStats *UserStats_MemoTypeStats `protobuf:"bytes,3,opt,name=memo_type_stats,json=memoTypeStats,proto3" json:"memo_type_stats,omitempty"`
// The count of tags.
TagCount map[string]int32 `protobuf:"bytes,4,rep,name=tag_count,json=tagCount,proto3" json:"tag_count,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
// The creation timestamps of the user's memos.
MemoCreatedTimestamps []*timestamppb.Timestamp `protobuf:"bytes,7,rep,name=memo_created_timestamps,json=memoCreatedTimestamps,proto3" json:"memo_created_timestamps,omitempty"`
// The pinned memos of the user.
PinnedMemos []string `protobuf:"bytes,5,rep,name=pinned_memos,json=pinnedMemos,proto3" json:"pinned_memos,omitempty"`
// Total memo count.
......@@ -900,23 +900,23 @@ func (x *UserStats) GetName() string {
return ""
}
func (x *UserStats) GetMemoDisplayTimestamps() []*timestamppb.Timestamp {
func (x *UserStats) GetMemoTypeStats() *UserStats_MemoTypeStats {
if x != nil {
return x.MemoDisplayTimestamps
return x.MemoTypeStats
}
return nil
}
func (x *UserStats) GetMemoTypeStats() *UserStats_MemoTypeStats {
func (x *UserStats) GetTagCount() map[string]int32 {
if x != nil {
return x.MemoTypeStats
return x.TagCount
}
return nil
}
func (x *UserStats) GetTagCount() map[string]int32 {
func (x *UserStats) GetMemoCreatedTimestamps() []*timestamppb.Timestamp {
if x != nil {
return x.TagCount
return x.MemoCreatedTimestamps
}
return nil
}
......@@ -3172,12 +3172,12 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\x11DeleteUserRequest\x12-\n" +
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x04name\x12\x19\n" +
"\x05force\x18\x02 \x01(\bB\x03\xe0A\x01R\x05force\"\xe4\x04\n" +
"\x05force\x18\x02 \x01(\bB\x03\xe0A\x01R\x05force\"\x83\x05\n" +
"\tUserStats\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12R\n" +
"\x17memo_display_timestamps\x18\x02 \x03(\v2\x1a.google.protobuf.TimestampR\x15memoDisplayTimestamps\x12M\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12M\n" +
"\x0fmemo_type_stats\x18\x03 \x01(\v2%.memos.api.v1.UserStats.MemoTypeStatsR\rmemoTypeStats\x12B\n" +
"\ttag_count\x18\x04 \x03(\v2%.memos.api.v1.UserStats.TagCountEntryR\btagCount\x12!\n" +
"\ttag_count\x18\x04 \x03(\v2%.memos.api.v1.UserStats.TagCountEntryR\btagCount\x12R\n" +
"\x17memo_created_timestamps\x18\a \x03(\v2\x1a.google.protobuf.TimestampR\x15memoCreatedTimestamps\x12!\n" +
"\fpinned_memos\x18\x05 \x03(\tR\vpinnedMemos\x12(\n" +
"\x10total_memo_count\x18\x06 \x01(\x05R\x0etotalMemoCount\x1a\x8b\x01\n" +
"\rMemoTypeStats\x12\x1d\n" +
......@@ -3192,7 +3192,7 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\rTagCountEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01:?\xeaA<\n" +
"\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStats\"D\n" +
"\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStatsJ\x04\b\x02\x10\x03R\x17memo_display_timestamps\"D\n" +
"\x13GetUserStatsRequest\x12-\n" +
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x04name\"\x19\n" +
......@@ -3475,9 +3475,9 @@ var file_api_v1_user_service_proto_depIdxs = []int32{
4, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
4, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
53, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
52, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
45, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
46, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
45, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
46, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
52, // 12: memos.api.v1.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp
13, // 13: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
47, // 14: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
48, // 15: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
......
......@@ -579,10 +579,10 @@ paths:
in: query
description: |-
Optional. The order to sort results by.
Default to "display_time desc".
Default to "create_time desc".
Supports comma-separated list of fields following AIP-132.
Example: "pinned desc, display_time desc" or "create_time asc"
Supported fields: pinned, display_time, create_time, update_time, name
Example: "pinned desc, create_time desc" or "update_time asc"
Supported fields: pinned, create_time, update_time, name
schema:
type: string
- name: filter
......@@ -2626,9 +2626,6 @@ components:
InstanceSetting_MemoRelatedSetting:
type: object
properties:
displayWithUpdateTime:
type: boolean
description: display_with_update_time orders and displays memo with update time.
contentLengthLimit:
type: integer
description: content_length_limit is the limit of content length. Unit is byte.
......@@ -2959,10 +2956,6 @@ components:
The last update timestamp.
If not set on creation, the server will set it to the current time.
format: date-time
displayTime:
type: string
description: The display timestamp of the memo.
format: date-time
content:
type: string
description: Required. The content of the memo in Markdown format.
......@@ -3637,12 +3630,6 @@ components:
description: |-
The resource name of the user whose stats these are.
Format: users/{user}
memoDisplayTimestamps:
type: array
items:
type: string
format: date-time
description: The timestamps when the memos were displayed.
memoTypeStats:
allOf:
- $ref: '#/components/schemas/UserStats_MemoTypeStats'
......@@ -3653,6 +3640,12 @@ components:
type: integer
format: int32
description: The count of tags.
memoCreatedTimestamps:
type: array
items:
type: string
format: date-time
description: The creation timestamps of the user's memos.
pinnedMemos:
type: array
items:
......
......@@ -751,8 +751,6 @@ func (x *StorageS3Config) GetUsePathStyle() bool {
type InstanceMemoRelatedSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// display_with_update_time orders and displays memo with update time.
DisplayWithUpdateTime bool `protobuf:"varint,2,opt,name=display_with_update_time,json=displayWithUpdateTime,proto3" json:"display_with_update_time,omitempty"`
// content_length_limit is the limit of content length. Unit is byte.
ContentLengthLimit int32 `protobuf:"varint,3,opt,name=content_length_limit,json=contentLengthLimit,proto3" json:"content_length_limit,omitempty"`
// enable_double_click_edit enables editing on double click.
......@@ -793,13 +791,6 @@ func (*InstanceMemoRelatedSetting) Descriptor() ([]byte, []int) {
return file_store_instance_setting_proto_rawDescGZIP(), []int{6}
}
func (x *InstanceMemoRelatedSetting) GetDisplayWithUpdateTime() bool {
if x != nil {
return x.DisplayWithUpdateTime
}
return false
}
func (x *InstanceMemoRelatedSetting) GetContentLengthLimit() int32 {
if x != nil {
return x.ContentLengthLimit
......@@ -1255,12 +1246,11 @@ const file_store_instance_setting_proto_rawDesc = "" +
"\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x16\n" +
"\x06region\x18\x04 \x01(\tR\x06region\x12\x16\n" +
"\x06bucket\x18\x05 \x01(\tR\x06bucket\x12$\n" +
"\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\"\xde\x01\n" +
"\x1aInstanceMemoRelatedSetting\x127\n" +
"\x18display_with_update_time\x18\x02 \x01(\bR\x15displayWithUpdateTime\x120\n" +
"\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\"\xc5\x01\n" +
"\x1aInstanceMemoRelatedSetting\x120\n" +
"\x14content_length_limit\x18\x03 \x01(\x05R\x12contentLengthLimit\x127\n" +
"\x18enable_double_click_edit\x18\x04 \x01(\bR\x15enableDoubleClickEdit\x12\x1c\n" +
"\treactions\x18\a \x03(\tR\treactions\"w\n" +
"\treactions\x18\a \x03(\tR\treactionsJ\x04\b\x02\x10\x03R\x18display_with_update_time\"w\n" +
"\x13InstanceTagMetadata\x12=\n" +
"\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorR\x0fbackgroundColor\x12!\n" +
"\fblur_content\x18\x02 \x01(\bR\vblurContent\"\xb0\x01\n" +
......
......@@ -103,8 +103,8 @@ message StorageS3Config {
}
message InstanceMemoRelatedSetting {
// display_with_update_time orders and displays memo with update time.
bool display_with_update_time = 2;
reserved 2;
reserved "display_with_update_time";
// content_length_limit is the limit of content length. Unit is byte.
int32 content_length_limit = 3;
// enable_double_click_edit enables editing on double click.
......
......@@ -325,7 +325,6 @@ func convertInstanceMemoRelatedSettingFromStore(setting *storepb.InstanceMemoRel
return nil
}
return &v1pb.InstanceSetting_MemoRelatedSetting{
DisplayWithUpdateTime: setting.DisplayWithUpdateTime,
ContentLengthLimit: setting.ContentLengthLimit,
EnableDoubleClickEdit: setting.EnableDoubleClickEdit,
Reactions: setting.Reactions,
......@@ -337,7 +336,6 @@ func convertInstanceMemoRelatedSettingToStore(setting *v1pb.InstanceSetting_Memo
return nil
}
return &storepb.InstanceMemoRelatedSetting{
DisplayWithUpdateTime: setting.DisplayWithUpdateTime,
ContentLengthLimit: setting.ContentLengthLimit,
EnableDoubleClickEdit: setting.EnableDoubleClickEdit,
Reactions: setting.Reactions,
......
......@@ -54,25 +54,7 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR
Visibility: convertVisibilityToStore(request.Memo.Visibility),
}
instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting")
}
// Handle display_time first: if provided, use it to set the appropriate timestamp
// based on the instance setting (similar to UpdateMemo logic)
// Note: explicit create_time/update_time below will override this if provided
if request.Memo.DisplayTime != nil && request.Memo.DisplayTime.IsValid() {
displayTs := request.Memo.DisplayTime.AsTime().Unix()
if instanceMemoRelatedSetting.DisplayWithUpdateTime {
create.UpdatedTs = displayTs
} else {
create.CreatedTs = displayTs
}
}
// Set custom timestamps if provided in the request
// These take precedence over display_time
// Set custom timestamps if provided in the request.
if request.Memo.CreateTime != nil && request.Memo.CreateTime.IsValid() {
createdTs := request.Memo.CreateTime.AsTime().Unix()
create.CreatedTs = createdTs
......@@ -196,7 +178,7 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
return nil, status.Errorf(codes.InvalidArgument, "invalid order_by: %v", err)
}
} else {
// Default ordering by display_time desc
// Default ordering by create_time desc.
memoFind.OrderByTimeAsc = false
}
......@@ -218,14 +200,6 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
}
}
instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting")
}
if instanceMemoRelatedSetting.DisplayWithUpdateTime {
memoFind.OrderByUpdatedTs = true
}
var limit, offset int
if request.PageToken != "" {
var pageToken v1pb.PageToken
......@@ -312,15 +286,13 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memo creators: %v", err)
}
conversionOptions := memoConversionOptions{displayWithUpdateTime: instanceMemoRelatedSetting.DisplayWithUpdateTime}
for _, memo := range memos {
memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
reactions := reactionMap[memoName]
attachments := attachmentMap[memo.ID]
relations := relationMap[memo.ID]
memoMessage, err := s.convertMemoFromStoreWithCreatorsAndOptions(ctx, memo, reactions, attachments, relations, creatorMap, conversionOptions)
memoMessage, err := s.convertMemoFromStoreWithCreators(ctx, memo, reactions, attachments, relations, creatorMap)
if err != nil {
if stderrors.Is(err, errMemoCreatorNotFound) {
slog.Warn("Skipping memo with missing creator",
......@@ -479,16 +451,7 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
}
update.UpdatedTs = &updatedTs
} else if path == "display_time" {
displayTs := request.Memo.DisplayTime.AsTime().Unix()
memoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting")
}
if memoRelatedSetting.DisplayWithUpdateTime {
update.UpdatedTs = &displayTs
} else {
update.CreatedTs = &displayTs
}
return nil, status.Errorf(codes.InvalidArgument, "display_time is not supported")
} else if path == "location" {
payload := memo.Payload
payload.Location = convertLocationToStore(request.Memo.Location)
......@@ -817,12 +780,6 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memo creators: %v", err)
}
instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance memo related setting")
}
conversionOptions := memoConversionOptions{displayWithUpdateTime: instanceMemoRelatedSetting.DisplayWithUpdateTime}
var memosResponse []*v1pb.Memo
for _, m := range memos {
memoName := memoIDToNameMap[m.ID]
......@@ -830,7 +787,7 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
attachments := attachmentMap[m.ID]
relations := relationMap[m.ID]
memoMessage, err := s.convertMemoFromStoreWithCreatorsAndOptions(ctx, m, reactions, attachments, relations, creatorMap, conversionOptions)
memoMessage, err := s.convertMemoFromStoreWithCreators(ctx, m, reactions, attachments, relations, creatorMap)
if err != nil {
if stderrors.Is(err, errMemoCreatorNotFound) {
slog.Warn("Skipping memo comment with missing creator",
......@@ -939,7 +896,7 @@ func (s *APIV1Service) getMemoContentSnippet(content string) (string, error) {
// parseMemoOrderBy parses the order_by field and sets the appropriate ordering in memoFind.
// Follows AIP-132: supports comma-separated list of fields with optional "desc" suffix.
// Example: "pinned desc, display_time desc" or "create_time asc".
// Example: "pinned desc, create_time desc" or "update_time asc".
func (*APIV1Service) parseMemoOrderBy(orderBy string, memoFind *store.FindMemo) error {
if strings.TrimSpace(orderBy) == "" {
return errors.New("empty order_by")
......@@ -950,6 +907,7 @@ func (*APIV1Service) parseMemoOrderBy(orderBy string, memoFind *store.FindMemo)
// Track if we've seen pinned field.
hasPinned := false
hasExplicitTimeField := false
for _, field := range fields {
parts := strings.Fields(strings.TrimSpace(field))
......@@ -971,16 +929,21 @@ func (*APIV1Service) parseMemoOrderBy(orderBy string, memoFind *store.FindMemo)
hasPinned = true
memoFind.OrderByPinned = true
// Note: pinned is always DESC (true first) regardless of direction specified.
case "display_time", "create_time", "name":
case "create_time", "name":
// Only set if this is the first time field we encounter.
if !memoFind.OrderByUpdatedTs {
if !hasExplicitTimeField {
memoFind.OrderByTimeAsc = fieldDirection == "asc"
}
hasExplicitTimeField = true
case "update_time":
memoFind.OrderByUpdatedTs = true
memoFind.OrderByTimeAsc = fieldDirection == "asc"
// Only set if this is the first time field we encounter.
if !hasExplicitTimeField {
memoFind.OrderByUpdatedTs = true
memoFind.OrderByTimeAsc = fieldDirection == "asc"
}
hasExplicitTimeField = true
default:
return errors.Errorf("unsupported order field: %s, supported fields are: pinned, display_time, create_time, update_time, name", fieldName)
return errors.Errorf("unsupported order field: %s, supported fields are: pinned, create_time, update_time, name", fieldName)
}
}
......
......@@ -20,10 +20,6 @@ var (
errReactionCreatorNotFound = stderrors.New("reaction creator not found")
)
type memoConversionOptions struct {
displayWithUpdateTime bool
}
func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation) (*v1pb.Memo, error) {
creatorMap, err := s.listUsersByID(ctx, []int32{memo.CreatorID})
if err != nil {
......@@ -33,42 +29,20 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
}
func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation, creatorMap map[int32]*store.User) (*v1pb.Memo, error) {
instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get instance memo related setting")
}
return s.convertMemoFromStoreWithCreatorsAndOptions(
ctx,
memo,
reactions,
attachments,
relations,
creatorMap,
memoConversionOptions{displayWithUpdateTime: instanceMemoRelatedSetting.DisplayWithUpdateTime},
)
}
func (s *APIV1Service) convertMemoFromStoreWithCreatorsAndOptions(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation, creatorMap map[int32]*store.User, options memoConversionOptions) (*v1pb.Memo, error) {
displayTs := memo.CreatedTs
if options.displayWithUpdateTime {
displayTs = memo.UpdatedTs
}
name := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
creator := creatorMap[memo.CreatorID]
if creator == nil {
return nil, errMemoCreatorNotFound
}
memoMessage := &v1pb.Memo{
Name: name,
State: convertStateFromStore(memo.RowStatus),
Creator: BuildUserName(creator.Username),
CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
Content: memo.Content,
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
Name: name,
State: convertStateFromStore(memo.RowStatus),
Creator: BuildUserName(creator.Username),
CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
Content: memo.Content,
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
}
if memo.Payload != nil {
memoMessage.Tags = memo.Payload.Tags
......
......@@ -254,6 +254,120 @@ func TestListMemos(t *testing.T) {
require.Equal(t, "👍", userTwoReaction.ReactionType)
}
func TestListMemosTimeOrderBy(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateHostUser(ctx, "time-order-user")
require.NoError(t, err)
userCtx := ts.CreateUserContext(ctx, user.ID)
memoEarlyCreateLateUpdate, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "early create late update",
Visibility: apiv1.Visibility_PRIVATE,
CreateTime: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
UpdateTime: timestamppb.New(time.Date(2020, 1, 3, 0, 0, 0, 0, time.UTC)),
},
})
require.NoError(t, err)
memoMiddleCreateEarlyUpdate, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "middle create early update",
Visibility: apiv1.Visibility_PRIVATE,
CreateTime: timestamppb.New(time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)),
UpdateTime: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
})
require.NoError(t, err)
memoLateCreateMiddleUpdate, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "late create middle update",
Visibility: apiv1.Visibility_PRIVATE,
CreateTime: timestamppb.New(time.Date(2020, 1, 3, 0, 0, 0, 0, time.UTC)),
UpdateTime: timestamppb.New(time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)),
},
})
require.NoError(t, err)
tests := []struct {
name string
orderBy string
wantNames []string
}{
{
name: "default create time",
orderBy: "",
wantNames: []string{
memoLateCreateMiddleUpdate.Name,
memoMiddleCreateEarlyUpdate.Name,
memoEarlyCreateLateUpdate.Name,
},
},
{
name: "explicit create time",
orderBy: "create_time desc",
wantNames: []string{
memoLateCreateMiddleUpdate.Name,
memoMiddleCreateEarlyUpdate.Name,
memoEarlyCreateLateUpdate.Name,
},
},
{
name: "explicit update time",
orderBy: "update_time desc",
wantNames: []string{
memoEarlyCreateLateUpdate.Name,
memoLateCreateMiddleUpdate.Name,
memoMiddleCreateEarlyUpdate.Name,
},
},
{
name: "pinned with explicit create time",
orderBy: "pinned desc, create_time desc",
wantNames: []string{
memoLateCreateMiddleUpdate.Name,
memoMiddleCreateEarlyUpdate.Name,
memoEarlyCreateLateUpdate.Name,
},
},
{
name: "explicit create time ascending",
orderBy: "create_time asc",
wantNames: []string{
memoEarlyCreateLateUpdate.Name,
memoMiddleCreateEarlyUpdate.Name,
memoLateCreateMiddleUpdate.Name,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resp, err := ts.Service.ListMemos(userCtx, &apiv1.ListMemosRequest{
PageSize: 10,
OrderBy: test.orderBy,
})
require.NoError(t, err)
require.Len(t, resp.Memos, len(test.wantNames))
gotNames := make([]string, 0, len(resp.Memos))
for _, memo := range resp.Memos {
gotNames = append(gotNames, memo.Name)
}
require.Equal(t, test.wantNames, gotNames)
})
}
_, err = ts.Service.ListMemos(userCtx, &apiv1.ListMemosRequest{
PageSize: 10,
OrderBy: "display_time desc",
})
require.Error(t, err)
}
func TestListMemosSkipsReactionsWithMissingCreators(t *testing.T) {
ctx := context.Background()
......@@ -431,7 +545,6 @@ func TestCreateMemoWithCustomTimestamps(t *testing.T) {
// Define custom timestamps (January 1, 2020)
customCreateTime := time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC)
customUpdateTime := time.Date(2020, 1, 2, 12, 0, 0, 0, time.UTC)
customDisplayTime := time.Date(2020, 1, 3, 12, 0, 0, 0, time.UTC)
// Test 1: Create a memo with custom create_time
memoWithCreateTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
......@@ -457,41 +570,21 @@ func TestCreateMemoWithCustomTimestamps(t *testing.T) {
require.NotNil(t, memoWithUpdateTime)
require.Equal(t, customUpdateTime.Unix(), memoWithUpdateTime.UpdateTime.AsTime().Unix(), "update_time should match the custom timestamp")
// Test 3: Create a memo with custom display_time
// Note: display_time is computed from either created_ts or updated_ts based on instance setting
// Since DisplayWithUpdateTime defaults to false, display_time maps to created_ts
memoWithDisplayTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "This memo has a custom display time",
Visibility: apiv1.Visibility_PRIVATE,
DisplayTime: timestamppb.New(customDisplayTime),
},
})
require.NoError(t, err)
require.NotNil(t, memoWithDisplayTime)
// Since DisplayWithUpdateTime is false by default, display_time sets created_ts
require.Equal(t, customDisplayTime.Unix(), memoWithDisplayTime.DisplayTime.AsTime().Unix(), "display_time should match the custom timestamp")
require.Equal(t, customDisplayTime.Unix(), memoWithDisplayTime.CreateTime.AsTime().Unix(), "create_time should also match since display_time maps to created_ts")
// Test 4: Create a memo with all custom timestamps
// When both display_time and create_time are provided, create_time takes precedence
// Test 3: Create a memo with all custom timestamps
memoWithAllTimestamps, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "This memo has all custom timestamps",
Visibility: apiv1.Visibility_PRIVATE,
CreateTime: timestamppb.New(customCreateTime),
UpdateTime: timestamppb.New(customUpdateTime),
DisplayTime: timestamppb.New(customDisplayTime),
Content: "This memo has all custom timestamps",
Visibility: apiv1.Visibility_PRIVATE,
CreateTime: timestamppb.New(customCreateTime),
UpdateTime: timestamppb.New(customUpdateTime),
},
})
require.NoError(t, err)
require.NotNil(t, memoWithAllTimestamps)
require.Equal(t, customCreateTime.Unix(), memoWithAllTimestamps.CreateTime.AsTime().Unix(), "create_time should match the custom timestamp")
require.Equal(t, customUpdateTime.Unix(), memoWithAllTimestamps.UpdateTime.AsTime().Unix(), "update_time should match the custom timestamp")
// display_time is computed from created_ts when DisplayWithUpdateTime is false
require.Equal(t, customCreateTime.Unix(), memoWithAllTimestamps.DisplayTime.AsTime().Unix(), "display_time should be derived from create_time")
// Test 5: Create a comment (memo relation) with custom timestamps
// Test 4: Create a comment (memo relation) with custom timestamps
parentMemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "This is the parent memo",
......@@ -514,7 +607,7 @@ func TestCreateMemoWithCustomTimestamps(t *testing.T) {
require.NotNil(t, comment)
require.Equal(t, customCommentCreateTime.Unix(), comment.CreateTime.AsTime().Unix(), "comment create_time should match the custom timestamp")
// Test 6: Verify that memos without custom timestamps still get auto-generated ones
// Test 5: Verify that memos without custom timestamps still get auto-generated ones
memoWithoutTimestamps, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "This memo has auto-generated timestamps",
......
......@@ -5,7 +5,6 @@ import (
"fmt"
"time"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
......@@ -55,11 +54,6 @@ func (s *APIV1Service) listUsernamesByID(ctx context.Context, userIDs []int32) (
}
func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUserStatsRequest) (*v1pb.ListAllUserStatsResponse, error) {
instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get instance memo related setting")
}
normalStatus := store.Normal
memoFind := &store.FindMemo{
// Exclude comments by default.
......@@ -105,7 +99,7 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{
Name: "",
TagCount: make(map[string]int32),
MemoDisplayTimestamps: []*timestamppb.Timestamp{},
MemoCreatedTimestamps: []*timestamppb.Timestamp{},
PinnedMemos: []string{},
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{
LinkCount: 0,
......@@ -118,12 +112,7 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
stats := userMemoStatMap[memo.CreatorID]
// Add display timestamp
displayTs := memo.CreatedTs
if instanceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}
stats.MemoDisplayTimestamps = append(stats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
stats.MemoCreatedTimestamps = append(stats.MemoCreatedTimestamps, timestamppb.New(time.Unix(memo.CreatedTs, 0)))
// Count memo stats
stats.TotalMemoCount++
......@@ -215,12 +204,7 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt
memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
}
instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get instance memo related setting")
}
displayTimestamps := []*timestamppb.Timestamp{}
createdTimestamps := []*timestamppb.Timestamp{}
tagCount := make(map[string]int32)
linkCount := int32(0)
codeCount := int32(0)
......@@ -246,11 +230,7 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt
totalMemoCount += int32(len(memos))
for _, memo := range memos {
displayTs := memo.CreatedTs
if instanceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}
displayTimestamps = append(displayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
createdTimestamps = append(createdTimestamps, timestamppb.New(time.Unix(memo.CreatedTs, 0)))
// Count different memo types based on content.
if memo.Payload != nil {
for _, tag := range memo.Payload.Tags {
......@@ -281,7 +261,7 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt
userStats := &v1pb.UserStats{
Name: fmt.Sprintf("%s/stats", BuildUserName(user.Username)),
MemoDisplayTimestamps: displayTimestamps,
MemoCreatedTimestamps: createdTimestamps,
TagCount: tagCount,
PinnedMemos: pinnedMemos,
TotalMemoCount: totalMemoCount,
......
......@@ -35,7 +35,7 @@ export const buildMemoShareImagePreviewModel = ({
}: BuildMemoShareImagePreviewModelOptions): MemoShareImagePreviewModel => {
const displayName = creator?.displayName || creator?.username || fallbackDisplayName;
const avatarUrl = getMemoSharePreviewAvatarUrl(creator?.avatarUrl);
const displayTime = memo.displayTime ? timestampDate(memo.displayTime) : memo.createTime ? timestampDate(memo.createTime) : undefined;
const displayTime = memo.createTime ? timestampDate(memo.createTime) : undefined;
const formattedDisplayTime = displayTime?.toLocaleString(locale, {
dateStyle: "medium",
timeStyle: "short",
......
......@@ -67,7 +67,7 @@ const MemoCommentSection = ({ memo, comments, parentPage }: Props) => {
</div>
)}
{comments.map((comment) => (
<div className="w-full" key={`${comment.name}-${comment.displayTime}`} id={extractMemoIdFromName(comment.name)}>
<div className="w-full" key={`${comment.name}-${comment.updateTime}`} id={extractMemoIdFromName(comment.name)}>
<MemoView memo={comment} parentPage={parentPage} showCreator compact />
</div>
))}
......
......@@ -11,8 +11,8 @@ interface Props {
function MemoDisplaySettingMenu({ className }: Props) {
const t = useTranslate();
const { orderByTimeAsc, toggleSortOrder } = useView();
const isApplying = orderByTimeAsc !== false;
const { orderByTimeAsc, timeBasis, setTimeBasis, toggleSortOrder } = useView();
const isApplying = orderByTimeAsc !== false || timeBasis !== "create_time";
return (
<Popover>
......@@ -22,7 +22,19 @@ function MemoDisplaySettingMenu({ className }: Props) {
<PopoverContent align="end" alignOffset={-12} sideOffset={14}>
<div className="flex flex-col gap-2 p-1">
<div className="w-full flex flex-row justify-between items-center">
<span className="text-sm shrink-0 mr-3 text-foreground">{t("memo.direction")}</span>
<span className="text-sm shrink-0 mr-3 text-foreground">{t("memo.shown-time")}</span>
<Select value={timeBasis} onValueChange={(value) => setTimeBasis(value === "update_time" ? "update_time" : "create_time")}>
<SelectTrigger size="sm" className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="create_time">{t("common.created-at")}</SelectItem>
<SelectItem value="update_time">{t("common.last-updated-at")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-full flex flex-row justify-between items-center">
<span className="text-sm shrink-0 mr-3 text-foreground">{t("memo.order")}</span>
<Select
value={orderByTimeAsc.toString()}
onValueChange={(value) => {
......@@ -31,12 +43,12 @@ function MemoDisplaySettingMenu({ className }: Props) {
}
}}
>
<SelectTrigger size="sm">
<SelectTrigger size="sm" className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="false">{t("memo.direction-desc")}</SelectItem>
<SelectItem value="true">{t("memo.direction-asc")}</SelectItem>
<SelectItem value="false">{t("memo.newest-first")}</SelectItem>
<SelectItem value="true">{t("memo.oldest-first")}</SelectItem>
</SelectContent>
</Select>
</div>
......
......@@ -74,7 +74,7 @@ export const LinkMemoDialog = ({
<div className="w-full flex flex-col gap-1">
<div className="flex items-center gap-1.5 text-sm text-muted-foreground select-none">
{alreadyLinked && <LinkIcon className="w-3 h-3 shrink-0" />}
<span>{memo.displayTime && timestampDate(memo.displayTime).toLocaleString()}</span>
<span>{memo.createTime && timestampDate(memo.createTime).toLocaleString()}</span>
</div>
<MemoPreview name={memo.name} content={memo.content} attachments={memo.attachments} showMemoId />
</div>
......
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { createContext, useContext } from "react";
import { useLocation } from "react-router-dom";
import { useView } from "@/contexts/ViewContext";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb";
......@@ -37,12 +38,14 @@ export const computeCommentAmount = (memo: Memo): number =>
export const useMemoViewDerived = () => {
const { memo, isArchived, readonly } = useMemoViewContext();
const { timeBasis } = useView();
const location = useLocation();
const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`) || location.pathname.startsWith("/memos/shares/");
const commentAmount = computeCommentAmount(memo);
const displayTime = memo.displayTime ? timestampDate(memo.displayTime) : undefined;
const displayTimestamp = timeBasis === "update_time" ? memo.updateTime : memo.createTime;
const displayTime = displayTimestamp ? timestampDate(displayTimestamp) : undefined;
const relativeTimeFormat: "datetime" | "auto" =
displayTime && Date.now() - displayTime.getTime() > RELATIVE_TIME_THRESHOLD_MS ? "datetime" : "auto";
......@@ -51,6 +54,7 @@ export const useMemoViewDerived = () => {
readonly,
isInMemoDetailPage,
commentAmount,
displayTime,
relativeTimeFormat,
};
};
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { BookmarkIcon } from "lucide-react";
import { useCallback, useState } from "react";
import { Link } from "react-router-dom";
......@@ -23,7 +22,7 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext();
const { relativeTimeFormat } = useMemoViewDerived();
const { displayTime: memoDisplayTime, relativeTimeFormat } = useMemoViewDerived();
const navigateTo = useNavigateTo();
const handleGotoMemoDetailPage = useCallback(() => {
......@@ -33,13 +32,9 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
const { unpinMemo } = useMemoActions(memo);
const displayTime = isArchived ? (
(memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language)
memoDisplayTime?.toLocaleString(i18n.language)
) : (
<relative-time
datetime={(memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toISOString()}
lang={i18n.language}
format={relativeTimeFormat}
></relative-time>
<relative-time datetime={memoDisplayTime?.toISOString()} lang={i18n.language} format={relativeTimeFormat}></relative-time>
);
return (
......
......@@ -88,7 +88,7 @@ const PagedMemoList = (props: Props) => {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteMemos(
{
state: props.state || State.NORMAL,
orderBy: props.orderBy || "display_time desc",
orderBy: props.orderBy || "create_time desc",
filter: props.filter,
pageSize: props.pageSize || DEFAULT_LIST_MEMOS_PAGE_SIZE,
},
......
......@@ -72,13 +72,6 @@ const MemoRelatedSettings = () => {
return (
<SettingSection title={t("setting.memo.label")}>
<SettingGroup title={t("common.basic")}>
<SettingRow label={t("setting.system.display-with-updated-time")}>
<Switch
checked={memoRelatedSetting.displayWithUpdateTime}
onCheckedChange={(checked) => updatePartialSetting({ displayWithUpdateTime: checked })}
/>
</SettingRow>
<SettingRow label={t("setting.system.enable-double-click-to-edit")}>
<Switch
checked={memoRelatedSetting.enableDoubleClickEdit}
......
......@@ -52,7 +52,7 @@ const UserMemoMap = ({ creator, className }: Props) => {
const { data, isLoading } = useInfiniteMemos(
{
state: State.NORMAL,
orderBy: "display_time desc",
orderBy: "create_time desc",
pageSize: 1000,
filter: creatorFilter,
},
......@@ -133,8 +133,8 @@ const UserMemoMap = ({ creator, className }: Props) => {
Memo
</span>
<span className="block text-[11px] font-medium text-muted-foreground">
{memo.displayTime &&
timestampDate(memo.displayTime).toLocaleDateString(undefined, {
{memo.createTime &&
timestampDate(memo.createTime).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
......
import { createContext, type ReactNode, useContext, useState } from "react";
export type MemoTimeBasis = "create_time" | "update_time";
interface ViewState {
orderByTimeAsc: boolean;
timeBasis?: MemoTimeBasis;
sortTimeField?: MemoTimeBasis;
}
interface ViewContextValue {
orderByTimeAsc: boolean;
timeBasis: MemoTimeBasis;
toggleSortOrder: () => void;
setTimeBasis: (field: MemoTimeBasis) => void;
}
const ViewContext = createContext<ViewContextValue | null>(null);
......@@ -10,13 +20,16 @@ const ViewContext = createContext<ViewContextValue | null>(null);
const LOCAL_STORAGE_KEY = "memos-view-setting";
export function ViewProvider({ children }: { children: ReactNode }) {
const getInitialState = () => {
const getInitialState = (): ViewState => {
try {
const cached = localStorage.getItem(LOCAL_STORAGE_KEY);
if (cached) {
const data = JSON.parse(cached);
const data = JSON.parse(cached) as Partial<ViewState>;
const cachedTimeBasis = data.timeBasis ?? data.sortTimeField;
const timeBasis = cachedTimeBasis === "create_time" || cachedTimeBasis === "update_time" ? cachedTimeBasis : undefined;
return {
orderByTimeAsc: Boolean(data.orderByTimeAsc ?? false),
timeBasis,
};
}
} catch (error) {
......@@ -26,8 +39,9 @@ export function ViewProvider({ children }: { children: ReactNode }) {
};
const [viewState, setViewState] = useState(getInitialState);
const timeBasis = viewState.timeBasis ?? "create_time";
const persistToStorage = (newState: typeof viewState) => {
const persistToStorage = (newState: ViewState) => {
try {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState));
} catch (error) {
......@@ -43,11 +57,21 @@ export function ViewProvider({ children }: { children: ReactNode }) {
});
};
const setTimeBasis = (field: MemoTimeBasis) => {
setViewState((prev) => {
const newState = { ...prev, timeBasis: field };
persistToStorage(newState);
return newState;
});
};
return (
<ViewContext.Provider
value={{
...viewState,
orderByTimeAsc: viewState.orderByTimeAsc,
timeBasis,
toggleSortOrder,
setTimeBasis,
}}
>
{children}
......
......@@ -49,15 +49,15 @@ export const useFilteredMemoStats = (options: UseFilteredMemoStatsOptions = {}):
}
}
const displayDates = (memosResponse?.memos ?? [])
.map((memo) => (memo.displayTime ? timestampDate(memo.displayTime) : undefined))
.map((memo) => (memo.createTime ? timestampDate(memo.createTime) : undefined))
.filter((date): date is Date => date !== undefined)
.map(toDateString);
activityStats = countBy(displayDates);
} else if (userName && userStats) {
// home/profile: use backend per-user stats
if (userStats.memoDisplayTimestamps && userStats.memoDisplayTimestamps.length > 0) {
if (userStats.memoCreatedTimestamps && userStats.memoCreatedTimestamps.length > 0) {
activityStats = countBy(
userStats.memoDisplayTimestamps
userStats.memoCreatedTimestamps
.map((ts) => (ts ? timestampDate(ts) : undefined))
.filter((date): date is Date => date !== undefined)
.map(toDateString),
......@@ -69,7 +69,7 @@ export const useFilteredMemoStats = (options: UseFilteredMemoStatsOptions = {}):
} else if (memosResponse?.memos) {
// archived/fallback: compute from cached memos
const displayDates = memosResponse.memos
.map((memo) => (memo.displayTime ? timestampDate(memo.displayTime) : undefined))
.map((memo) => (memo.createTime ? timestampDate(memo.createTime) : undefined))
.filter((date): date is Date => date !== undefined)
.map(toDateString);
activityStats = countBy(displayDates);
......
import { useMemo } from "react";
import { useAuth } from "@/contexts/AuthContext";
import { useInstance } from "@/contexts/InstanceContext";
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
import { buildMemoCreatorFilter } from "@/helpers/resource-names";
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
......@@ -37,7 +36,6 @@ export const useMemoFilters = (options: UseMemoFiltersOptions = {}): string | un
const { shortcuts } = useAuth();
const { filters, shortcut: currentShortcut } = useMemoFilterContext();
const { memoRelatedSetting } = useInstance();
// Get selected shortcut if needed
const selectedShortcut = useMemo(() => {
......@@ -79,14 +77,11 @@ export const useMemoFilters = (options: UseMemoFiltersOptions = {}): string | un
} else if (filter.factor === "property.hasCode") {
conditions.push(`has_code`);
} else if (filter.factor === "displayTime") {
const displayWithUpdateTime = memoRelatedSetting?.displayWithUpdateTime ?? false;
const factor = displayWithUpdateTime ? "updated_ts" : "created_ts";
const filterDate = new Date(filter.value);
const filterUtcTimestamp = filterDate.getTime() + filterDate.getTimezoneOffset() * 60 * 1000;
const timestampAfter = filterUtcTimestamp / 1000;
conditions.push(`${factor} >= ${timestampAfter} && ${factor} < ${timestampAfter + 60 * 60 * 24}`);
conditions.push(`created_ts >= ${timestampAfter} && created_ts < ${timestampAfter + 60 * 60 * 24}`);
}
}
......@@ -97,5 +92,5 @@ export const useMemoFilters = (options: UseMemoFiltersOptions = {}): string | un
}
return conditions.length > 0 ? conditions.join(" && ") : undefined;
}, [creatorName, includeShortcuts, includePinned, visibilities, selectedShortcut, filters, memoRelatedSetting]);
}, [creatorName, includeShortcuts, includePinned, visibilities, selectedShortcut, filters]);
};
import { timestampDate } from "@bufbuild/protobuf/wkt";
import dayjs from "dayjs";
import { useMemo } from "react";
import { useView } from "@/contexts/ViewContext";
import { type MemoTimeBasis, useView } from "@/contexts/ViewContext";
import { State } from "@/types/proto/api/v1/common_pb";
import { Memo } from "@/types/proto/api/v1/memo_service_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
export interface UseMemoSortingOptions {
pinnedFirst?: boolean;
......@@ -15,15 +15,20 @@ export interface UseMemoSortingResult {
orderBy: string;
}
const getMemoSortTime = (memo: Memo, timeBasis: MemoTimeBasis): Date | undefined => {
const timestamp = timeBasis === "update_time" ? memo.updateTime : memo.createTime;
return timestamp ? timestampDate(timestamp) : undefined;
};
export const useMemoSorting = (options: UseMemoSortingOptions = {}): UseMemoSortingResult => {
const { pinnedFirst = false, state = State.NORMAL } = options;
const { orderByTimeAsc } = useView();
const { orderByTimeAsc, timeBasis } = useView();
// Generate orderBy string for API
const orderBy = useMemo(() => {
const timeOrder = orderByTimeAsc ? "display_time asc" : "display_time desc";
const timeOrder = orderByTimeAsc ? `${timeBasis} asc` : `${timeBasis} desc`;
return pinnedFirst ? `pinned desc, ${timeOrder}` : timeOrder;
}, [pinnedFirst, orderByTimeAsc]);
}, [pinnedFirst, orderByTimeAsc, timeBasis]);
// Generate listSort function for client-side sorting
const listSort = useMemo(() => {
......@@ -36,13 +41,13 @@ export const useMemoSorting = (options: UseMemoSortingOptions = {}): UseMemoSort
return b.pinned ? 1 : -1;
}
// Then sort by display time
const aTime = a.displayTime ? timestampDate(a.displayTime) : undefined;
const bTime = b.displayTime ? timestampDate(b.displayTime) : undefined;
// Then sort by the selected time field.
const aTime = getMemoSortTime(a, timeBasis);
const bTime = getMemoSortTime(b, timeBasis);
return orderByTimeAsc ? dayjs(aTime).unix() - dayjs(bTime).unix() : dayjs(bTime).unix() - dayjs(aTime).unix();
});
};
}, [pinnedFirst, state, orderByTimeAsc]);
}, [pinnedFirst, state, orderByTimeAsc, timeBasis]);
return { listSort, orderBy };
};
......@@ -247,9 +247,13 @@
"load-more": "Load more",
"no-archived-memos": "No archived memos.",
"no-memos": "No memos.",
"newest-first": "Newest first",
"oldest-first": "Oldest first",
"order": "Order",
"order-by": "Order By",
"outline": "Outline",
"search-placeholder": "Search memos...",
"shown-time": "Shown time",
"share": {
"active-links": "Active share links",
"copied": "Copied!",
......
......@@ -209,8 +209,12 @@
"load-more": "加载更多",
"no-archived-memos": "没有已归档备忘录。",
"no-memos": "无备忘录",
"newest-first": "最新优先",
"oldest-first": "最早优先",
"order": "顺序",
"order-by": "排序",
"search-placeholder": "搜索备忘录",
"shown-time": "显示时间",
"show-less": "显示较少",
"show-more": "查看更多",
"to-do": "待办",
......
......@@ -206,8 +206,12 @@
"load-more": "載入更多",
"no-archived-memos": "無已封存的備忘錄",
"no-memos": "無備忘錄",
"newest-first": "最新優先",
"oldest-first": "最早優先",
"order": "順序",
"order-by": "排序",
"search-placeholder": "搜尋備忘錄",
"shown-time": "顯示時間",
"show-less": "顯示較少",
"show-more": "查看更多",
"to-do": "待辦事項",
......
......@@ -24,7 +24,7 @@ const Home = () => {
return (
<div className="w-full min-h-full bg-background text-foreground">
<PagedMemoList
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />}
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility showPinned compact />}
listSort={listSort}
orderBy={orderBy}
filter={memoFilter}
......
......@@ -100,7 +100,7 @@ const MemoDetail = () => {
</div>
)}
<MemoView
key={`${displayMemo.name}-${displayMemo.displayTime}`}
key={`${displayMemo.name}-${displayMemo.updateTime}`}
memo={displayMemo}
compact={false}
parentPage={locationState?.from}
......
......@@ -130,7 +130,7 @@ const UserProfile = () => {
{activeTab === "memos" ? (
<PagedMemoList
renderer={(memo: Memo) => (
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />
<MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility showPinned compact />
)}
listSort={listSort}
orderBy={orderBy}
......
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file api/v1/ai_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
......@@ -31,14 +31,14 @@ export type TranscribeRequest = Message<"memos.api.v1.TranscribeRequest"> & {
*
* @generated from field: memos.api.v1.TranscriptionConfig config = 2;
*/
config?: TranscriptionConfig;
config?: TranscriptionConfig | undefined;
/**
* Required. Audio input.
*
* @generated from field: memos.api.v1.TranscriptionAudio audio = 3;
*/
audio?: TranscriptionAudio;
audio?: TranscriptionAudio | undefined;
};
/**
......
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file api/v1/attachment_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
......@@ -72,7 +72,7 @@ export type Attachment = Message<"memos.api.v1.Attachment"> & {
*
* @generated from field: google.protobuf.Timestamp create_time = 2;
*/
createTime?: Timestamp;
createTime?: Timestamp | undefined;
/**
* The filename of the attachment.
......@@ -115,14 +115,14 @@ export type Attachment = Message<"memos.api.v1.Attachment"> & {
*
* @generated from field: optional string memo = 8;
*/
memo?: string;
memo?: string | undefined;
/**
* Optional. Motion media metadata.
*
* @generated from field: memos.api.v1.MotionMedia motion_media = 9;
*/
motionMedia?: MotionMedia;
motionMedia?: MotionMedia | undefined;
};
/**
......@@ -141,7 +141,7 @@ export type CreateAttachmentRequest = Message<"memos.api.v1.CreateAttachmentRequ
*
* @generated from field: memos.api.v1.Attachment attachment = 1;
*/
attachment?: Attachment;
attachment?: Attachment | undefined;
/**
* Optional. The attachment ID to use for this attachment.
......@@ -270,14 +270,14 @@ export type UpdateAttachmentRequest = Message<"memos.api.v1.UpdateAttachmentRequ
*
* @generated from field: memos.api.v1.Attachment attachment = 1;
*/
attachment?: Attachment;
attachment?: Attachment | undefined;
/**
* Required. The list of fields to update.
*
* @generated from field: google.protobuf.FieldMask update_mask = 2;
*/
updateMask?: FieldMask;
updateMask?: FieldMask | undefined;
};
/**
......
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file api/v1/auth_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
......@@ -40,7 +40,7 @@ export type GetCurrentUserResponse = Message<"memos.api.v1.GetCurrentUserRespons
*
* @generated from field: memos.api.v1.User user = 1;
*/
user?: User;
user?: User | undefined;
};
/**
......@@ -166,7 +166,7 @@ export type SignInResponse = Message<"memos.api.v1.SignInResponse"> & {
*
* @generated from field: memos.api.v1.User user = 1;
*/
user?: User;
user?: User | undefined;
/**
* The short-lived access token for API requests.
......@@ -182,7 +182,7 @@ export type SignInResponse = Message<"memos.api.v1.SignInResponse"> & {
*
* @generated from field: google.protobuf.Timestamp access_token_expires_at = 3;
*/
accessTokenExpiresAt?: Timestamp;
accessTokenExpiresAt?: Timestamp | undefined;
};
/**
......@@ -234,7 +234,7 @@ export type RefreshTokenResponse = Message<"memos.api.v1.RefreshTokenResponse">
*
* @generated from field: google.protobuf.Timestamp expires_at = 2;
*/
expiresAt?: Timestamp;
expiresAt?: Timestamp | undefined;
};
/**
......
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file api/v1/common.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
......
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file api/v1/idp_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
......@@ -56,7 +56,7 @@ export type IdentityProvider = Message<"memos.api.v1.IdentityProvider"> & {
*
* @generated from field: memos.api.v1.IdentityProviderConfig config = 5;
*/
config?: IdentityProviderConfig;
config?: IdentityProviderConfig | undefined;
};
/**
......@@ -181,7 +181,7 @@ export type OAuth2Config = Message<"memos.api.v1.OAuth2Config"> & {
/**
* @generated from field: memos.api.v1.FieldMapping field_mapping = 7;
*/
fieldMapping?: FieldMapping;
fieldMapping?: FieldMapping | undefined;
};
/**
......@@ -252,7 +252,7 @@ export type CreateIdentityProviderRequest = Message<"memos.api.v1.CreateIdentity
*
* @generated from field: memos.api.v1.IdentityProvider identity_provider = 1;
*/
identityProvider?: IdentityProvider;
identityProvider?: IdentityProvider | undefined;
/**
* Optional. The ID to use for the identity provider, which will become the final component of the resource name.
......@@ -279,7 +279,7 @@ export type UpdateIdentityProviderRequest = Message<"memos.api.v1.UpdateIdentity
*
* @generated from field: memos.api.v1.IdentityProvider identity_provider = 1;
*/
identityProvider?: IdentityProvider;
identityProvider?: IdentityProvider | undefined;
/**
* Required. The update mask applies to the resource. Only the top level fields of
......@@ -287,7 +287,7 @@ export type UpdateIdentityProviderRequest = Message<"memos.api.v1.UpdateIdentity
*
* @generated from field: google.protobuf.FieldMask update_mask = 2;
*/
updateMask?: FieldMask;
updateMask?: FieldMask | undefined;
};
/**
......
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file api/v1/shortcut_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
......@@ -128,7 +128,7 @@ export type CreateShortcutRequest = Message<"memos.api.v1.CreateShortcutRequest"
*
* @generated from field: memos.api.v1.Shortcut shortcut = 2;
*/
shortcut?: Shortcut;
shortcut?: Shortcut | undefined;
/**
* Optional. If set, validate the request, but do not actually create the shortcut.
......@@ -154,14 +154,14 @@ export type UpdateShortcutRequest = Message<"memos.api.v1.UpdateShortcutRequest"
*
* @generated from field: memos.api.v1.Shortcut shortcut = 1;
*/
shortcut?: Shortcut;
shortcut?: Shortcut | undefined;
/**
* Optional. The list of fields to update.
*
* @generated from field: google.protobuf.FieldMask update_mask = 2;
*/
updateMask?: FieldMask;
updateMask?: FieldMask | undefined;
};
/**
......
......@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file google/api/annotations.proto (package google.api, syntax proto3)
/* eslint-disable */
......
......@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file google/api/client.proto (package google.api, syntax proto3)
/* eslint-disable */
......@@ -57,7 +57,7 @@ export type CommonLanguageSettings = Message<"google.api.CommonLanguageSettings"
*
* @generated from field: google.api.SelectiveGapicGeneration selective_gapic_generation = 3;
*/
selectiveGapicGeneration?: SelectiveGapicGeneration;
selectiveGapicGeneration?: SelectiveGapicGeneration | undefined;
};
/**
......@@ -102,56 +102,56 @@ export type ClientLibrarySettings = Message<"google.api.ClientLibrarySettings">
*
* @generated from field: google.api.JavaSettings java_settings = 21;
*/
javaSettings?: JavaSettings;
javaSettings?: JavaSettings | undefined;
/**
* Settings for C++ client libraries.
*
* @generated from field: google.api.CppSettings cpp_settings = 22;
*/
cppSettings?: CppSettings;
cppSettings?: CppSettings | undefined;
/**
* Settings for PHP client libraries.
*
* @generated from field: google.api.PhpSettings php_settings = 23;
*/
phpSettings?: PhpSettings;
phpSettings?: PhpSettings | undefined;
/**
* Settings for Python client libraries.
*
* @generated from field: google.api.PythonSettings python_settings = 24;
*/
pythonSettings?: PythonSettings;
pythonSettings?: PythonSettings | undefined;
/**
* Settings for Node client libraries.
*
* @generated from field: google.api.NodeSettings node_settings = 25;
*/
nodeSettings?: NodeSettings;
nodeSettings?: NodeSettings | undefined;
/**
* Settings for .NET client libraries.
*
* @generated from field: google.api.DotnetSettings dotnet_settings = 26;
*/
dotnetSettings?: DotnetSettings;
dotnetSettings?: DotnetSettings | undefined;
/**
* Settings for Ruby client libraries.
*
* @generated from field: google.api.RubySettings ruby_settings = 27;
*/
rubySettings?: RubySettings;
rubySettings?: RubySettings | undefined;
/**
* Settings for Go client libraries.
*
* @generated from field: google.api.GoSettings go_settings = 28;
*/
goSettings?: GoSettings;
goSettings?: GoSettings | undefined;
};
/**
......@@ -313,7 +313,7 @@ export type JavaSettings = Message<"google.api.JavaSettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 3;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
};
/**
......@@ -334,7 +334,7 @@ export type CppSettings = Message<"google.api.CppSettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 1;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
};
/**
......@@ -355,7 +355,7 @@ export type PhpSettings = Message<"google.api.PhpSettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 1;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
};
/**
......@@ -376,14 +376,14 @@ export type PythonSettings = Message<"google.api.PythonSettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 1;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
/**
* Experimental features to be included during client library generation.
*
* @generated from field: google.api.PythonSettings.ExperimentalFeatures experimental_features = 2;
*/
experimentalFeatures?: PythonSettings_ExperimentalFeatures;
experimentalFeatures?: PythonSettings_ExperimentalFeatures | undefined;
};
/**
......@@ -450,7 +450,7 @@ export type NodeSettings = Message<"google.api.NodeSettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 1;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
};
/**
......@@ -471,7 +471,7 @@ export type DotnetSettings = Message<"google.api.DotnetSettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 1;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
/**
* Map from original service names to renamed versions.
......@@ -542,7 +542,7 @@ export type RubySettings = Message<"google.api.RubySettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 1;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
};
/**
......@@ -563,7 +563,7 @@ export type GoSettings = Message<"google.api.GoSettings"> & {
*
* @generated from field: google.api.CommonLanguageSettings common = 1;
*/
common?: CommonLanguageSettings;
common?: CommonLanguageSettings | undefined;
/**
* Map of service names to renamed services. Keys are the package relative
......@@ -626,7 +626,7 @@ export type MethodSettings = Message<"google.api.MethodSettings"> & {
*
* @generated from field: google.api.MethodSettings.LongRunning long_running = 2;
*/
longRunning?: MethodSettings_LongRunning;
longRunning?: MethodSettings_LongRunning | undefined;
/**
* List of top-level fields of the request message, that should be
......@@ -669,7 +669,7 @@ export type MethodSettings_LongRunning = Message<"google.api.MethodSettings.Long
*
* @generated from field: google.protobuf.Duration initial_poll_delay = 1;
*/
initialPollDelay?: Duration;
initialPollDelay?: Duration | undefined;
/**
* Multiplier to gradually increase delay between subsequent polls until it
......@@ -686,7 +686,7 @@ export type MethodSettings_LongRunning = Message<"google.api.MethodSettings.Long
*
* @generated from field: google.protobuf.Duration max_poll_delay = 3;
*/
maxPollDelay?: Duration;
maxPollDelay?: Duration | undefined;
/**
* Total polling timeout.
......@@ -694,7 +694,7 @@ export type MethodSettings_LongRunning = Message<"google.api.MethodSettings.Long
*
* @generated from field: google.protobuf.Duration total_poll_timeout = 4;
*/
totalPollTimeout?: Duration;
totalPollTimeout?: Duration | undefined;
};
/**
......
......@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file google/api/field_behavior.proto (package google.api, syntax proto3)
/* eslint-disable */
......
......@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file google/api/http.proto (package google.api, syntax proto3)
/* eslint-disable */
......
......@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file google/api/launch_stage.proto (package google.api, syntax proto3)
/* eslint-disable */
......
......@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file google/api/resource.proto (package google.api, syntax proto3)
/* eslint-disable */
......
......@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file google/type/color.proto (package google.type, syntax proto3)
/* eslint-disable */
......@@ -192,7 +192,7 @@ export type Color = Message<"google.type.Color"> & {
*
* @generated from field: google.protobuf.FloatValue alpha = 4;
*/
alpha?: number;
alpha?: number | undefined;
};
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment