diff --git a/api/v2/memo_service.go b/api/v2/memo_service.go index 6cef7f1fd..0b5dafebe 100644 --- a/api/v2/memo_service.go +++ b/api/v2/memo_service.go @@ -59,6 +59,9 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos if filter.Visibility != nil { memoFind.VisibilityList = []store.Visibility{*filter.Visibility} } + if len(filter.Visibilities) > 0 { + memoFind.VisibilityList = filter.Visibilities + } if filter.CreatedTsBefore != nil { memoFind.CreatedTsBefore = filter.CreatedTsBefore } @@ -81,6 +84,12 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos } memoFind.CreatorID = &user.ID } + if filter.Tag != nil { + memoFind.ContentSearch = append(memoFind.ContentSearch, fmt.Sprintf("#%s", *filter.Tag)) + } + if filter.ContentSearch != nil { + memoFind.ContentSearch = append(memoFind.ContentSearch, *filter.ContentSearch) + } if filter.RowStatus != nil { memoFind.RowStatus = filter.RowStatus } @@ -95,9 +104,8 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} } - if request.PageSize != 0 { - offset := int(request.Page * request.PageSize) - limit := int(request.PageSize) + if request.Limit != 0 { + offset, limit := int(request.Offset), int(request.Limit) memoFind.Offset = &offset memoFind.Limit = &limit } @@ -187,6 +195,9 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe rowStatus := convertRowStatusToStore(request.Memo.RowStatus) println("rowStatus", rowStatus) update.RowStatus = &rowStatus + } else if path == "created_ts" { + createdTs := request.Memo.CreateTime.AsTime().Unix() + update.CreatedTs = &createdTs } } @@ -502,6 +513,7 @@ func convertVisibilityToStore(visibility apiv2pb.Visibility) store.Visibility { // ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter. var ListMemosFilterCELAttributes = []cel.EnvOption{ cel.Variable("visibility", cel.StringType), + cel.Variable("visibilities", cel.ListType(cel.StringType)), cel.Variable("created_ts_before", cel.IntType), cel.Variable("created_ts_after", cel.IntType), cel.Variable("creator", cel.StringType), @@ -510,9 +522,12 @@ var ListMemosFilterCELAttributes = []cel.EnvOption{ type ListMemosFilter struct { Visibility *store.Visibility + Visibilities []store.Visibility CreatedTsBefore *int64 CreatedTsAfter *int64 Creator *string + Tag *string + ContentSearch *string RowStatus *store.RowStatus } @@ -543,6 +558,14 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) { visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue()) filter.Visibility = &visibility } + if idExpr.Name == "visibilities" { + visibilities := []store.Visibility{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + visibilities = append(visibilities, store.Visibility(value)) + } + filter.Visibilities = visibilities + } if idExpr.Name == "created_ts_before" { createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value() filter.CreatedTsBefore = &createdTsBefore @@ -555,6 +578,14 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) { creator := callExpr.Args[1].GetConstExpr().GetStringValue() filter.Creator = &creator } + if idExpr.Name == "tag" { + tag := callExpr.Args[1].GetConstExpr().GetStringValue() + filter.Tag = &tag + } + if idExpr.Name == "content_search" { + contentSearch := callExpr.Args[1].GetConstExpr().GetStringValue() + filter.ContentSearch = &contentSearch + } if idExpr.Name == "row_status" { rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue()) filter.RowStatus = &rowStatus diff --git a/proto/api/v2/memo_service.proto b/proto/api/v2/memo_service.proto index af1956439..6c3449d55 100644 --- a/proto/api/v2/memo_service.proto +++ b/proto/api/v2/memo_service.proto @@ -128,9 +128,11 @@ message CreateMemoResponse { } message ListMemosRequest { - int32 page = 1; + // offset is the offset of the first memo to return. + int32 offset = 1; - int32 page_size = 2; + // limit is the maximum number of memos to return. + int32 limit = 2; // Filter is used to filter memos returned in the list. // Format: "creator == users/{username} && visibility == PUBLIC" diff --git a/proto/gen/api/v2/README.md b/proto/gen/api/v2/README.md index 00a2371a3..43c352321 100644 --- a/proto/gen/api/v2/README.md +++ b/proto/gen/api/v2/README.md @@ -1760,8 +1760,8 @@ | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| page | [int32](#int32) | | | -| page_size | [int32](#int32) | | | +| offset | [int32](#int32) | | offset is the offset of the first memo to return. | +| limit | [int32](#int32) | | limit is the maximum number of memos to return. | | filter | [string](#string) | | Filter is used to filter memos returned in the list. Format: "creator == users/{username} && visibility == PUBLIC" | diff --git a/proto/gen/api/v2/memo_service.pb.go b/proto/gen/api/v2/memo_service.pb.go index f68814f64..a8ccdfce7 100644 --- a/proto/gen/api/v2/memo_service.pb.go +++ b/proto/gen/api/v2/memo_service.pb.go @@ -311,8 +311,10 @@ type ListMemosRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` - PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // offset is the offset of the first memo to return. + Offset int32 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"` + // limit is the maximum number of memos to return. + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` // Filter is used to filter memos returned in the list. // Format: "creator == users/{username} && visibility == PUBLIC" Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` @@ -350,16 +352,16 @@ func (*ListMemosRequest) Descriptor() ([]byte, []int) { return file_api_v2_memo_service_proto_rawDescGZIP(), []int{3} } -func (x *ListMemosRequest) GetPage() int32 { +func (x *ListMemosRequest) GetOffset() int32 { if x != nil { - return x.Page + return x.Offset } return 0 } -func (x *ListMemosRequest) GetPageSize() int32 { +func (x *ListMemosRequest) GetLimit() int32 { if x != nil { - return x.PageSize + return x.Limit } return 0 } @@ -1338,195 +1340,195 @@ var file_api_v2_memo_service_proto_rawDesc = []byte{ 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, - 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x5b, 0x0a, 0x10, 0x4c, 0x69, - 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, - 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, - 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, - 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, - 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, - 0x05, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, - 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x39, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, - 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, - 0x65, 0x6d, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, - 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, - 0x65, 0x6d, 0x6f, 0x22, 0x88, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, - 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, - 0x6f, 0x12, 0x3b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, - 0x73, 0x6b, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x3c, - 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x23, 0x0a, 0x11, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, - 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x65, - 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4d, - 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x58, 0x0a, 0x10, 0x4c, 0x69, + 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, + 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x6d, 0x65, 0x6d, + 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x05, 0x6d, 0x65, + 0x6d, 0x6f, 0x73, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x39, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, + 0x22, 0x88, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x3b, + 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, + 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x3c, 0x0a, 0x12, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, + 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14, + 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x2a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x51, 0x0a, + 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x22, 0x63, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x09, 0x72, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, + 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, + 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x2a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x55, 0x0a, + 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, + 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x63, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, + 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, - 0x22, 0x51, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, - 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x22, 0x63, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, - 0x0a, 0x09, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, - 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, - 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4d, - 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, - 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, - 0x22, 0x55, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, - 0x09, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, - 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x63, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x52, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x22, 0x43, 0x0a, 0x19, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, - 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, - 0x6f, 0x22, 0x29, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, - 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x44, 0x0a, 0x18, - 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x6d, 0x65, 0x6d, 0x6f, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x05, 0x6d, 0x65, 0x6d, - 0x6f, 0x73, 0x2a, 0x50, 0x0a, 0x0a, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, - 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, - 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x4f, - 0x54, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, - 0x49, 0x43, 0x10, 0x03, 0x32, 0xa2, 0x0b, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, - 0x22, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x12, - 0x63, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x1e, 0x2e, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x67, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x12, - 0x1c, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, - 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, - 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, - 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0xda, 0x41, - 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x80, 0x01, - 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, - 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, - 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x2f, 0xda, 0x41, 0x0f, 0x69, 0x64, 0x2c, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, - 0x61, 0x73, 0x6b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x32, 0x12, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, - 0x12, 0x70, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, - 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x1f, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x2a, 0x12, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, - 0x64, 0x7d, 0x12, 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, - 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, - 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, - 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, - 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, - 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, - 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, 0x41, 0x02, - 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, - 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, + 0x12, 0x37, 0x0a, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x22, 0x43, 0x0a, 0x19, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x29, + 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x44, 0x0a, 0x18, 0x4c, 0x69, 0x73, + 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x05, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2a, + 0x50, 0x0a, 0x0a, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x1a, 0x0a, + 0x16, 0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x49, + 0x56, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x4f, 0x54, 0x45, 0x43, + 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, + 0x03, 0x32, 0xa2, 0x0b, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x69, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, + 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x63, 0x0a, 0x09, + 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x1e, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, + 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, + 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x12, 0x67, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1c, 0x2e, 0x6d, + 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, + 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6d, 0x65, 0x6d, + 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, + 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0xda, 0x41, 0x02, 0x69, 0x64, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, + 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x80, 0x01, 0x0a, 0x0a, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, + 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, + 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, + 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2f, 0xda, 0x41, + 0x0f, 0x69, 0x64, 0x2c, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x32, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x70, 0x0a, + 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65, + 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, + 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, + 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x2a, 0x12, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, + 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, - 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, - 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, - 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, - 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, - 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x29, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, - 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8e, 0x01, 0x0a, 0x11, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, - 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x28, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x1b, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, - 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x8b, 0x01, 0x0a, 0x10, - 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, - 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x28, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, - 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0xa8, 0x01, 0x0a, 0x10, 0x63, 0x6f, - 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x10, - 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, - 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, - 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, - 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, - 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, - 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, - 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, + 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, + 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, + 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, + 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, + 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, + 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, + 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, + 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, 0x41, + 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8e, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x2e, + 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, + 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, + 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x1b, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, + 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x8b, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, + 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, + 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0xda, 0x41, + 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x63, 0x6f, + 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0xa8, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, + 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x10, 0x4d, 0x65, 0x6d, + 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, + 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, + 0x32, 0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, + 0x41, 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, + 0x70, 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, + 0x69, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0xea, 0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, + 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/web/src/components/ChangeMemoCreatedTsDialog.tsx b/web/src/components/ChangeMemoCreatedTsDialog.tsx index c4d1a70a1..4d80d3772 100644 --- a/web/src/components/ChangeMemoCreatedTsDialog.tsx +++ b/web/src/components/ChangeMemoCreatedTsDialog.tsx @@ -2,26 +2,26 @@ import { Button } from "@mui/joy"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { getNormalizedTimeString, getUnixTime } from "@/helpers/datetime"; -import { useMemoStore } from "@/store/module"; +import { useMemoV1Store } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; import { generateDialog } from "./Dialog"; import Icon from "./Icon"; interface Props extends DialogProps { - memoId: MemoId; + memoId: number; } const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { const t = useTranslate(); const { destroy, memoId } = props; - const memoStore = useMemoStore(); + const memoStore = useMemoV1Store(); const [createdAt, setCreatedAt] = useState(""); const maxDatetimeValue = getNormalizedTimeString(); useEffect(() => { - memoStore.getMemoById(memoId).then((memo) => { + memoStore.getOrFetchMemoById(memoId).then((memo) => { if (memo) { - const datetime = getNormalizedTimeString(memo.createdTs); + const datetime = getNormalizedTimeString(memo.createTime); setCreatedAt(datetime); } else { toast.error(t("message.memo-not-found")); @@ -41,18 +41,19 @@ const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { const nowTs = getUnixTime(); - const createdTs = getUnixTime(createdAt); - - if (createdTs > nowTs) { + if (getUnixTime(createdAt) > nowTs) { toast.error(t("message.invalid-created-datetime")); return; } try { - await memoStore.patchMemo({ - id: memoId, - createdTs, - }); + await memoStore.updateMemo( + { + id: memoId, + createTime: new Date(createdAt), + }, + ["created_ts"] + ); toast.success(t("message.memo-updated-datetime")); handleCloseBtnClick(); } catch (error: any) { @@ -90,7 +91,7 @@ const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { ); }; -function showChangeMemoCreatedTsDialog(memoId: MemoId) { +function showChangeMemoCreatedTsDialog(memoId: number) { generateDialog( { className: "change-memo-created-ts-dialog", diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx deleted file mode 100644 index 548b17f12..000000000 --- a/web/src/components/Memo.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import { Divider, Tooltip } from "@mui/joy"; -import { memo, useEffect, useRef, useState } from "react"; -import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; -import { UNKNOWN_ID } from "@/helpers/consts"; -import { getRelativeTimeString } from "@/helpers/datetime"; -import useCurrentUser from "@/hooks/useCurrentUser"; -import useNavigateTo from "@/hooks/useNavigateTo"; -import { useFilterStore, useMemoStore } from "@/store/module"; -import { useUserV1Store, extractUsernameFromName } from "@/store/v1"; -import { useTranslate } from "@/utils/i18n"; -import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; -import { showCommonDialog } from "./Dialog/CommonDialog"; -import Icon from "./Icon"; -import MemoContentV1 from "./MemoContentV1"; -import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog"; -import MemoRelationListView from "./MemoRelationListView"; -import MemoResourceListView from "./MemoResourceListView"; -import showPreviewImageDialog from "./PreviewImageDialog"; -import showShareMemoDialog from "./ShareMemoDialog"; -import UserAvatar from "./UserAvatar"; -import VisibilityIcon from "./VisibilityIcon"; -import "@/less/memo.less"; - -interface Props { - memo: Memo; - showCreator?: boolean; - showParent?: boolean; - showVisibility?: boolean; - showPinnedStyle?: boolean; - lazyRendering?: boolean; -} - -const Memo: React.FC = (props: Props) => { - const { memo, lazyRendering } = props; - const t = useTranslate(); - const navigateTo = useNavigateTo(); - const { i18n } = useTranslation(); - const filterStore = useFilterStore(); - const memoStore = useMemoStore(); - const userV1Store = useUserV1Store(); - const user = useCurrentUser(); - const [shouldRender, setShouldRender] = useState(lazyRendering ? false : true); - const [displayTime, setDisplayTime] = useState(getRelativeTimeString(memo.displayTs)); - const memoContainerRef = useRef(null); - const readonly = memo.creatorUsername !== extractUsernameFromName(user?.name); - const [creator, setCreator] = useState(userV1Store.getUserByUsername(memo.creatorUsername)); - const referenceRelations = memo.relationList.filter((relation) => relation.type === "REFERENCE"); - - // Prepare memo creator. - useEffect(() => { - if (creator) return; - - const fn = async () => { - const user = await userV1Store.getOrFetchUserByUsername(memo.creatorUsername); - setCreator(user); - }; - - fn(); - }, [memo.creatorUsername]); - - // Update display time string. - useEffect(() => { - let intervalFlag: any = -1; - if (Date.now() - memo.displayTs < 1000 * 60 * 60 * 24) { - intervalFlag = setInterval(() => { - setDisplayTime(getRelativeTimeString(memo.displayTs)); - }, 1000 * 1); - } - - return () => { - clearInterval(intervalFlag); - }; - }, [i18n.language]); - - // Lazy rendering. - useEffect(() => { - if (shouldRender) return; - if (!memoContainerRef.current) return; - - const observer = new IntersectionObserver(([entry]) => { - if (!entry.isIntersecting) return; - observer.disconnect(); - - setShouldRender(true); - }); - observer.observe(memoContainerRef.current); - - return () => observer.disconnect(); - }, [lazyRendering, filterStore.state]); - - if (!shouldRender) { - // Render a placeholder to occupy the space. - return
; - } - - const handleGotoMemoDetailPage = (event: React.MouseEvent) => { - if (event.altKey) { - showChangeMemoCreatedTsDialog(memo.id); - } else { - navigateTo(`/m/${memo.id}`); - } - }; - - const handleTogglePinMemoBtnClick = async () => { - try { - if (memo.pinned) { - await memoStore.unpinMemo(memo.id); - } else { - await memoStore.pinMemo(memo.id); - } - } catch (error) { - // do nth - } - }; - - const handleEditMemoClick = () => { - showMemoEditorDialog({ - memoId: memo.id, - }); - }; - - const handleMarkMemoClick = () => { - showMemoEditorDialog({ - relationList: [ - { - memoId: UNKNOWN_ID, - relatedMemoId: memo.id, - type: "REFERENCE", - }, - ], - }); - }; - - const handleArchiveMemoClick = async () => { - try { - await memoStore.patchMemo({ - id: memo.id, - rowStatus: "ARCHIVED", - }); - } catch (error: any) { - console.error(error); - toast.error(error.response.data.message); - } - }; - - const handleDeleteMemoClick = async () => { - showCommonDialog({ - title: t("memo.delete-memo"), - content: t("memo.delete-confirm"), - style: "danger", - dialogName: "delete-memo-dialog", - onConfirm: async () => { - await memoStore.deleteMemoById(memo.id); - }, - }); - }; - - const handleMemoContentClick = async (e: React.MouseEvent) => { - const targetEl = e.target as HTMLElement; - - if (targetEl.className === "tag-span") { - const tagName = targetEl.innerText.slice(1); - const currTagQuery = filterStore.getState().tag; - if (currTagQuery === tagName) { - filterStore.setTagFilter(undefined); - } else { - filterStore.setTagFilter(tagName); - } - } else if (targetEl.classList.contains("todo-block")) { - if (readonly) { - return; - } - - const status = targetEl.dataset?.value; - const todoElementList = [...(memoContainerRef.current?.querySelectorAll(`span.todo-block[data-value=${status}]`) ?? [])]; - for (const element of todoElementList) { - if (element === targetEl) { - const index = todoElementList.indexOf(element); - const tempList = memo.content.split(status === "DONE" ? /- \[x\] / : /- \[ \] /); - let finalContent = ""; - - for (let i = 0; i < tempList.length; i++) { - if (i === 0) { - finalContent += `${tempList[i]}`; - } else { - if (i === index + 1) { - finalContent += status === "DONE" ? "- [ ] " : "- [x] "; - } else { - finalContent += status === "DONE" ? "- [x] " : "- [ ] "; - } - finalContent += `${tempList[i]}`; - } - } - await memoStore.patchMemo({ - id: memo.id, - content: finalContent, - }); - } - } - } else if (targetEl.tagName === "IMG") { - const imgUrl = targetEl.getAttribute("src"); - if (imgUrl) { - showPreviewImageDialog([imgUrl], 0); - } - } - }; - - return ( -
-
-
- {props.showCreator && creator && ( - <> - - - - - - {creator.nickname || extractUsernameFromName(creator.name)} - - - - - - - )} - - {displayTime} - - {props.showPinnedStyle && memo.pinned && ( - <> - - - - - - )} -
- - - - #{memo.id} - - - {props.showVisibility && memo.visibility !== "PRIVATE" && ( - <> - - - - - - - - )} -
-
-
- {!readonly && ( - <> - - - -
-
- {!memo.parent && ( - - {memo.pinned ? : } - {memo.pinned ? t("common.unpin") : t("common.pin")} - - )} - - - {t("common.edit")} - - {!memo.parent && ( - - - {t("common.mark")} - - )} - showShareMemoDialog(memo)}> - - {t("common.share")} - - - - - {t("common.archive")} - - - - {t("common.delete")} - -
-
- - )} -
-
- {props.showParent && memo.parent && ( -
- - - #{memo.parent.id} - {memo.parent.content} - -
- )} - - - -
- ); -}; - -export default memo(Memo); diff --git a/web/src/components/MemoEditor/ActionButton/TagSelector.tsx b/web/src/components/MemoEditor/ActionButton/TagSelector.tsx deleted file mode 100644 index f9cbbd97b..000000000 --- a/web/src/components/MemoEditor/ActionButton/TagSelector.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { IconButton } from "@mui/joy"; -import { useEffect } from "react"; -import Icon from "@/components/Icon"; -import OverflowTip from "@/components/kit/OverflowTip"; -import { useTagStore } from "@/store/module"; - -interface Props { - onTagSelectorClick: (tag: string) => void; -} - -const TagSelector = (props: Props) => { - const { onTagSelectorClick } = props; - const tagStore = useTagStore(); - const tags = tagStore.state.tags; - - useEffect(() => { - (async () => { - try { - await tagStore.fetchTags(); - } catch (error) { - // do nothing. - } - })(); - }, []); - - return ( - - -
- {tags.length > 0 ? ( - tags.map((tag) => { - return ( -
onTagSelectorClick(tag)} - key={tag} - > - #{tag} -
- ); - }) - ) : ( -

e.stopPropagation()}> - No tags found -

- )} -
-
- ); -}; - -export default TagSelector; diff --git a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx b/web/src/components/MemoEditor/Editor/TagSuggestions.tsx deleted file mode 100644 index 7ba64b140..000000000 --- a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import classNames from "classnames"; -import { useEffect, useRef, useState } from "react"; -import getCaretCoordinates from "textarea-caret"; -import OverflowTip from "@/components/kit/OverflowTip"; -import { useTagStore } from "@/store/module"; -import { EditorRefActions } from "."; - -type Props = { - editorRef: React.RefObject; - editorActions: React.ForwardedRef; -}; - -type Position = { left: number; top: number; height: number }; - -const TagSuggestions = ({ editorRef, editorActions }: Props) => { - const [position, setPosition] = useState(null); - const hide = () => setPosition(null); - - const { state } = useTagStore(); - const tagsRef = useRef(state.tags); - tagsRef.current = state.tags; - - const [selected, select] = useState(0); - const selectedRef = useRef(selected); - selectedRef.current = selected; - - const getCurrentWord = (): [word: string, startIndex: number] => { - const editor = editorRef.current; - if (!editor) return ["", 0]; - const cursorPos = editor.selectionEnd; - const before = editor.value.slice(0, cursorPos).match(/\S*$/) || { 0: "", index: cursorPos }; - const after = editor.value.slice(cursorPos).match(/^\S*/) || { 0: "" }; - return [before[0] + after[0], before.index ?? cursorPos]; - }; - - const suggestionsRef = useRef([]); - suggestionsRef.current = (() => { - const input = getCurrentWord()[0].slice(1).toLowerCase(); - - const customMatches = (tag: string, input: string) => { - const tagLowerCase = tag.toLowerCase(); - const inputLowerCase = input.toLowerCase(); - let inputIndex = 0; - - for (let i = 0; i < tagLowerCase.length; i++) { - if (tagLowerCase[i] === inputLowerCase[inputIndex]) { - inputIndex++; - if (inputIndex === inputLowerCase.length) { - return true; - } - } - } - - return false; - }; - - const matchedTags = tagsRef.current.filter((tag) => customMatches(tag, input)); - return matchedTags.slice(0, 5); - })(); - - const isVisibleRef = useRef(false); - isVisibleRef.current = !!(position && suggestionsRef.current.length > 0); - - const autocomplete = (tag: string) => { - if (!editorActions || !("current" in editorActions) || !editorActions.current) return; - const [word, index] = getCurrentWord(); - editorActions.current.removeText(index, word.length); - editorActions.current.insertText(`#${tag}`); - hide(); - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (!isVisibleRef.current) return; - const suggestions = suggestionsRef.current; - const selected = selectedRef.current; - if (["Escape", "ArrowLeft", "ArrowRight"].includes(e.code)) hide(); - if ("ArrowDown" === e.code) { - select((selected + 1) % suggestions.length); - e.preventDefault(); - e.stopPropagation(); - } - if ("ArrowUp" === e.code) { - select((selected - 1 + suggestions.length) % suggestions.length); - e.preventDefault(); - e.stopPropagation(); - } - if (["Enter", "Tab"].includes(e.code)) { - autocomplete(suggestions[selected]); - e.preventDefault(); - e.stopPropagation(); - } - }; - - const handleInput = () => { - if (!editorRef.current) return; - select(0); - const [word, index] = getCurrentWord(); - const isActive = word.startsWith("#") && !word.slice(1).includes("#"); - isActive ? setPosition(getCaretCoordinates(editorRef.current, index)) : hide(); - }; - - const listenersAreRegisteredRef = useRef(false); - const registerListeners = () => { - const editor = editorRef.current; - if (!editor || listenersAreRegisteredRef.current) return; - editor.addEventListener("click", hide); - editor.addEventListener("blur", hide); - editor.addEventListener("keydown", handleKeyDown); - editor.addEventListener("input", handleInput); - listenersAreRegisteredRef.current = true; - }; - useEffect(registerListeners, [!!editorRef.current]); - - if (!isVisibleRef.current || !position) return null; - return ( -
- {suggestionsRef.current.map((tag, i) => ( -
autocomplete(tag)} - className={classNames( - "rounded p-1 px-2 w-full truncate text-sm dark:text-gray-300 cursor-pointer hover:bg-zinc-300 dark:hover:bg-zinc-700", - i === selected ? "bg-zinc-300 dark:bg-zinc-700" : "" - )} - > - #{tag} -
- ))} -
- ); -}; - -export default TagSuggestions; diff --git a/web/src/components/MemoEditor/Editor/index.tsx b/web/src/components/MemoEditor/Editor/index.tsx deleted file mode 100644 index 41fa11eb8..000000000 --- a/web/src/components/MemoEditor/Editor/index.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import classNames from "classnames"; -import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef } from "react"; -import TagSuggestions from "./TagSuggestions"; - -export interface EditorRefActions { - focus: FunctionType; - scrollToCursor: FunctionType; - insertText: (text: string, prefix?: string, suffix?: string) => void; - removeText: (start: number, length: number) => void; - setContent: (text: string) => void; - getContent: () => string; - getSelectedContent: () => string; - getCursorPosition: () => number; - setCursorPosition: (startPos: number, endPos?: number) => void; - getCursorLineNumber: () => number; - getLine: (lineNumber: number) => string; - setLine: (lineNumber: number, text: string) => void; -} - -interface Props { - className: string; - initialContent: string; - placeholder: string; - tools?: ReactNode; - onContentChange: (content: string) => void; - onPaste: (event: React.ClipboardEvent) => void; -} - -const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef) { - const { className, initialContent, placeholder, onPaste, onContentChange: handleContentChangeCallback } = props; - const editorRef = useRef(null); - - useEffect(() => { - if (editorRef.current && initialContent) { - editorRef.current.value = initialContent; - handleContentChangeCallback(initialContent); - } - }, []); - - useEffect(() => { - if (editorRef.current) { - updateEditorHeight(); - } - }, [editorRef.current?.value]); - - const updateEditorHeight = () => { - if (editorRef.current) { - editorRef.current.style.height = "auto"; - editorRef.current.style.height = (editorRef.current.scrollHeight ?? 0) + "px"; - } - }; - - useImperativeHandle( - ref, - () => ({ - focus: () => { - editorRef.current?.focus(); - }, - scrollToCursor: () => { - if (editorRef.current) { - editorRef.current.scrollTop = editorRef.current.scrollHeight; - } - }, - insertText: (content = "", prefix = "", suffix = "") => { - if (!editorRef.current) { - return; - } - - const cursorPosition = editorRef.current.selectionStart; - const endPosition = editorRef.current.selectionEnd; - const prevValue = editorRef.current.value; - const value = - prevValue.slice(0, cursorPosition) + - prefix + - (content || prevValue.slice(cursorPosition, endPosition)) + - suffix + - prevValue.slice(endPosition); - - editorRef.current.value = value; - editorRef.current.focus(); - editorRef.current.selectionEnd = endPosition + prefix.length + content.length; - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - }, - removeText: (start: number, length: number) => { - if (!editorRef.current) { - return; - } - - const prevValue = editorRef.current.value; - const value = prevValue.slice(0, start) + prevValue.slice(start + length); - editorRef.current.value = value; - editorRef.current.focus(); - editorRef.current.selectionEnd = start; - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - }, - setContent: (text: string) => { - if (editorRef.current) { - editorRef.current.value = text; - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - } - }, - getContent: (): string => { - return editorRef.current?.value ?? ""; - }, - getCursorPosition: (): number => { - return editorRef.current?.selectionStart ?? 0; - }, - getSelectedContent: () => { - const start = editorRef.current?.selectionStart; - const end = editorRef.current?.selectionEnd; - return editorRef.current?.value.slice(start, end) ?? ""; - }, - setCursorPosition: (startPos: number, endPos?: number) => { - const _endPos = isNaN(endPos as number) ? startPos : (endPos as number); - editorRef.current?.setSelectionRange(startPos, _endPos); - }, - getCursorLineNumber: () => { - const cursorPosition = editorRef.current?.selectionStart ?? 0; - const lines = editorRef.current?.value.slice(0, cursorPosition).split("\n") ?? []; - return lines.length - 1; - }, - getLine: (lineNumber: number) => { - return editorRef.current?.value.split("\n")[lineNumber] ?? ""; - }, - setLine: (lineNumber: number, text: string) => { - const lines = editorRef.current?.value.split("\n") ?? []; - lines[lineNumber] = text; - if (editorRef.current) { - editorRef.current.value = lines.join("\n"); - editorRef.current.focus(); - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - } - }, - }), - [] - ); - - const handleEditorInput = useCallback(() => { - handleContentChangeCallback(editorRef.current?.value ?? ""); - updateEditorHeight(); - }, []); - - return ( -
- - -
- ); -}); - -export default Editor; diff --git a/web/src/components/MemoEditor/MemoEditorDialog.tsx b/web/src/components/MemoEditor/MemoEditorDialog.tsx deleted file mode 100644 index 57e878436..000000000 --- a/web/src/components/MemoEditor/MemoEditorDialog.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useEffect } from "react"; -import { useGlobalStore, useTagStore } from "@/store/module"; -import MemoEditor from "."; -import { generateDialog } from "../Dialog"; -import Icon from "../Icon"; - -interface Props extends DialogProps { - memoId?: MemoId; - relationList?: MemoRelation[]; -} - -const MemoEditorDialog: React.FC = ({ memoId, relationList, destroy }: Props) => { - const globalStore = useGlobalStore(); - const tagStore = useTagStore(); - const { systemStatus } = globalStore.state; - - useEffect(() => { - tagStore.fetchTags(); - }, []); - - const handleCloseBtnClick = () => { - destroy(); - }; - - return ( - <> -
-
- -

{systemStatus.customizedProfile.name}

-
- -
-
- -
- - ); -}; - -export default function showMemoEditorDialog(props: Pick = {}): void { - generateDialog( - { - className: "memo-editor-dialog", - dialogName: "memo-editor-dialog", - containerClassName: "dark:!bg-zinc-700", - }, - MemoEditorDialog, - props - ); -} diff --git a/web/src/components/MemoEditor/RelationListView.tsx b/web/src/components/MemoEditor/RelationListView.tsx deleted file mode 100644 index f9aabcb7d..000000000 --- a/web/src/components/MemoEditor/RelationListView.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect, useState } from "react"; -import { useMemoCacheStore } from "@/store/v1"; -import Icon from "../Icon"; - -interface Props { - relationList: MemoRelation[]; - setRelationList: (relationList: MemoRelation[]) => void; -} - -const RelationListView = (props: Props) => { - const { relationList, setRelationList } = props; - const memoCacheStore = useMemoCacheStore(); - const [referencingMemoList, setReferencingMemoList] = useState([]); - - useEffect(() => { - (async () => { - const requests = relationList - .filter((relation) => relation.type === "REFERENCE") - .map(async (relation) => { - return await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId); - }); - const list = await Promise.all(requests); - setReferencingMemoList(list); - })(); - }, [relationList]); - - const handleDeleteRelation = async (memo: Memo) => { - setRelationList(relationList.filter((relation) => relation.relatedMemoId !== memo.id)); - }; - - return ( - <> - {referencingMemoList.length > 0 && ( -
- {referencingMemoList.map((memo) => { - return ( -
handleDeleteRelation(memo)} - > - - #{memo.id} - {memo.content} - -
- ); - })} -
- )} - - ); -}; - -export default RelationListView; diff --git a/web/src/components/MemoEditor/ResourceListView.tsx b/web/src/components/MemoEditor/ResourceListView.tsx deleted file mode 100644 index be9af0897..000000000 --- a/web/src/components/MemoEditor/ResourceListView.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Resource } from "@/types/proto/api/v2/resource_service"; -import Icon from "../Icon"; -import ResourceIcon from "../ResourceIcon"; - -interface Props { - resourceList: Resource[]; - setResourceList: (resourceList: Resource[]) => void; -} - -const ResourceListView = (props: Props) => { - const { resourceList, setResourceList } = props; - - const handleDeleteResource = async (resourceId: ResourceId) => { - setResourceList(resourceList.filter((resource) => resource.id !== resourceId)); - }; - - return ( - <> - {resourceList.length > 0 && ( -
- {resourceList.map((resource) => { - return ( -
- - {resource.filename} - handleDeleteResource(resource.id)} - /> -
- ); - })} -
- )} - - ); -}; - -export default ResourceListView; diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx deleted file mode 100644 index 847ea6681..000000000 --- a/web/src/components/MemoEditor/index.tsx +++ /dev/null @@ -1,488 +0,0 @@ -import { Select, Option, Button, IconButton, Divider } from "@mui/joy"; -import { isNumber, last, uniqBy } from "lodash-es"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { toast } from "react-hot-toast"; -import { useTranslation } from "react-i18next"; -import useLocalStorage from "react-use/lib/useLocalStorage"; -import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; -import useCurrentUser from "@/hooks/useCurrentUser"; -import { useGlobalStore, useMemoStore, useResourceStore } from "@/store/module"; -import { useUserV1Store } from "@/store/v1"; -import { Resource } from "@/types/proto/api/v2/resource_service"; -import { UserSetting, User_Role } from "@/types/proto/api/v2/user_service"; -import { useTranslate } from "@/utils/i18n"; -import showCreateMemoRelationDialog from "../CreateMemoRelationDialog"; -import showCreateResourceDialog from "../CreateResourceDialog"; -import Icon from "../Icon"; -import VisibilityIcon from "../VisibilityIcon"; -import TagSelector from "./ActionButton/TagSelector"; -import Editor, { EditorRefActions } from "./Editor"; -import RelationListView from "./RelationListView"; -import ResourceListView from "./ResourceListView"; - -const listItemSymbolList = ["- [ ] ", "- [x] ", "- [X] ", "* ", "- "]; -const emptyOlReg = /^(\d+)\. $/; - -interface Props { - className?: string; - editorClassName?: string; - cacheKey?: string; - memoId?: MemoId; - relationList?: MemoRelation[]; - onConfirm?: () => void; -} - -interface State { - memoVisibility: Visibility; - resourceList: Resource[]; - relationList: MemoRelation[]; - isUploadingResource: boolean; - isRequesting: boolean; -} - -const MemoEditor = (props: Props) => { - const { className, editorClassName, cacheKey, memoId, onConfirm } = props; - const { i18n } = useTranslation(); - const t = useTranslate(); - const contentCacheKey = `memo-editor-${cacheKey}`; - const [contentCache, setContentCache] = useLocalStorage(contentCacheKey, ""); - const { - state: { systemStatus }, - } = useGlobalStore(); - const userV1Store = useUserV1Store(); - const memoStore = useMemoStore(); - const resourceStore = useResourceStore(); - const currentUser = useCurrentUser(); - const [state, setState] = useState({ - memoVisibility: "PRIVATE", - resourceList: [], - relationList: props.relationList ?? [], - isUploadingResource: false, - isRequesting: false, - }); - const [hasContent, setHasContent] = useState(false); - const [isInIME, setIsInIME] = useState(false); - const editorRef = useRef(null); - const userSetting = userV1Store.userSetting as UserSetting; - const referenceRelations = memoId - ? state.relationList.filter( - (relation) => relation.memoId === memoId && relation.relatedMemoId !== memoId && relation.type === "REFERENCE" - ) - : state.relationList.filter((relation) => relation.type === "REFERENCE"); - - useEffect(() => { - editorRef.current?.setContent(contentCache || ""); - }, []); - - useEffect(() => { - let visibility = userSetting.memoVisibility; - if (systemStatus.disablePublicMemos && visibility === "PUBLIC") { - visibility = "PRIVATE"; - } - setState((prevState) => ({ - ...prevState, - memoVisibility: visibility as Visibility, - })); - }, [userSetting.memoVisibility, systemStatus.disablePublicMemos]); - - useEffect(() => { - if (memoId) { - memoStore.getMemoById(memoId ?? UNKNOWN_ID).then((memo) => { - if (memo) { - handleEditorFocus(); - setState((prevState) => ({ - ...prevState, - memoVisibility: memo.visibility, - resourceList: memo.resourceList, - relationList: memo.relationList, - })); - if (!contentCache) { - editorRef.current?.setContent(memo.content ?? ""); - } - } - }); - } - }, [memoId]); - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (!editorRef.current) { - return; - } - - const isMetaKey = event.ctrlKey || event.metaKey; - if (isMetaKey) { - if (event.key === "Enter") { - handleSaveBtnClick(); - return; - } - } - if (event.key === "Enter" && !isInIME) { - const cursorPosition = editorRef.current.getCursorPosition(); - const contentBeforeCursor = editorRef.current.getContent().slice(0, cursorPosition); - const rowValue = last(contentBeforeCursor.split("\n")); - if (rowValue) { - if (listItemSymbolList.includes(rowValue) || emptyOlReg.test(rowValue)) { - event.preventDefault(); - editorRef.current.removeText(cursorPosition - rowValue.length, rowValue.length); - } else { - // unordered/todo list - let matched = false; - for (const listItemSymbol of listItemSymbolList) { - if (rowValue.startsWith(listItemSymbol)) { - event.preventDefault(); - editorRef.current.insertText("", `\n${listItemSymbol}`); - matched = true; - break; - } - } - - if (!matched) { - // ordered list - const olMatchRes = /^(\d+)\. /.exec(rowValue); - if (olMatchRes) { - const order = parseInt(olMatchRes[1]); - if (isNumber(order)) { - event.preventDefault(); - editorRef.current.insertText("", `\n${order + 1}. `); - } - } - } - editorRef.current?.scrollToCursor(); - } - } - return; - } - if (event.key === "Tab") { - event.preventDefault(); - const tabSpace = " ".repeat(TAB_SPACE_WIDTH); - const cursorPosition = editorRef.current.getCursorPosition(); - const selectedContent = editorRef.current.getSelectedContent(); - editorRef.current.insertText(tabSpace); - if (selectedContent) { - editorRef.current.setCursorPosition(cursorPosition + TAB_SPACE_WIDTH); - } - return; - } - }; - - const handleMemoVisibilityChange = (visibility: Visibility) => { - setState((prevState) => ({ - ...prevState, - memoVisibility: visibility, - })); - }; - - const handleUploadFileBtnClick = () => { - showCreateResourceDialog({ - onConfirm: (resourceList) => { - setState((prevState) => ({ - ...prevState, - resourceList: [...prevState.resourceList, ...resourceList], - })); - }, - }); - }; - - const handleAddMemoRelationBtnClick = () => { - showCreateMemoRelationDialog({ - onConfirm: (memoIdList) => { - setState((prevState) => ({ - ...prevState, - relationList: uniqBy( - [ - ...memoIdList.map((id) => ({ memoId: memoId || UNKNOWN_ID, relatedMemoId: id, type: "REFERENCE" as MemoRelationType })), - ...state.relationList, - ].filter((relation) => relation.relatedMemoId !== (memoId || UNKNOWN_ID)), - "relatedMemoId" - ), - })); - }, - }); - }; - - const handleSetResourceList = (resourceList: Resource[]) => { - setState((prevState) => ({ - ...prevState, - resourceList, - })); - }; - - const handleSetRelationList = (relationList: MemoRelation[]) => { - setState((prevState) => ({ - ...prevState, - relationList, - })); - }; - - const handleUploadResource = async (file: File) => { - setState((state) => { - return { - ...state, - isUploadingResource: true, - }; - }); - - let resource = undefined; - try { - resource = await resourceStore.createResourceWithBlob(file); - } catch (error: any) { - console.error(error); - toast.error(typeof error === "string" ? error : error.response.data.message); - } - - setState((state) => { - return { - ...state, - isUploadingResource: false, - }; - }); - return resource; - }; - - const uploadMultiFiles = async (files: FileList) => { - const uploadedResourceList: Resource[] = []; - for (const file of files) { - const resource = await handleUploadResource(file); - if (resource) { - uploadedResourceList.push(resource); - if (memoId) { - await resourceStore.updateResource({ - resource: Resource.fromPartial({ - id: resource.id, - memoId, - }), - updateMask: ["memo_id"], - }); - } - } - } - if (uploadedResourceList.length > 0) { - setState((prevState) => ({ - ...prevState, - resourceList: [...prevState.resourceList, ...uploadedResourceList], - })); - } - }; - - const handleDropEvent = async (event: React.DragEvent) => { - if (event.dataTransfer && event.dataTransfer.files.length > 0) { - event.preventDefault(); - await uploadMultiFiles(event.dataTransfer.files); - } - }; - - const handlePasteEvent = async (event: React.ClipboardEvent) => { - if (event.clipboardData && event.clipboardData.files.length > 0) { - event.preventDefault(); - await uploadMultiFiles(event.clipboardData.files); - } - }; - - const handleContentChange = (content: string) => { - setHasContent(content !== ""); - if (content !== "") { - setContentCache(content); - } else { - localStorage.removeItem(contentCacheKey); - } - }; - - const handleSaveBtnClick = async () => { - if (state.isRequesting) { - return; - } - - setState((state) => { - return { - ...state, - isRequesting: true, - }; - }); - const content = editorRef.current?.getContent() ?? ""; - try { - if (memoId && memoId !== UNKNOWN_ID) { - const prevMemo = await memoStore.getMemoById(memoId ?? UNKNOWN_ID); - - if (prevMemo) { - await memoStore.patchMemo({ - id: prevMemo.id, - content, - visibility: state.memoVisibility, - resourceIdList: state.resourceList.map((resource) => resource.id), - relationList: state.relationList, - }); - } - } else { - await memoStore.createMemo({ - content, - visibility: state.memoVisibility, - resourceIdList: state.resourceList.map((resource) => resource.id), - relationList: state.relationList, - }); - } - editorRef.current?.setContent(""); - } catch (error: any) { - console.error(error); - toast.error(error.response.data.message); - } - setState((state) => { - return { - ...state, - isRequesting: false, - }; - }); - - setState((prevState) => ({ - ...prevState, - resourceList: [], - })); - if (onConfirm) { - onConfirm(); - } - }; - - const handleCheckBoxBtnClick = () => { - if (!editorRef.current) { - return; - } - const currentPosition = editorRef.current?.getCursorPosition(); - const currentLineNumber = editorRef.current?.getCursorLineNumber(); - const currentLine = editorRef.current?.getLine(currentLineNumber); - let newLine = ""; - let cursorChange = 0; - if (/^- \[( |x|X)\] /.test(currentLine)) { - newLine = currentLine.replace(/^- \[( |x|X)\] /, ""); - cursorChange = -6; - } else if (/^\d+\. |- /.test(currentLine)) { - const match = currentLine.match(/^\d+\. |- /) ?? [""]; - newLine = currentLine.replace(/^\d+\. |- /, "- [ ] "); - cursorChange = -match[0].length + 6; - } else { - newLine = "- [ ] " + currentLine; - cursorChange = 6; - } - editorRef.current?.setLine(currentLineNumber, newLine); - editorRef.current.setCursorPosition(currentPosition + cursorChange); - editorRef.current?.scrollToCursor(); - }; - - const handleCodeBlockBtnClick = () => { - if (!editorRef.current) { - return; - } - - const cursorPosition = editorRef.current.getCursorPosition(); - const prevValue = editorRef.current.getContent().slice(0, cursorPosition); - if (prevValue === "" || prevValue.endsWith("\n")) { - editorRef.current?.insertText("", "```\n", "\n```"); - } else { - editorRef.current?.insertText("", "\n```\n", "\n```"); - } - editorRef.current?.scrollToCursor(); - }; - - const handleTagSelectorClick = useCallback((tag: string) => { - editorRef.current?.insertText(`#${tag} `); - }, []); - - const handleEditorFocus = () => { - editorRef.current?.focus(); - }; - - const editorConfig = useMemo( - () => ({ - className: editorClassName ?? "", - initialContent: "", - placeholder: t("editor.placeholder"), - onContentChange: handleContentChange, - onPaste: handlePasteEvent, - }), - [i18n.language] - ); - - const allowSave = (hasContent || state.resourceList.length > 0) && !state.isUploadingResource && !state.isRequesting; - - const disableOption = (v: string) => { - const isAdminOrHost = currentUser.role === User_Role.ADMIN || currentUser.role === User_Role.HOST; - - if (v === "PUBLIC" && !isAdminOrHost) { - return systemStatus.disablePublicMemos; - } - return false; - }; - - return ( -
setIsInIME(true)} - onCompositionEnd={() => setIsInIME(false)} - > - -
-
- handleTagSelectorClick(tag)} /> - - - - - - - - - - - - -
-
- - - -
-
e.stopPropagation()}> - -
-
- -
-
-
- ); -}; - -export default MemoEditor; diff --git a/web/src/components/MemoEditorV1/MemoEditorDialog.tsx b/web/src/components/MemoEditorV1/MemoEditorDialog.tsx index c01fde64c..74b6c479e 100644 --- a/web/src/components/MemoEditorV1/MemoEditorDialog.tsx +++ b/web/src/components/MemoEditorV1/MemoEditorDialog.tsx @@ -6,7 +6,7 @@ import { generateDialog } from "../Dialog"; import Icon from "../Icon"; interface Props extends DialogProps { - memoId?: MemoId; + memoId?: number; relationList?: MemoRelation[]; } diff --git a/web/src/components/MemoEditorV1/RelationListView.tsx b/web/src/components/MemoEditorV1/RelationListView.tsx index 8eb8e5c50..fb8e659ad 100644 --- a/web/src/components/MemoEditorV1/RelationListView.tsx +++ b/web/src/components/MemoEditorV1/RelationListView.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; -import { useMemoCacheStore } from "@/store/v1"; +import { useMemoV1Store } from "@/store/v1"; import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service"; +import { Memo } from "@/types/proto/api/v2/memo_service"; import Icon from "../Icon"; interface Props { @@ -10,7 +11,7 @@ interface Props { const RelationListView = (props: Props) => { const { relationList, setRelationList } = props; - const memoCacheStore = useMemoCacheStore(); + const memoStore = useMemoV1Store(); const [referencingMemoList, setReferencingMemoList] = useState([]); useEffect(() => { @@ -18,7 +19,7 @@ const RelationListView = (props: Props) => { const requests = relationList .filter((relation) => relation.type === MemoRelation_Type.REFERENCE) .map(async (relation) => { - return await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId); + return await memoStore.getOrFetchMemoById(relation.relatedMemoId); }); const list = await Promise.all(requests); setReferencingMemoList(list); diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx deleted file mode 100644 index 87de0d2bb..000000000 --- a/web/src/components/MemoList.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { useEffect, useRef } from "react"; -import { toast } from "react-hot-toast"; -import { useParams } from "react-router-dom"; -import MemoFilter from "@/components/MemoFilter"; -import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; -import { getTimeStampByDate } from "@/helpers/datetime"; -import useCurrentUser from "@/hooks/useCurrentUser"; -import { useFilterStore, useMemoStore } from "@/store/module"; -import { extractUsernameFromName } from "@/store/v1"; -import { useTranslate } from "@/utils/i18n"; -import { TAG_REG } from "@/utils/tag"; -import Empty from "./Empty"; -import Memo from "./Memo"; - -const MemoList: React.FC = () => { - const t = useTranslate(); - const params = useParams(); - const memoStore = useMemoStore(); - const filterStore = useFilterStore(); - const filter = filterStore.state; - const { loadingStatus, memos } = memoStore.state; - const user = useCurrentUser(); - const { tag: tagQuery, duration, text: textQuery, visibility } = filter; - const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || textQuery || visibility); - const username = params.username || extractUsernameFromName(user.name); - - const fetchMoreRef = useRef(null); - - const shownMemos = ( - showMemoFilter - ? memos.filter((memo) => { - let shouldShow = true; - - if (tagQuery) { - const tagsSet = new Set(); - for (const t of Array.from(memo.content.match(new RegExp(TAG_REG, "gu")) ?? [])) { - const tag = t.replace(TAG_REG, "$1").trim(); - const items = tag.split("/"); - let temp = ""; - for (const i of items) { - temp += i; - tagsSet.add(temp); - temp += "/"; - } - } - if (!tagsSet.has(tagQuery)) { - shouldShow = false; - } - } - if ( - duration && - duration.from < duration.to && - (getTimeStampByDate(memo.displayTs) < duration.from || getTimeStampByDate(memo.displayTs) > duration.to) - ) { - shouldShow = false; - } - if (textQuery && !memo.content.toLowerCase().includes(textQuery.toLowerCase())) { - shouldShow = false; - } - if (visibility) { - shouldShow = memo.visibility === visibility; - } - - return shouldShow; - }) - : memos - ).filter((memo) => memo.creatorUsername === username && memo.rowStatus === "NORMAL"); - - const pinnedMemos = shownMemos.filter((m) => m.pinned); - const unpinnedMemos = shownMemos.filter((m) => !m.pinned); - const memoSort = (mi: Memo, mj: Memo) => { - return mj.displayTs - mi.displayTs; - }; - pinnedMemos.sort(memoSort); - unpinnedMemos.sort(memoSort); - const sortedMemos = pinnedMemos.concat(unpinnedMemos).filter((m) => m.rowStatus === "NORMAL"); - - useEffect(() => { - const root = document.body.querySelector("#root"); - if (root) { - root.scrollTo(0, 0); - } - }, [filter]); - - useEffect(() => { - memoStore.setLoadingStatus("incomplete"); - }, []); - - useEffect(() => { - if (!fetchMoreRef.current) return; - - const observer = new IntersectionObserver(([entry]) => { - if (!entry.isIntersecting) return; - observer.disconnect(); - handleFetchMoreClick(); - }); - observer.observe(fetchMoreRef.current); - - return () => observer.disconnect(); - }, [loadingStatus]); - - const handleFetchMoreClick = async () => { - try { - await memoStore.fetchMemos(username, DEFAULT_MEMO_LIMIT, memos.length); - } catch (error: any) { - toast.error(error.response.data.message); - } - }; - - return ( -
- - {sortedMemos.map((memo) => ( - - ))} - - {loadingStatus === "fetching" ? ( -
-

{t("memo.fetching-data")}

-
- ) : ( -
-
- {loadingStatus === "complete" ? ( - sortedMemos.length === 0 && ( -
- -

{t("message.no-data")}

-
- ) - ) : ( - - {t("memo.fetch-more")} - - )} -
-
- )} -
- ); -}; - -export default MemoList; diff --git a/web/src/components/MemoRelationListView.tsx b/web/src/components/MemoRelationListView.tsx deleted file mode 100644 index a7f186b0f..000000000 --- a/web/src/components/MemoRelationListView.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Tooltip } from "@mui/joy"; -import { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; -import { useMemoCacheStore } from "@/store/v1"; -import Icon from "./Icon"; - -interface Props { - memo: Memo; - relationList: MemoRelation[]; -} - -const MemoRelationListView = (props: Props) => { - const { memo, relationList } = props; - const memoCacheStore = useMemoCacheStore(); - const [referencingMemoList, setReferencingMemoList] = useState([]); - const [referencedMemoList, setReferencedMemoList] = useState([]); - - useEffect(() => { - (async () => { - const referencingMemoList = await Promise.all( - relationList - .filter((relation) => relation.memoId === memo.id && relation.relatedMemoId !== memo.id) - .map((relation) => memoCacheStore.getOrFetchMemoById(relation.relatedMemoId)) - ); - setReferencingMemoList(referencingMemoList); - const referencedMemoList = await Promise.all( - relationList - .filter((relation) => relation.memoId !== memo.id && relation.relatedMemoId === memo.id) - .map((relation) => memoCacheStore.getOrFetchMemoById(relation.memoId)) - ); - setReferencedMemoList(referencedMemoList); - })(); - }, [memo, relationList]); - - return ( - <> - {referencingMemoList.length > 0 && ( -
- {referencingMemoList.map((memo) => { - return ( -
- - - - - #{memo.id} - {memo.content} - -
- ); - })} -
- )} - {referencedMemoList.length > 0 && ( -
- {referencedMemoList.map((memo) => { - return ( -
- - - - - #{memo.id} - {memo.content} - -
- ); - })} -
- )} - - ); -}; - -export default MemoRelationListView; diff --git a/web/src/components/MemoViewV1.tsx b/web/src/components/MemoViewV1.tsx index 3cda3a609..5d5250643 100644 --- a/web/src/components/MemoViewV1.tsx +++ b/web/src/components/MemoViewV1.tsx @@ -19,7 +19,7 @@ import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; import { showCommonDialog } from "./Dialog/CommonDialog"; import Icon from "./Icon"; import MemoContentV1 from "./MemoContentV1"; -import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog"; +import showMemoEditorDialog from "./MemoEditorV1/MemoEditorDialog"; import MemoRelationListViewV1 from "./MemoRelationListViewV1"; import MemoResourceListView from "./MemoResourceListView"; import showPreviewImageDialog from "./PreviewImageDialog"; @@ -56,23 +56,6 @@ const MemoViewV1: React.FC = (props: Props) => { const referenceRelations = memoRelations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); const readonly = memo.creator !== user?.name; - useEffect(() => { - memoStore.fetchMemoResources(memo.id).then((resources: Resource[]) => { - setResources(resources); - }); - memoStore.fetchMemoRelations(memo.id).then((relations: MemoRelation[]) => { - setMemoRelations(relations); - const parentMemoId = relations.find( - (relation) => relation.memoId === memo.id && relation.type === MemoRelation_Type.COMMENT - )?.relatedMemoId; - if (parentMemoId) { - memoStore.getOrFetchMemoById(parentMemoId).then((memo: Memo) => { - setParentMemo(memo); - }); - } - }); - }, []); - // Prepare memo creator. useEffect(() => { if (creator) return; @@ -113,6 +96,27 @@ const MemoViewV1: React.FC = (props: Props) => { return () => observer.disconnect(); }, [lazyRendering, filterStore.state]); + useEffect(() => { + if (!shouldRender) { + return; + } + + memoStore.fetchMemoResources(memo.id).then((resources: Resource[]) => { + setResources(resources); + }); + memoStore.fetchMemoRelations(memo.id).then((relations: MemoRelation[]) => { + setMemoRelations(relations); + const parentMemoId = relations.find( + (relation) => relation.memoId === memo.id && relation.type === MemoRelation_Type.COMMENT + )?.relatedMemoId; + if (parentMemoId) { + memoStore.getOrFetchMemoById(parentMemoId).then((memo: Memo) => { + setParentMemo(memo); + }); + } + }); + }, [shouldRender]); + if (!shouldRender) { // Render a placeholder to occupy the space. return
; @@ -162,7 +166,7 @@ const MemoViewV1: React.FC = (props: Props) => { { memoId: UNKNOWN_ID, relatedMemoId: memo.id, - type: "REFERENCE", + type: MemoRelation_Type.REFERENCE, }, ], }); diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx deleted file mode 100644 index d9b882457..000000000 --- a/web/src/components/ShareMemoDialog.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Button } from "@mui/joy"; -import copy from "copy-to-clipboard"; -import React, { useEffect, useRef } from "react"; -import { toast } from "react-hot-toast"; -import { getDateTimeString } from "@/helpers/datetime"; -import useLoading from "@/hooks/useLoading"; -import toImage from "@/labs/html2image"; -import { useUserV1Store, extractUsernameFromName } from "@/store/v1"; -import { useTranslate } from "@/utils/i18n"; -import { generateDialog } from "./Dialog"; -import Icon from "./Icon"; -import MemoContentV1 from "./MemoContentV1"; -import MemoResourceListView from "./MemoResourceListView"; -import UserAvatar from "./UserAvatar"; -import "@/less/share-memo-dialog.less"; - -interface Props extends DialogProps { - memo: Memo; -} - -const ShareMemoDialog: React.FC = (props: Props) => { - const { memo: propsMemo, destroy } = props; - const t = useTranslate(); - const userV1Store = useUserV1Store(); - const downloadingImageState = useLoading(false); - const loadingState = useLoading(); - const memoElRef = useRef(null); - const memo = { - ...propsMemo, - displayTsStr: getDateTimeString(propsMemo.displayTs), - }; - const user = userV1Store.getUserByUsername(memo.creatorUsername); - - useEffect(() => { - (async () => { - await userV1Store.getOrFetchUserByUsername(memo.creatorUsername); - loadingState.setFinish(); - })(); - }, []); - - const handleCloseBtnClick = () => { - destroy(); - }; - - const handleDownloadImageBtnClick = () => { - if (!memoElRef.current) { - return; - } - - downloadingImageState.setLoading(); - toImage(memoElRef.current, { - pixelRatio: window.devicePixelRatio * 2, - }) - .then((url) => { - const a = document.createElement("a"); - a.href = url; - a.download = `memos-${getDateTimeString(Date.now())}.png`; - a.click(); - - downloadingImageState.setFinish(); - }) - .catch((err) => { - console.error(err); - }); - }; - - const handleCopyLinkBtnClick = () => { - copy(`${window.location.origin}/m/${memo.id}`); - toast.success(t("message.succeed-copy-link")); - }; - - if (loadingState.isLoading) { - return null; - } - - return ( - <> -
-

{t("common.share")} Memo

- -
-
-
- - -
-
-
- {memo.displayTsStr} -
- - -
-
-
- -
- - {user.nickname || extractUsernameFromName(user.name)} - -
-
- via memos -
-
-
-
- - ); -}; - -export default function showShareMemoDialog(memo: Memo): void { - generateDialog( - { - className: "share-memo-dialog", - dialogName: "share-memo-dialog", - }, - ShareMemoDialog, - { memo } - ); -} diff --git a/web/src/components/UsageHeatMap.tsx b/web/src/components/UsageHeatMap.tsx index 346100625..e3aa9275f 100644 --- a/web/src/components/UsageHeatMap.tsx +++ b/web/src/components/UsageHeatMap.tsx @@ -5,9 +5,9 @@ import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers import * as utils from "@/helpers/utils"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useGlobalStore } from "@/store/module"; -import { useUserV1Store, extractUsernameFromName } from "@/store/v1"; +import { useUserV1Store, extractUsernameFromName, useMemoV1Store } from "@/store/v1"; import { useTranslate, Translations } from "@/utils/i18n"; -import { useFilterStore, useMemoStore } from "../store/module"; +import { useFilterStore } from "../store/module"; import "@/less/usage-heat-map.less"; const tableConfig = { @@ -36,7 +36,7 @@ const UsageHeatMap = () => { const filterStore = useFilterStore(); const userV1Store = useUserV1Store(); const user = useCurrentUser(); - const memoStore = useMemoStore(); + const memoStore = useMemoV1Store(); const todayTimeStamp = getDateStampByDate(Date.now()); const weekDay = new Date(todayTimeStamp).getDay(); const weekFromMonday = ["zh-Hans", "ko"].includes(useGlobalStore().state.locale); @@ -45,12 +45,12 @@ const UsageHeatMap = () => { const nullCell = new Array(7 - todayDay).fill(0); const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay; const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP; - const memos = memoStore.state.memos; const [memoAmount, setMemoAmount] = useState(0); const [createdDays, setCreatedDays] = useState(0); const [allStat, setAllStat] = useState(getInitialUsageStat(usedDaysAmount, beginDayTimestamp)); const [currentStat, setCurrentStat] = useState(null); const containerElRef = useRef(null); + const memos = Array.from(memoStore.getState().memoById.values()); useEffect(() => { userV1Store.getOrFetchUserByUsername(extractUsernameFromName(user.name)).then((user) => { diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 51d4699ff..c46111688 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -56,74 +56,10 @@ export function upsertUserSetting(upsert: UserSettingUpsert) { return axios.post(`/api/v1/user/setting`, upsert); } -export function getAllMemos(memoFind?: MemoFind) { - const queryList = []; - if (memoFind?.offset) { - queryList.push(`offset=${memoFind.offset}`); - } - if (memoFind?.limit) { - queryList.push(`limit=${memoFind.limit}`); - } - - if (memoFind?.creatorUsername) { - queryList.push(`creatorUsername=${memoFind.creatorUsername}`); - } - - return axios.get(`/api/v1/memo/all?${queryList.join("&")}`); -} - -export function getMemoList(memoFind?: MemoFind) { - const queryList = []; - if (memoFind?.creatorUsername) { - queryList.push(`creatorUsername=${memoFind.creatorUsername}`); - } - if (memoFind?.rowStatus) { - queryList.push(`rowStatus=${memoFind.rowStatus}`); - } - if (memoFind?.pinned) { - queryList.push(`pinned=${memoFind.pinned}`); - } - if (memoFind?.offset) { - queryList.push(`offset=${memoFind.offset}`); - } - if (memoFind?.limit) { - queryList.push(`limit=${memoFind.limit}`); - } - return axios.get(`/api/v1/memo?${queryList.join("&")}`); -} - export function getMemoStats(username: string) { return axios.get(`/api/v1/memo/stats?creatorUsername=${username}`); } -export function getMemoById(id: MemoId) { - return axios.get(`/api/v1/memo/${id}`); -} - -export function createMemo(memoCreate: MemoCreate) { - return axios.post("/api/v1/memo", memoCreate); -} - -export function patchMemo(memoPatch: MemoPatch) { - return axios.patch(`/api/v1/memo/${memoPatch.id}`, memoPatch); -} - -export function pinMemo(memoId: MemoId) { - return axios.post(`/api/v1/memo/${memoId}/organizer`, { - pinned: true, - }); -} - -export function unpinMemo(memoId: MemoId) { - return axios.post(`/api/v1/memo/${memoId}/organizer`, { - pinned: false, - }); -} - -export function deleteMemo(memoId: MemoId) { - return axios.delete(`/api/v1/memo/${memoId}`); -} - export function createResource(resourceCreate: ResourceCreate) { return axios.post("/api/v1/resource", resourceCreate); } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index c49d90f51..fa9f0a761 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -94,7 +94,7 @@ "archived-memos": "Archived Memos", "no-archived-memos": "No archived memos.", "fetching-data": "Fetching data…", - "fetch-more": "Click here to fetch more", + "fetch-more": "Click to fetch more", "archived-at": "Archived at", "search-placeholder": "Search memos", "visibility": { diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index ebfee8bcd..2c4ecffba 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -1,56 +1,42 @@ -import { useEffect, useRef } from "react"; -import { toast } from "react-hot-toast"; +import { useEffect, useState } from "react"; import Empty from "@/components/Empty"; -import Memo from "@/components/Memo"; import MemoFilter from "@/components/MemoFilter"; +import MemoViewV1 from "@/components/MemoViewV1"; import MobileHeader from "@/components/MobileHeader"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; -import { useFilterStore, useMemoStore } from "@/store/module"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { useFilterStore } from "@/store/module"; +import { useMemoV1Store } from "@/store/v1"; +import { Memo } from "@/types/proto/api/v2/memo_service"; import { useTranslate } from "@/utils/i18n"; const Explore = () => { const t = useTranslate(); + const user = useCurrentUser(); const filterStore = useFilterStore(); - const memoStore = useMemoStore(); - const filter = filterStore.state; - const { loadingStatus, memos } = memoStore.state; - const { text: textQuery } = filter; - const fetchMoreRef = useRef(null); - - const fetchedMemos = memos.filter((memo) => { - if (textQuery && !memo.content.toLowerCase().includes(textQuery.toLowerCase())) { - return false; - } - return true; - }); - - const sortedMemos = fetchedMemos - .filter((m) => m.rowStatus === "NORMAL" && m.visibility !== "PRIVATE") - .sort((mi, mj) => mj.displayTs - mi.displayTs); + const memoStore = useMemoV1Store(); + const [memos, setMemos] = useState([]); + const [isComplete, setIsComplete] = useState(false); + const [isRequesting, setIsRequesting] = useState(false); + const { tag: tagQuery, text: textQuery } = filterStore.state; useEffect(() => { - memoStore.setLoadingStatus("incomplete"); - }, []); + fetchMemos(); + }, [tagQuery, textQuery]); - useEffect(() => { - if (!fetchMoreRef.current) return; - - const observer = new IntersectionObserver(([entry]) => { - if (!entry.isIntersecting) return; - observer.disconnect(); - handleFetchMoreClick(); + const fetchMemos = async () => { + const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`]; + if (tagQuery) filters.push(`tags == "${tagQuery}"`); + if (textQuery) filters.push(`content_search == "${textQuery}"`); + setIsRequesting(true); + const data = await memoStore.fetchMemos({ + limit: DEFAULT_MEMO_LIMIT, + offset: memos.length, + filter: filters.join(" && "), }); - observer.observe(fetchMoreRef.current); - - return () => observer.disconnect(); - }, [loadingStatus]); - - const handleFetchMoreClick = async () => { - try { - await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, sortedMemos.length); - } catch (error: any) { - toast.error(error.response.data.message); - } + setIsRequesting(false); + setMemos([...memos, ...data]); + setIsComplete(data.length < DEFAULT_MEMO_LIMIT); }; return ( @@ -58,30 +44,27 @@ const Explore = () => {
- {sortedMemos.map((memo) => ( - + {memos.map((memo) => ( + ))} - {loadingStatus === "fetching" ? ( -
+ {isRequesting && ( +

{t("memo.fetching-data")}

- ) : ( -
-
- {loadingStatus === "complete" ? ( - sortedMemos.length === 0 && ( -
- -

{t("message.no-data")}

-
- ) - ) : ( - - {t("memo.fetch-more")} - - )} + )} + {isComplete ? ( + memos.length === 0 && ( +
+ +

{t("message.no-data")}

+ ) + ) : ( +
+ + {t("memo.fetch-more")} +
)}
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index cd561638b..8358e1e7a 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,20 +1,85 @@ +import { useEffect, useState } from "react"; +import Empty from "@/components/Empty"; import HomeSidebar from "@/components/HomeSidebar"; import HomeSidebarDrawer from "@/components/HomeSidebarDrawer"; -import MemoEditor from "@/components/MemoEditor"; -import MemoList from "@/components/MemoList"; +import MemoEditorV1 from "@/components/MemoEditorV1"; +import MemoFilter from "@/components/MemoFilter"; +import MemoViewV1 from "@/components/MemoViewV1"; import MobileHeader from "@/components/MobileHeader"; +import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; +import useCurrentUser from "@/hooks/useCurrentUser"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import { useFilterStore } from "@/store/module"; +import { useMemoV1Store } from "@/store/v1"; +import { Memo } from "@/types/proto/api/v2/memo_service"; +import { useTranslate } from "@/utils/i18n"; const Home = () => { + const t = useTranslate(); const { md } = useResponsiveWidth(); + const user = useCurrentUser(); + const filterStore = useFilterStore(); + const memoStore = useMemoV1Store(); + const [memos, setMemos] = useState([]); + const [isComplete, setIsComplete] = useState(false); + const [isRequesting, setIsRequesting] = useState(false); + const { tag: tagQuery, text: textQuery } = filterStore.state; + + useEffect(() => { + fetchMemos(); + }, [tagQuery, textQuery]); + + const fetchMemos = async () => { + const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`]; + if (tagQuery) filters.push(`tags == "${tagQuery}"`); + if (textQuery) filters.push(`content_search == "${textQuery}"`); + setIsRequesting(true); + const data = await memoStore.fetchMemos({ + limit: DEFAULT_MEMO_LIMIT, + offset: memos.length, + filter: filters.join(" && "), + }); + setIsRequesting(false); + setMemos([...memos, ...data]); + setIsComplete(data.length < DEFAULT_MEMO_LIMIT); + }; + + const handleMemoCreated = async (memoId: number) => { + const memo = await memoStore.getOrFetchMemoById(memoId); + setMemos([memo, ...memos]); + }; return (
{!md && }
- - + +
+ + {memos.map((memo) => ( + + ))} + {isRequesting && ( +
+

{t("memo.fetching-data")}

+
+ )} + {isComplete ? ( + memos.length === 0 && ( +
+ +

{t("message.no-data")}

+
+ ) + ) : ( +
+ + {t("memo.fetch-more")} + +
+ )} +
{md && ( diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 7809066f1..716c28664 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -5,8 +5,8 @@ import { toast } from "react-hot-toast"; import { Link, useParams } from "react-router-dom"; import Icon from "@/components/Icon"; import MemoContentV1 from "@/components/MemoContentV1"; -import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog"; import MemoEditorV1 from "@/components/MemoEditorV1"; +import showMemoEditorDialog from "@/components/MemoEditorV1/MemoEditorDialog"; import MemoRelationListViewV1 from "@/components/MemoRelationListViewV1"; import MemoResourceListView from "@/components/MemoResourceListView"; import MemoViewV1 from "@/components/MemoViewV1"; diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index 323bb9ea9..d07917366 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -1,11 +1,15 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { useParams } from "react-router-dom"; -import MemoList from "@/components/MemoList"; +import Empty from "@/components/Empty"; +import MemoViewV1 from "@/components/MemoViewV1"; import MobileHeader from "@/components/MobileHeader"; import UserAvatar from "@/components/UserAvatar"; +import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import useLoading from "@/hooks/useLoading"; -import { useUserV1Store } from "@/store/v1"; +import { useFilterStore } from "@/store/module"; +import { useMemoV1Store, useUserV1Store } from "@/store/v1"; +import { Memo } from "@/types/proto/api/v2/memo_service"; import { User } from "@/types/proto/api/v2/user_service"; import { useTranslate } from "@/utils/i18n"; @@ -15,6 +19,12 @@ const UserProfile = () => { const userV1Store = useUserV1Store(); const loadingState = useLoading(); const [user, setUser] = useState(); + const filterStore = useFilterStore(); + const memoStore = useMemoV1Store(); + const [memos, setMemos] = useState([]); + const [isComplete, setIsComplete] = useState(false); + const [isRequesting, setIsRequesting] = useState(false); + const { tag: tagQuery, text: textQuery } = filterStore.state; useEffect(() => { const username = params.username; @@ -34,6 +44,29 @@ const UserProfile = () => { }); }, [params.username]); + useEffect(() => { + fetchMemos(); + }, [tagQuery, textQuery]); + + const fetchMemos = async () => { + if (!user) { + return; + } + + const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`]; + if (tagQuery) filters.push(`tags == "${tagQuery}"`); + if (textQuery) filters.push(`content_search == "${textQuery}"`); + setIsRequesting(true); + const data = await memoStore.fetchMemos({ + limit: DEFAULT_MEMO_LIMIT, + offset: memos.length, + filter: filters.join(" && "), + }); + setIsRequesting(false); + setMemos([...memos, ...data]); + setIsComplete(data.length < DEFAULT_MEMO_LIMIT); + }; + return (
@@ -45,7 +78,28 @@ const UserProfile = () => {

{user?.nickname}

- + {memos.map((memo) => ( + + ))} + {isRequesting && ( +
+

{t("memo.fetching-data")}

+
+ )} + {isComplete ? ( + memos.length === 0 && ( +
+ +

{t("message.no-data")}

+
+ ) + ) : ( +
+ + {t("memo.fetch-more")} + +
+ )} ) : (

Not found

diff --git a/web/src/store/index.ts b/web/src/store/index.ts index 3aa3829ee..90709b18b 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -3,14 +3,12 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import dialogReducer from "./reducer/dialog"; import filterReducer from "./reducer/filter"; import globalReducer from "./reducer/global"; -import memoReducer from "./reducer/memo"; import resourceReducer from "./reducer/resource"; import tagReducer from "./reducer/tag"; const store = configureStore({ reducer: { global: globalReducer, - memo: memoReducer, tag: tagReducer, filter: filterReducer, resource: resourceReducer, diff --git a/web/src/store/module/index.ts b/web/src/store/module/index.ts index 08021e779..4d6b25b5b 100644 --- a/web/src/store/module/index.ts +++ b/web/src/store/module/index.ts @@ -1,6 +1,5 @@ export * from "./global"; export * from "./filter"; -export * from "./memo"; export * from "./tag"; export * from "./resource"; export * from "./dialog"; diff --git a/web/src/store/module/memo.ts b/web/src/store/module/memo.ts deleted file mode 100644 index 11e914986..000000000 --- a/web/src/store/module/memo.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { omit } from "lodash-es"; -import * as api from "@/helpers/api"; -import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; -import store, { useAppSelector } from "../"; -import { updateLoadingStatus, createMemo, deleteMemo, patchMemo, upsertMemos, LoadingStatus } from "../reducer/memo"; -import { useMemoCacheStore } from "../v1"; - -export const convertResponseModelMemo = (memo: Memo): Memo => { - return { - ...memo, - createdTs: memo.createdTs * 1000, - updatedTs: memo.updatedTs * 1000, - displayTs: memo.displayTs * 1000, - }; -}; - -export const useMemoStore = () => { - const state = useAppSelector((state) => state.memo); - const memoCacheStore = useMemoCacheStore(); - - const fetchMemoById = async (memoId: MemoId) => { - const { data } = await api.getMemoById(memoId); - const memo = convertResponseModelMemo(data); - store.dispatch(upsertMemos([memo])); - - return memo; - }; - - return { - state, - getState: () => { - return store.getState().memo; - }, - fetchMemos: async (username = "", limit = DEFAULT_MEMO_LIMIT, offset = 0) => { - const memoFind: MemoFind = { - rowStatus: "NORMAL", - limit, - offset, - }; - if (username) { - memoFind.creatorUsername = username; - } - - store.dispatch(updateLoadingStatus("fetching")); - const { data } = await api.getMemoList(memoFind); - const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); - store.dispatch(upsertMemos(fetchedMemos)); - store.dispatch(updateLoadingStatus(fetchedMemos.length === limit ? "incomplete" : "complete")); - - for (const m of fetchedMemos) { - memoCacheStore.setMemoCache(m); - } - - return fetchedMemos; - }, - fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => { - const memoFind: MemoFind = { - rowStatus: "NORMAL", - limit, - offset, - }; - - store.dispatch(updateLoadingStatus("fetching")); - const { data } = await api.getAllMemos(memoFind); - const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); - store.dispatch(upsertMemos(fetchedMemos)); - store.dispatch(updateLoadingStatus(fetchedMemos.length === limit ? "incomplete" : "complete")); - - for (const m of fetchedMemos) { - memoCacheStore.setMemoCache(m); - } - - return fetchedMemos; - }, - fetchArchivedMemos: async () => { - const memoFind: MemoFind = { - rowStatus: "ARCHIVED", - }; - const { data } = await api.getMemoList(memoFind); - const archivedMemos = data.map((m) => { - return convertResponseModelMemo(m); - }); - return archivedMemos; - }, - setLoadingStatus: (status: LoadingStatus) => { - store.dispatch(updateLoadingStatus(status)); - }, - fetchMemoById, - getMemoById: async (memoId: MemoId) => { - for (const m of state.memos) { - if (m.id === memoId) { - return m; - } - } - - return await fetchMemoById(memoId); - }, - getLinkedMemos: async (memoId: MemoId): Promise => { - const regex = new RegExp(`[@(.+?)](${memoId})`); - return state.memos.filter((m) => m.content.match(regex)); - }, - createMemo: async (memoCreate: MemoCreate) => { - const { data } = await api.createMemo(memoCreate); - const memo = convertResponseModelMemo(data); - store.dispatch(createMemo(memo)); - memoCacheStore.setMemoCache(memo); - return memo; - }, - patchMemo: async (memoPatch: MemoPatch): Promise => { - const { data } = await api.patchMemo(memoPatch); - const memo = convertResponseModelMemo(data); - store.dispatch(patchMemo(omit(memo, "pinned"))); - memoCacheStore.setMemoCache(memo); - return memo; - }, - pinMemo: async (memoId: MemoId) => { - await api.pinMemo(memoId); - store.dispatch( - patchMemo({ - id: memoId, - pinned: true, - }) - ); - }, - unpinMemo: async (memoId: MemoId) => { - await api.unpinMemo(memoId); - store.dispatch( - patchMemo({ - id: memoId, - pinned: false, - }) - ); - }, - deleteMemoById: async (memoId: MemoId) => { - await api.deleteMemo(memoId); - store.dispatch(deleteMemo(memoId)); - memoCacheStore.deleteMemoCache(memoId); - }, - }; -}; diff --git a/web/src/store/reducer/memo.ts b/web/src/store/reducer/memo.ts deleted file mode 100644 index 31ac21f29..000000000 --- a/web/src/store/reducer/memo.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { uniqBy } from "lodash-es"; - -export type LoadingStatus = "incomplete" | "fetching" | "complete"; - -interface State { - loadingStatus: LoadingStatus; - memos: Memo[]; -} - -const memoSlice = createSlice({ - name: "memo", - initialState: { - loadingStatus: "incomplete", - memos: [], - } as State, - reducers: { - updateLoadingStatus: (state, action: PayloadAction) => { - return { - ...state, - loadingStatus: action.payload, - }; - }, - upsertMemos: (state, action: PayloadAction) => { - return { - ...state, - memos: uniqBy([...action.payload, ...state.memos], "id"), - }; - }, - createMemo: (state, action: PayloadAction) => { - return { - ...state, - memos: state.memos.concat(action.payload), - }; - }, - patchMemo: (state, action: PayloadAction>) => { - return { - ...state, - memos: state.memos - .map((memo) => { - if (memo.id === action.payload.id) { - return { - ...memo, - ...action.payload, - }; - } else { - return memo; - } - }) - .filter((memo) => memo.rowStatus === "NORMAL"), - }; - }, - deleteMemo: (state, action: PayloadAction) => { - return { - ...state, - memos: state.memos.filter((memo) => { - return memo.id !== action.payload; - }), - }; - }, - }, -}); - -export const { updateLoadingStatus, upsertMemos, createMemo, patchMemo, deleteMemo } = memoSlice.actions; - -export default memoSlice.reducer; diff --git a/web/src/store/v1/index.ts b/web/src/store/v1/index.ts index 24225dce4..276ba9765 100644 --- a/web/src/store/v1/index.ts +++ b/web/src/store/v1/index.ts @@ -1,5 +1,4 @@ export * from "./user"; export * from "./memo"; -export * from "./memoCache"; export * from "./inbox"; export * from "./resourceName"; diff --git a/web/src/store/v1/memo.ts b/web/src/store/v1/memo.ts index b40b49d69..42e4f724c 100644 --- a/web/src/store/v1/memo.ts +++ b/web/src/store/v1/memo.ts @@ -10,7 +10,7 @@ export const useMemoV1Store = create( const { memos } = await memoServiceClient.listMemos(request); return memos; }, - getOrFetchMemoById: async (id: MemoId) => { + getOrFetchMemoById: async (id: number) => { const memo = get().memoById.get(id); if (memo) { return memo; diff --git a/web/src/store/v1/memoCache.ts b/web/src/store/v1/memoCache.ts deleted file mode 100644 index 669d69ac3..000000000 --- a/web/src/store/v1/memoCache.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { create } from "zustand"; -import { combine } from "zustand/middleware"; -import * as api from "@/helpers/api"; -import { convertResponseModelMemo } from "../module"; - -export const useMemoCacheStore = create( - combine({ memoById: new Map() }, (set, get) => ({ - getState: () => get(), - getOrFetchMemoById: async (memoId: MemoId) => { - const memo = get().memoById.get(memoId); - if (memo) { - return memo; - } - - const { data } = await api.getMemoById(memoId); - const formatedMemo = convertResponseModelMemo(data); - - set((state) => { - state.memoById.set(memoId, formatedMemo); - return state; - }); - - return formatedMemo; - }, - getMemoById: (memoId: MemoId) => { - return get().memoById.get(memoId); - }, - setMemoCache: (memo: Memo) => { - set((state) => { - state.memoById.set(memo.id, memo); - return state; - }); - }, - deleteMemoCache: (memoId: MemoId) => { - set((state) => { - state.memoById.delete(memoId); - return state; - }); - }, - })) -); diff --git a/web/src/types/memo.d.ts b/web/src/types/memo.d.ts deleted file mode 100644 index feb495b18..000000000 --- a/web/src/types/memo.d.ts +++ /dev/null @@ -1 +0,0 @@ -type MemoSpecType = "NOT_TAGGED" | "LINKED" | "IMAGED" | "CONNECTED"; diff --git a/web/src/types/modules/common.d.ts b/web/src/types/modules/common.d.ts index 4089da3f3..1d6373bc9 100644 --- a/web/src/types/modules/common.d.ts +++ b/web/src/types/modules/common.d.ts @@ -1 +1,3 @@ type RowStatus = "NORMAL" | "ARCHIVED"; + +type Visibility = "PUBLIC" | "PROTECTED" | "PRIVATE"; diff --git a/web/src/types/modules/memo.d.ts b/web/src/types/modules/memo.d.ts deleted file mode 100644 index 4d6aa8341..000000000 --- a/web/src/types/modules/memo.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -type MemoId = number; - -type Visibility = "PUBLIC" | "PROTECTED" | "PRIVATE"; - -interface Memo { - id: MemoId; - - creatorUsername: string; - createdTs: number; - updatedTs: number; - rowStatus: RowStatus; - - displayTs: number; - content: string; - visibility: Visibility; - pinned: boolean; - - creatorName: string; - resourceList: any[]; - relationList: MemoRelation[]; - parent?: Memo; -} - -interface MemoCreate { - content: string; - resourceIdList: ResourceId[]; - relationList: MemoRelationUpsert[]; - visibility?: Visibility; -} - -interface MemoPatch { - id: MemoId; - createdTs?: number; - rowStatus?: RowStatus; - content?: string; - resourceIdList?: ResourceId[]; - relationList?: MemoRelationUpsert[]; - visibility?: Visibility; -} - -interface MemoFind { - creatorUsername?: string; - rowStatus?: RowStatus; - pinned?: boolean; - visibility?: Visibility; - offset?: number; - limit?: number; -} diff --git a/web/src/types/modules/memoRelation.d.ts b/web/src/types/modules/memoRelation.d.ts deleted file mode 100644 index dddd37317..000000000 --- a/web/src/types/modules/memoRelation.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -type MemoRelationType = "REFERENCE" | "COMMENT"; - -interface MemoRelation { - memoId: MemoId; - relatedMemoId: MemoId; - type: MemoRelationType; -} - -interface MemoRelationUpsert { - relatedMemoId: MemoId; - type: MemoRelationType; -}