mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: refactor memo module
This commit is contained in:
@ -59,6 +59,9 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos
|
|||||||
if filter.Visibility != nil {
|
if filter.Visibility != nil {
|
||||||
memoFind.VisibilityList = []store.Visibility{*filter.Visibility}
|
memoFind.VisibilityList = []store.Visibility{*filter.Visibility}
|
||||||
}
|
}
|
||||||
|
if len(filter.Visibilities) > 0 {
|
||||||
|
memoFind.VisibilityList = filter.Visibilities
|
||||||
|
}
|
||||||
if filter.CreatedTsBefore != nil {
|
if filter.CreatedTsBefore != nil {
|
||||||
memoFind.CreatedTsBefore = filter.CreatedTsBefore
|
memoFind.CreatedTsBefore = filter.CreatedTsBefore
|
||||||
}
|
}
|
||||||
@ -81,6 +84,12 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos
|
|||||||
}
|
}
|
||||||
memoFind.CreatorID = &user.ID
|
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 {
|
if filter.RowStatus != nil {
|
||||||
memoFind.RowStatus = filter.RowStatus
|
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}
|
memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.PageSize != 0 {
|
if request.Limit != 0 {
|
||||||
offset := int(request.Page * request.PageSize)
|
offset, limit := int(request.Offset), int(request.Limit)
|
||||||
limit := int(request.PageSize)
|
|
||||||
memoFind.Offset = &offset
|
memoFind.Offset = &offset
|
||||||
memoFind.Limit = &limit
|
memoFind.Limit = &limit
|
||||||
}
|
}
|
||||||
@ -187,6 +195,9 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
|
|||||||
rowStatus := convertRowStatusToStore(request.Memo.RowStatus)
|
rowStatus := convertRowStatusToStore(request.Memo.RowStatus)
|
||||||
println("rowStatus", rowStatus)
|
println("rowStatus", rowStatus)
|
||||||
update.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.
|
// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter.
|
||||||
var ListMemosFilterCELAttributes = []cel.EnvOption{
|
var ListMemosFilterCELAttributes = []cel.EnvOption{
|
||||||
cel.Variable("visibility", cel.StringType),
|
cel.Variable("visibility", cel.StringType),
|
||||||
|
cel.Variable("visibilities", cel.ListType(cel.StringType)),
|
||||||
cel.Variable("created_ts_before", cel.IntType),
|
cel.Variable("created_ts_before", cel.IntType),
|
||||||
cel.Variable("created_ts_after", cel.IntType),
|
cel.Variable("created_ts_after", cel.IntType),
|
||||||
cel.Variable("creator", cel.StringType),
|
cel.Variable("creator", cel.StringType),
|
||||||
@ -510,9 +522,12 @@ var ListMemosFilterCELAttributes = []cel.EnvOption{
|
|||||||
|
|
||||||
type ListMemosFilter struct {
|
type ListMemosFilter struct {
|
||||||
Visibility *store.Visibility
|
Visibility *store.Visibility
|
||||||
|
Visibilities []store.Visibility
|
||||||
CreatedTsBefore *int64
|
CreatedTsBefore *int64
|
||||||
CreatedTsAfter *int64
|
CreatedTsAfter *int64
|
||||||
Creator *string
|
Creator *string
|
||||||
|
Tag *string
|
||||||
|
ContentSearch *string
|
||||||
RowStatus *store.RowStatus
|
RowStatus *store.RowStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,6 +558,14 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
|
|||||||
visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue())
|
visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue())
|
||||||
filter.Visibility = &visibility
|
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" {
|
if idExpr.Name == "created_ts_before" {
|
||||||
createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value()
|
createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value()
|
||||||
filter.CreatedTsBefore = &createdTsBefore
|
filter.CreatedTsBefore = &createdTsBefore
|
||||||
@ -555,6 +578,14 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
|
|||||||
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
|
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
|
||||||
filter.Creator = &creator
|
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" {
|
if idExpr.Name == "row_status" {
|
||||||
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
|
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
|
||||||
filter.RowStatus = &rowStatus
|
filter.RowStatus = &rowStatus
|
||||||
|
@ -128,9 +128,11 @@ message CreateMemoResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message ListMemosRequest {
|
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.
|
// Filter is used to filter memos returned in the list.
|
||||||
// Format: "creator == users/{username} && visibility == PUBLIC"
|
// Format: "creator == users/{username} && visibility == PUBLIC"
|
||||||
|
@ -1760,8 +1760,8 @@
|
|||||||
|
|
||||||
| Field | Type | Label | Description |
|
| Field | Type | Label | Description |
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| page | [int32](#int32) | | |
|
| offset | [int32](#int32) | | offset is the offset of the first memo to return. |
|
||||||
| page_size | [int32](#int32) | | |
|
| 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" |
|
| filter | [string](#string) | | Filter is used to filter memos returned in the list. Format: "creator == users/{username} && visibility == PUBLIC" |
|
||||||
|
|
||||||
|
|
||||||
|
@ -311,8 +311,10 @@ type ListMemosRequest struct {
|
|||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"`
|
// offset is the offset of the first memo to return.
|
||||||
PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
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.
|
// Filter is used to filter memos returned in the list.
|
||||||
// Format: "creator == users/{username} && visibility == PUBLIC"
|
// Format: "creator == users/{username} && visibility == PUBLIC"
|
||||||
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
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}
|
return file_api_v2_memo_service_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListMemosRequest) GetPage() int32 {
|
func (x *ListMemosRequest) GetOffset() int32 {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Page
|
return x.Offset
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListMemosRequest) GetPageSize() int32 {
|
func (x *ListMemosRequest) GetLimit() int32 {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.PageSize
|
return x.Limit
|
||||||
}
|
}
|
||||||
return 0
|
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,
|
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,
|
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,
|
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,
|
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, 0x12,
|
0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16,
|
||||||
0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61,
|
0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06,
|
||||||
0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18,
|
0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18,
|
||||||
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12,
|
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06,
|
||||||
0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69,
|
||||||
0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d,
|
0x6c, 0x74, 0x65, 0x72, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f,
|
||||||
0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05,
|
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x6d, 0x65, 0x6d,
|
||||||
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65,
|
0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73,
|
||||||
0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52,
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x05, 0x6d, 0x65,
|
||||||
0x05, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d,
|
0x6d, 0x6f, 0x73, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65,
|
||||||
0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x39, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d,
|
0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x39, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f,
|
||||||
0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f,
|
||||||
0x65, 0x6d, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61,
|
||||||
0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d,
|
0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f,
|
||||||
0x65, 0x6d, 0x6f, 0x22, 0x88, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65,
|
0x22, 0x88, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52,
|
||||||
0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d,
|
0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x02,
|
||||||
0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d,
|
0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x3b,
|
||||||
0x6f, 0x12, 0x3b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b,
|
0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20,
|
||||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61,
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52,
|
||||||
0x73, 0x6b, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x3c,
|
0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x3c, 0x0a, 0x12, 0x55,
|
||||||
0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70,
|
0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x01, 0x20, 0x01,
|
0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
|
0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d,
|
||||||
0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x23, 0x0a, 0x11,
|
0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c,
|
||||||
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
|
||||||
0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69,
|
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14,
|
||||||
0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52,
|
0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x65,
|
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52,
|
||||||
0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02,
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12,
|
||||||
0x69, 0x64, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18,
|
0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03,
|
||||||
0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70,
|
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
|
||||||
0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72,
|
0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f,
|
||||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4d,
|
0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f,
|
||||||
0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
|
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f,
|
0x65, 0x22, 0x2a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73,
|
||||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
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,
|
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,
|
0x12, 0x37, 0x0a, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||||
0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a,
|
0x32, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e,
|
||||||
0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e,
|
0x74, 0x52, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x22, 0x43, 0x0a, 0x19, 0x43, 0x72, 0x65,
|
||||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65,
|
||||||
0x63, 0x65, 0x73, 0x22, 0x63, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x01,
|
||||||
0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38,
|
0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x22, 0x29,
|
||||||
0x0a, 0x09, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
|
0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
|
||||||
0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32,
|
0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
||||||
0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72,
|
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x44, 0x0a, 0x18, 0x4c, 0x69, 0x73,
|
||||||
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4d,
|
0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73,
|
||||||
0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70,
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x18, 0x01,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f,
|
0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x05, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2a,
|
||||||
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64,
|
0x50, 0x0a, 0x0a, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x1a, 0x0a,
|
||||||
0x22, 0x55, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61,
|
0x16, 0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50,
|
||||||
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a,
|
0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x49,
|
||||||
0x09, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
0x56, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x4f, 0x54, 0x45, 0x43,
|
||||||
0x32, 0x1a, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e,
|
0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10,
|
||||||
0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65,
|
0x03, 0x32, 0xa2, 0x0b, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||||
0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x63, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74,
|
0x65, 0x12, 0x69, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12,
|
||||||
0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75,
|
0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43,
|
||||||
0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52,
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||||
0x02, 0x69, 0x64, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20,
|
0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||||
0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71,
|
0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x52, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x22, 0x43, 0x0a, 0x19,
|
0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x63, 0x0a, 0x09,
|
||||||
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
|
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x12, 0x1e, 0x2e, 0x6d, 0x65, 0x6d, 0x6f,
|
||||||
0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6d, 0x65, 0x6d,
|
0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d,
|
||||||
0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
|
0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x04, 0x6d, 0x65, 0x6d,
|
0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d,
|
||||||
0x6f, 0x22, 0x29, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d,
|
0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93,
|
||||||
0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02,
|
0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f,
|
||||||
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x44, 0x0a, 0x18,
|
0x73, 0x12, 0x67, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1c, 0x2e, 0x6d,
|
||||||
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73,
|
0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x6d, 0x65, 0x6d, 0x6f,
|
0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6d, 0x65, 0x6d,
|
||||||
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
|
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x05, 0x6d, 0x65, 0x6d,
|
0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0xda, 0x41, 0x02, 0x69, 0x64,
|
||||||
0x6f, 0x73, 0x2a, 0x50, 0x0a, 0x0a, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
|
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f,
|
||||||
0x12, 0x1a, 0x0a, 0x16, 0x56, 0x49, 0x53, 0x49, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55,
|
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x80, 0x01, 0x0a, 0x0a, 0x55,
|
||||||
0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07,
|
0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f,
|
||||||
0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x4f,
|
0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d,
|
||||||
0x54, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c,
|
0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d,
|
||||||
0x49, 0x43, 0x10, 0x03, 0x32, 0xa2, 0x0b, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72,
|
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65,
|
0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2f, 0xda, 0x41,
|
||||||
0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
|
0x0f, 0x69, 0x64, 0x2c, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b,
|
||||||
0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75,
|
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x32, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x70, 0x0a,
|
||||||
0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73,
|
0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x1f, 0x2e, 0x6d, 0x65,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a,
|
0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74,
|
||||||
0x22, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x12,
|
0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d,
|
||||||
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, 0x44, 0x65, 0x6c, 0x65,
|
||||||
0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f,
|
||||||
0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6d,
|
0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x2a, 0x12, 0x2f, 0x61, 0x70,
|
||||||
0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12,
|
||||||
0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x82,
|
0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75,
|
||||||
0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d,
|
0x72, 0x63, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x65, 0x6d, 0x6f, 0x73, 0x12, 0x67, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x12,
|
0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75,
|
||||||
0x1c, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47,
|
0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65,
|
||||||
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,
|
|
||||||
0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 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,
|
0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
|
0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21,
|
||||||
0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f,
|
0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d,
|
||||||
0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69,
|
0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||||
0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69,
|
0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65,
|
||||||
0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72,
|
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
|
||||||
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73,
|
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52,
|
||||||
0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26,
|
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||||
0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69,
|
0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c,
|
||||||
0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52,
|
0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82,
|
||||||
0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65,
|
0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d,
|
||||||
0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
||||||
0x29, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61,
|
0x63, 0x65, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52,
|
||||||
0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d,
|
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73,
|
||||||
0x2f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8e, 0x01, 0x0a, 0x11, 0x43,
|
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52,
|
||||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
|
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||||
0x12, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e,
|
0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x53,
|
||||||
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
|
0x65, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52,
|
||||||
0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3,
|
||||||
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65,
|
0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32,
|
||||||
0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65, 0x6c, 0x61,
|
||||||
0x65, 0x22, 0x28, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x1b,
|
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65,
|
||||||
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69,
|
0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x2e, 0x6d, 0x65,
|
||||||
0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x8b, 0x01, 0x0a, 0x10,
|
0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d,
|
||||||
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73,
|
0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75,
|
||||||
0x12, 0x25, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e,
|
0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73,
|
0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x52, 0x65, 0x6c, 0x61, 0x74,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
|
0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0xda, 0x41,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43,
|
0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||||
0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65,
|
||||||
0x28, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x1b, 0x2f, 0x61,
|
0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8e, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61,
|
||||||
0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d,
|
0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x2e,
|
||||||
0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0xa8, 0x01, 0x0a, 0x10, 0x63, 0x6f,
|
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65,
|
||||||
0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x10,
|
0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65,
|
||||||
0x4d, 0x65, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70,
|
||||||
0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75,
|
0x69, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x6f, 0x43,
|
||||||
0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72,
|
0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28,
|
||||||
0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61,
|
0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x22, 0x1b, 0x2f, 0x61, 0x70,
|
||||||
0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d,
|
0x69, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f,
|
||||||
0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f,
|
0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x8b, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73,
|
||||||
0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73,
|
0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e,
|
||||||
0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73,
|
||||||
0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69,
|
0x74, 0x4d, 0x65, 0x6d, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71,
|
||||||
0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
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 (
|
var (
|
||||||
|
@ -2,26 +2,26 @@ import { Button } from "@mui/joy";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { getNormalizedTimeString, getUnixTime } from "@/helpers/datetime";
|
import { getNormalizedTimeString, getUnixTime } from "@/helpers/datetime";
|
||||||
import { useMemoStore } from "@/store/module";
|
import { useMemoV1Store } from "@/store/v1";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
memoId: MemoId;
|
memoId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const { destroy, memoId } = props;
|
const { destroy, memoId } = props;
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoV1Store();
|
||||||
const [createdAt, setCreatedAt] = useState("");
|
const [createdAt, setCreatedAt] = useState("");
|
||||||
const maxDatetimeValue = getNormalizedTimeString();
|
const maxDatetimeValue = getNormalizedTimeString();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
memoStore.getMemoById(memoId).then((memo) => {
|
memoStore.getOrFetchMemoById(memoId).then((memo) => {
|
||||||
if (memo) {
|
if (memo) {
|
||||||
const datetime = getNormalizedTimeString(memo.createdTs);
|
const datetime = getNormalizedTimeString(memo.createTime);
|
||||||
setCreatedAt(datetime);
|
setCreatedAt(datetime);
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("message.memo-not-found"));
|
toast.error(t("message.memo-not-found"));
|
||||||
@ -41,18 +41,19 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
const handleSaveBtnClick = async () => {
|
const handleSaveBtnClick = async () => {
|
||||||
const nowTs = getUnixTime();
|
const nowTs = getUnixTime();
|
||||||
const createdTs = getUnixTime(createdAt);
|
if (getUnixTime(createdAt) > nowTs) {
|
||||||
|
|
||||||
if (createdTs > nowTs) {
|
|
||||||
toast.error(t("message.invalid-created-datetime"));
|
toast.error(t("message.invalid-created-datetime"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await memoStore.patchMemo({
|
await memoStore.updateMemo(
|
||||||
id: memoId,
|
{
|
||||||
createdTs,
|
id: memoId,
|
||||||
});
|
createTime: new Date(createdAt),
|
||||||
|
},
|
||||||
|
["created_ts"]
|
||||||
|
);
|
||||||
toast.success(t("message.memo-updated-datetime"));
|
toast.success(t("message.memo-updated-datetime"));
|
||||||
handleCloseBtnClick();
|
handleCloseBtnClick();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -90,7 +91,7 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function showChangeMemoCreatedTsDialog(memoId: MemoId) {
|
function showChangeMemoCreatedTsDialog(memoId: number) {
|
||||||
generateDialog(
|
generateDialog(
|
||||||
{
|
{
|
||||||
className: "change-memo-created-ts-dialog",
|
className: "change-memo-created-ts-dialog",
|
||||||
|
@ -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: 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<boolean>(lazyRendering ? false : true);
|
|
||||||
const [displayTime, setDisplayTime] = useState<string>(getRelativeTimeString(memo.displayTs));
|
|
||||||
const memoContainerRef = useRef<HTMLDivElement>(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 <div className={`w-full h-32 !bg-transparent ${"memos-" + memo.id}`} ref={memoContainerRef} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGotoMemoDetailPage = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className={`group memo-wrapper ${"memos-" + memo.id} ${memo.pinned && props.showPinnedStyle ? "pinned" : ""}`}
|
|
||||||
ref={memoContainerRef}
|
|
||||||
>
|
|
||||||
<div className="memo-top-wrapper mb-1">
|
|
||||||
<div className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
|
|
||||||
{props.showCreator && creator && (
|
|
||||||
<>
|
|
||||||
<Link to={`/u/${encodeURIComponent(memo.creatorUsername)}`}>
|
|
||||||
<Tooltip title={"Creator"} placement="top">
|
|
||||||
<span className="flex flex-row justify-start items-center">
|
|
||||||
<UserAvatar className="!w-5 !h-5 mr-1" avatarUrl={creator.avatarUrl} />
|
|
||||||
<span className="text-sm text-gray-600 max-w-[8em] truncate dark:text-gray-400">
|
|
||||||
{creator.nickname || extractUsernameFromName(creator.name)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Link>
|
|
||||||
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span className="text-sm text-gray-400 select-none" onClick={handleGotoMemoDetailPage}>
|
|
||||||
{displayTime}
|
|
||||||
</span>
|
|
||||||
{props.showPinnedStyle && memo.pinned && (
|
|
||||||
<>
|
|
||||||
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
|
|
||||||
<Tooltip title={"Pinned"} placement="top">
|
|
||||||
<Icon.Bookmark className="w-4 h-auto text-green-600" />
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="w-auto hidden group-hover:flex flex-row justify-between items-center">
|
|
||||||
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
|
|
||||||
<Link className="flex flex-row justify-start items-center" to={`/m/${memo.id}`}>
|
|
||||||
<Tooltip title={"Identifier"} placement="top">
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Link>
|
|
||||||
{props.showVisibility && memo.visibility !== "PRIVATE" && (
|
|
||||||
<>
|
|
||||||
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
|
|
||||||
<Tooltip title={t(`memo.visibility.${memo.visibility.toLowerCase()}` as any)} placement="top">
|
|
||||||
<span>
|
|
||||||
<VisibilityIcon visibility={memo.visibility} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="btns-container space-x-2">
|
|
||||||
{!readonly && (
|
|
||||||
<>
|
|
||||||
<span className="btn more-action-btn">
|
|
||||||
<Icon.MoreVertical className="icon-img" />
|
|
||||||
</span>
|
|
||||||
<div className="more-action-btns-wrapper">
|
|
||||||
<div className="more-action-btns-container min-w-[6em]">
|
|
||||||
{!memo.parent && (
|
|
||||||
<span className="btn" onClick={handleTogglePinMemoBtnClick}>
|
|
||||||
{memo.pinned ? <Icon.BookmarkMinus className="w-4 h-auto mr-2" /> : <Icon.BookmarkPlus className="w-4 h-auto mr-2" />}
|
|
||||||
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="btn" onClick={handleEditMemoClick}>
|
|
||||||
<Icon.Edit3 className="w-4 h-auto mr-2" />
|
|
||||||
{t("common.edit")}
|
|
||||||
</span>
|
|
||||||
{!memo.parent && (
|
|
||||||
<span className="btn" onClick={handleMarkMemoClick}>
|
|
||||||
<Icon.Link className="w-4 h-auto mr-2" />
|
|
||||||
{t("common.mark")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="btn" onClick={() => showShareMemoDialog(memo)}>
|
|
||||||
<Icon.Share className="w-4 h-auto mr-2" />
|
|
||||||
{t("common.share")}
|
|
||||||
</span>
|
|
||||||
<Divider className="!my-1" />
|
|
||||||
<span className="btn text-orange-500" onClick={handleArchiveMemoClick}>
|
|
||||||
<Icon.Archive className="w-4 h-auto mr-2" />
|
|
||||||
{t("common.archive")}
|
|
||||||
</span>
|
|
||||||
<span className="btn text-red-600" onClick={handleDeleteMemoClick}>
|
|
||||||
<Icon.Trash className="w-4 h-auto mr-2" />
|
|
||||||
{t("common.delete")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{props.showParent && memo.parent && (
|
|
||||||
<div className="w-auto max-w-full mb-1">
|
|
||||||
<Link
|
|
||||||
className="px-2 py-0.5 border rounded-full max-w-xs w-auto text-xs flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-400 dark:border-gray-500 hover:shadow hover:opacity-80"
|
|
||||||
to={`/m/${memo.parent.id}`}
|
|
||||||
>
|
|
||||||
<Icon.ArrowUpRightFromCircle className="w-3 h-auto shrink-0 opacity-60" />
|
|
||||||
<span className="mx-1 opacity-60">#{memo.parent.id}</span>
|
|
||||||
<span className="truncate">{memo.parent.content}</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<MemoContentV1 content={memo.content} onMemoContentClick={handleMemoContentClick} />
|
|
||||||
<MemoResourceListView resourceList={memo.resourceList} />
|
|
||||||
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Memo);
|
|
@ -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 (
|
|
||||||
<IconButton className="relative group flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-400 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow">
|
|
||||||
<Icon.Hash className="w-5 h-5 mx-auto" />
|
|
||||||
<div className="hidden flex-row justify-start items-start flex-wrap absolute top-8 left-0 mt-1 p-1 z-1 rounded w-52 h-auto max-h-48 overflow-y-auto font-mono shadow bg-zinc-200 dark:bg-zinc-600 group-hover:flex">
|
|
||||||
{tags.length > 0 ? (
|
|
||||||
tags.map((tag) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-auto max-w-full text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0"
|
|
||||||
onClick={() => onTagSelectorClick(tag)}
|
|
||||||
key={tag}
|
|
||||||
>
|
|
||||||
<OverflowTip>#{tag}</OverflowTip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<p className="italic text-sm ml-2" onClick={(e) => e.stopPropagation()}>
|
|
||||||
No tags found
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagSelector;
|
|
@ -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<HTMLTextAreaElement>;
|
|
||||||
editorActions: React.ForwardedRef<EditorRefActions>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Position = { left: number; top: number; height: number };
|
|
||||||
|
|
||||||
const TagSuggestions = ({ editorRef, editorActions }: Props) => {
|
|
||||||
const [position, setPosition] = useState<Position | null>(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<string[]>([]);
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className="z-20 p-1 mt-1 -ml-2 absolute max-w-[12rem] gap-px rounded font-mono flex flex-col justify-start items-start overflow-auto shadow bg-zinc-200 dark:bg-zinc-600"
|
|
||||||
style={{ left: position.left, top: position.top + position.height }}
|
|
||||||
>
|
|
||||||
{suggestionsRef.current.map((tag, i) => (
|
|
||||||
<div
|
|
||||||
key={tag}
|
|
||||||
onMouseDown={() => 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" : ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<OverflowTip>#{tag}</OverflowTip>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagSuggestions;
|
|
@ -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<EditorRefActions>) {
|
|
||||||
const { className, initialContent, placeholder, onPaste, onContentChange: handleContentChangeCallback } = props;
|
|
||||||
const editorRef = useRef<HTMLTextAreaElement>(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 (
|
|
||||||
<div className={classNames("flex flex-col justify-start items-start relative w-full h-auto bg-inherit dark:text-gray-200", className)}>
|
|
||||||
<textarea
|
|
||||||
className="w-full h-full max-h-[300px] my-1 text-base resize-none overflow-x-hidden overflow-y-auto bg-transparent outline-none whitespace-pre-wrap word-break"
|
|
||||||
rows={1}
|
|
||||||
placeholder={placeholder}
|
|
||||||
ref={editorRef}
|
|
||||||
onPaste={onPaste}
|
|
||||||
onInput={handleEditorInput}
|
|
||||||
></textarea>
|
|
||||||
<TagSuggestions editorRef={editorRef} editorActions={ref} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Editor;
|
|
@ -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<Props> = ({ memoId, relationList, destroy }: Props) => {
|
|
||||||
const globalStore = useGlobalStore();
|
|
||||||
const tagStore = useTagStore();
|
|
||||||
const { systemStatus } = globalStore.state;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
tagStore.fetchTags();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCloseBtnClick = () => {
|
|
||||||
destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="dialog-header-container">
|
|
||||||
<div className="flex flex-row justify-start items-center">
|
|
||||||
<img className="w-5 h-auto rounded-full shadow" src={systemStatus.customizedProfile.logoUrl} alt="" />
|
|
||||||
<p className="ml-1 text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
|
|
||||||
</div>
|
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
|
||||||
<Icon.X />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-start items-start max-w-full w-[36rem]">
|
|
||||||
<MemoEditor
|
|
||||||
className="border-none !p-0 -mb-2"
|
|
||||||
cacheKey={`memo-editor-${memoId}`}
|
|
||||||
memoId={memoId}
|
|
||||||
relationList={relationList}
|
|
||||||
onConfirm={handleCloseBtnClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function showMemoEditorDialog(props: Pick<Props, "memoId" | "relationList"> = {}): void {
|
|
||||||
generateDialog(
|
|
||||||
{
|
|
||||||
className: "memo-editor-dialog",
|
|
||||||
dialogName: "memo-editor-dialog",
|
|
||||||
containerClassName: "dark:!bg-zinc-700",
|
|
||||||
},
|
|
||||||
MemoEditorDialog,
|
|
||||||
props
|
|
||||||
);
|
|
||||||
}
|
|
@ -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<Memo[]>([]);
|
|
||||||
|
|
||||||
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 && (
|
|
||||||
<div className="w-full flex flex-row gap-2 mt-2 flex-wrap">
|
|
||||||
{referencingMemoList.map((memo) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={memo.id}
|
|
||||||
className="w-auto max-w-xs overflow-hidden flex flex-row justify-start items-center bg-gray-100 dark:bg-zinc-800 hover:opacity-80 rounded-md text-sm p-1 px-2 text-gray-500 cursor-pointer hover:line-through"
|
|
||||||
onClick={() => handleDeleteRelation(memo)}
|
|
||||||
>
|
|
||||||
<Icon.Link className="w-4 h-auto shrink-0 opacity-80" />
|
|
||||||
<span className="px-1 shrink-0 opacity-80">#{memo.id}</span>
|
|
||||||
<span className="max-w-full text-ellipsis whitespace-nowrap overflow-hidden">{memo.content}</span>
|
|
||||||
<Icon.X className="w-4 h-auto hover:opacity-80 shrink-0 ml-1" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RelationListView;
|
|
@ -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 && (
|
|
||||||
<div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2">
|
|
||||||
{resourceList.map((resource) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={resource.id}
|
|
||||||
className="max-w-full flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-gray-100 dark:bg-zinc-800 px-2 py-1 rounded text-gray-500"
|
|
||||||
>
|
|
||||||
<ResourceIcon resource={resource} className="!w-4 !h-4 !opacity-100" />
|
|
||||||
<span className="text-sm max-w-[8rem] truncate">{resource.filename}</span>
|
|
||||||
<Icon.X
|
|
||||||
className="w-4 h-auto cursor-pointer opacity-60 hover:opacity-100"
|
|
||||||
onClick={() => handleDeleteResource(resource.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ResourceListView;
|
|
@ -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<string>(contentCacheKey, "");
|
|
||||||
const {
|
|
||||||
state: { systemStatus },
|
|
||||||
} = useGlobalStore();
|
|
||||||
const userV1Store = useUserV1Store();
|
|
||||||
const memoStore = useMemoStore();
|
|
||||||
const resourceStore = useResourceStore();
|
|
||||||
const currentUser = useCurrentUser();
|
|
||||||
const [state, setState] = useState<State>({
|
|
||||||
memoVisibility: "PRIVATE",
|
|
||||||
resourceList: [],
|
|
||||||
relationList: props.relationList ?? [],
|
|
||||||
isUploadingResource: false,
|
|
||||||
isRequesting: false,
|
|
||||||
});
|
|
||||||
const [hasContent, setHasContent] = useState<boolean>(false);
|
|
||||||
const [isInIME, setIsInIME] = useState(false);
|
|
||||||
const editorRef = useRef<EditorRefActions>(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 (
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
className ?? ""
|
|
||||||
} relative w-full flex flex-col justify-start items-start bg-white dark:bg-zinc-700 px-4 pt-4 rounded-lg border border-gray-200 dark:border-zinc-600`}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onDrop={handleDropEvent}
|
|
||||||
onFocus={handleEditorFocus}
|
|
||||||
onCompositionStart={() => setIsInIME(true)}
|
|
||||||
onCompositionEnd={() => setIsInIME(false)}
|
|
||||||
>
|
|
||||||
<Editor ref={editorRef} {...editorConfig} />
|
|
||||||
<div className="relative w-full flex flex-row justify-between items-center pt-2">
|
|
||||||
<div className="flex flex-row justify-start items-center">
|
|
||||||
<TagSelector onTagSelectorClick={(tag) => handleTagSelectorClick(tag)} />
|
|
||||||
<IconButton
|
|
||||||
className="flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-400 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow"
|
|
||||||
onClick={handleUploadFileBtnClick}
|
|
||||||
>
|
|
||||||
<Icon.Image className="w-5 h-5 mx-auto" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
className="flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-400 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow"
|
|
||||||
onClick={handleAddMemoRelationBtnClick}
|
|
||||||
>
|
|
||||||
<Icon.Link className="w-5 h-5 mx-auto" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
className="flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-400 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow"
|
|
||||||
onClick={handleCheckBoxBtnClick}
|
|
||||||
>
|
|
||||||
<Icon.CheckSquare className="w-5 h-5 mx-auto" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
className="flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-400 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow"
|
|
||||||
onClick={handleCodeBlockBtnClick}
|
|
||||||
>
|
|
||||||
<Icon.Code className="w-5 h-5 mx-auto" />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
|
|
||||||
<RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} />
|
|
||||||
<Divider className="!mt-2" />
|
|
||||||
<div className="w-full flex flex-row justify-between items-center py-3 dark:border-t-zinc-500">
|
|
||||||
<div className="relative flex flex-row justify-start items-center" onFocus={(e) => e.stopPropagation()}>
|
|
||||||
<Select
|
|
||||||
variant="plain"
|
|
||||||
value={state.memoVisibility}
|
|
||||||
startDecorator={<VisibilityIcon visibility={state.memoVisibility} />}
|
|
||||||
onChange={(_, visibility) => {
|
|
||||||
if (visibility) {
|
|
||||||
handleMemoVisibilityChange(visibility);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{VISIBILITY_SELECTOR_ITEMS.map((item) => (
|
|
||||||
<Option key={item} value={item} className="whitespace-nowrap" disabled={disableOption(item)}>
|
|
||||||
{t(`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`)}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0 flex flex-row justify-end items-center">
|
|
||||||
<Button color="success" disabled={!allowSave} onClick={handleSaveBtnClick}>
|
|
||||||
{t("editor.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemoEditor;
|
|
@ -6,7 +6,7 @@ import { generateDialog } from "../Dialog";
|
|||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
memoId?: MemoId;
|
memoId?: number;
|
||||||
relationList?: MemoRelation[];
|
relationList?: MemoRelation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service";
|
||||||
|
import { Memo } from "@/types/proto/api/v2/memo_service";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -10,7 +11,7 @@ interface Props {
|
|||||||
|
|
||||||
const RelationListView = (props: Props) => {
|
const RelationListView = (props: Props) => {
|
||||||
const { relationList, setRelationList } = props;
|
const { relationList, setRelationList } = props;
|
||||||
const memoCacheStore = useMemoCacheStore();
|
const memoStore = useMemoV1Store();
|
||||||
const [referencingMemoList, setReferencingMemoList] = useState<Memo[]>([]);
|
const [referencingMemoList, setReferencingMemoList] = useState<Memo[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -18,7 +19,7 @@ const RelationListView = (props: Props) => {
|
|||||||
const requests = relationList
|
const requests = relationList
|
||||||
.filter((relation) => relation.type === MemoRelation_Type.REFERENCE)
|
.filter((relation) => relation.type === MemoRelation_Type.REFERENCE)
|
||||||
.map(async (relation) => {
|
.map(async (relation) => {
|
||||||
return await memoCacheStore.getOrFetchMemoById(relation.relatedMemoId);
|
return await memoStore.getOrFetchMemoById(relation.relatedMemoId);
|
||||||
});
|
});
|
||||||
const list = await Promise.all(requests);
|
const list = await Promise.all(requests);
|
||||||
setReferencingMemoList(list);
|
setReferencingMemoList(list);
|
||||||
|
@ -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<HTMLSpanElement>(null);
|
|
||||||
|
|
||||||
const shownMemos = (
|
|
||||||
showMemoFilter
|
|
||||||
? memos.filter((memo) => {
|
|
||||||
let shouldShow = true;
|
|
||||||
|
|
||||||
if (tagQuery) {
|
|
||||||
const tagsSet = new Set<string>();
|
|
||||||
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 (
|
|
||||||
<div className="flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar">
|
|
||||||
<MemoFilter />
|
|
||||||
{sortedMemos.map((memo) => (
|
|
||||||
<Memo key={memo.id} memo={memo} lazyRendering showVisibility showPinnedStyle showParent />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{loadingStatus === "fetching" ? (
|
|
||||||
<div className="flex flex-col justify-start items-center w-full mt-2 mb-1">
|
|
||||||
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col justify-start items-center w-full my-6">
|
|
||||||
<div className="text-gray-400 italic">
|
|
||||||
{loadingStatus === "complete" ? (
|
|
||||||
sortedMemos.length === 0 && (
|
|
||||||
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
|
||||||
<Empty />
|
|
||||||
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span ref={fetchMoreRef} className="cursor-pointer hover:text-green-600" onClick={handleFetchMoreClick}>
|
|
||||||
{t("memo.fetch-more")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemoList;
|
|
@ -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<Memo[]>([]);
|
|
||||||
const [referencedMemoList, setReferencedMemoList] = useState<Memo[]>([]);
|
|
||||||
|
|
||||||
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 && (
|
|
||||||
<div className="w-full mt-2 flex flex-row justify-start items-center flex-wrap gap-2">
|
|
||||||
{referencingMemoList.map((memo) => {
|
|
||||||
return (
|
|
||||||
<div key={memo.id} className="block w-auto max-w-[50%]">
|
|
||||||
<Link
|
|
||||||
className="px-2 border rounded-md w-auto text-sm leading-6 flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-300 dark:border-gray-600 hover:shadow hover:opacity-80"
|
|
||||||
to={`/m/${memo.id}`}
|
|
||||||
>
|
|
||||||
<Tooltip title="Reference" placement="top">
|
|
||||||
<Icon.Link className="w-4 h-auto shrink-0 opacity-70" />
|
|
||||||
</Tooltip>
|
|
||||||
<span className="opacity-70 mx-1">#{memo.id}</span>
|
|
||||||
<span className="truncate">{memo.content}</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{referencedMemoList.length > 0 && (
|
|
||||||
<div className="w-full mt-2 flex flex-row justify-start items-center flex-wrap gap-2">
|
|
||||||
{referencedMemoList.map((memo) => {
|
|
||||||
return (
|
|
||||||
<div key={memo.id} className="block w-auto max-w-[50%]">
|
|
||||||
<Link
|
|
||||||
className="px-2 border rounded-md w-auto text-sm leading-6 flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-300 dark:border-gray-600 hover:shadow hover:opacity-80"
|
|
||||||
to={`/m/${memo.id}`}
|
|
||||||
>
|
|
||||||
<Tooltip title="Backlink" placement="top">
|
|
||||||
<Icon.Milestone className="w-4 h-auto shrink-0 opacity-70" />
|
|
||||||
</Tooltip>
|
|
||||||
<span className="opacity-70 mx-1">#{memo.id}</span>
|
|
||||||
<span className="truncate">{memo.content}</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemoRelationListView;
|
|
@ -19,7 +19,7 @@ import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
|||||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import MemoContentV1 from "./MemoContentV1";
|
import MemoContentV1 from "./MemoContentV1";
|
||||||
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
|
import showMemoEditorDialog from "./MemoEditorV1/MemoEditorDialog";
|
||||||
import MemoRelationListViewV1 from "./MemoRelationListViewV1";
|
import MemoRelationListViewV1 from "./MemoRelationListViewV1";
|
||||||
import MemoResourceListView from "./MemoResourceListView";
|
import MemoResourceListView from "./MemoResourceListView";
|
||||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
@ -56,23 +56,6 @@ const MemoViewV1: React.FC<Props> = (props: Props) => {
|
|||||||
const referenceRelations = memoRelations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
|
const referenceRelations = memoRelations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
|
||||||
const readonly = memo.creator !== user?.name;
|
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.
|
// Prepare memo creator.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (creator) return;
|
if (creator) return;
|
||||||
@ -113,6 +96,27 @@ const MemoViewV1: React.FC<Props> = (props: Props) => {
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [lazyRendering, filterStore.state]);
|
}, [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) {
|
if (!shouldRender) {
|
||||||
// Render a placeholder to occupy the space.
|
// Render a placeholder to occupy the space.
|
||||||
return <div className={`w-full h-32 !bg-transparent ${"memos-" + memo.id}`} ref={memoContainerRef} />;
|
return <div className={`w-full h-32 !bg-transparent ${"memos-" + memo.id}`} ref={memoContainerRef} />;
|
||||||
@ -162,7 +166,7 @@ const MemoViewV1: React.FC<Props> = (props: Props) => {
|
|||||||
{
|
{
|
||||||
memoId: UNKNOWN_ID,
|
memoId: UNKNOWN_ID,
|
||||||
relatedMemoId: memo.id,
|
relatedMemoId: memo.id,
|
||||||
type: "REFERENCE",
|
type: MemoRelation_Type.REFERENCE,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -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: Props) => {
|
|
||||||
const { memo: propsMemo, destroy } = props;
|
|
||||||
const t = useTranslate();
|
|
||||||
const userV1Store = useUserV1Store();
|
|
||||||
const downloadingImageState = useLoading(false);
|
|
||||||
const loadingState = useLoading();
|
|
||||||
const memoElRef = useRef<HTMLDivElement>(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 (
|
|
||||||
<>
|
|
||||||
<div className="dialog-header-container py-3 px-4 !mb-0 rounded-t-lg">
|
|
||||||
<p className="">{t("common.share")} Memo</p>
|
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
|
||||||
<Icon.X className="icon-img" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="dialog-content-container w-full flex flex-col justify-start items-start relative">
|
|
||||||
<div className="px-4 pb-3 w-full flex flex-row justify-start items-center space-x-2">
|
|
||||||
<Button color="neutral" variant="outlined" disabled={downloadingImageState.isLoading} onClick={handleDownloadImageBtnClick}>
|
|
||||||
{downloadingImageState.isLoading ? (
|
|
||||||
<Icon.Loader className="w-4 h-auto mr-1 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Icon.Download className="w-4 h-auto mr-1" />
|
|
||||||
)}
|
|
||||||
{t("common.image")}
|
|
||||||
</Button>
|
|
||||||
<Button color="neutral" variant="outlined" onClick={handleCopyLinkBtnClick}>
|
|
||||||
<Icon.Link className="w-4 h-auto mr-1" />
|
|
||||||
{t("common.link")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="w-full border-t dark:border-zinc-700 overflow-clip">
|
|
||||||
<div
|
|
||||||
className="w-full h-auto select-none relative flex flex-col justify-start items-start bg-white dark:bg-zinc-800"
|
|
||||||
ref={memoElRef}
|
|
||||||
>
|
|
||||||
<span className="w-full px-6 pt-5 pb-2 text-sm text-gray-500">{memo.displayTsStr}</span>
|
|
||||||
<div className="w-full px-6 text-base pb-4">
|
|
||||||
<MemoContentV1 content={memo.content} />
|
|
||||||
<MemoResourceListView resourceList={memo.resourceList} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row justify-between items-center w-full bg-gray-100 dark:bg-zinc-700 py-4 px-6">
|
|
||||||
<div className="flex flex-row justify-start items-center">
|
|
||||||
<UserAvatar className="mr-2" avatarUrl={user.avatarUrl} />
|
|
||||||
<div className="w-auto grow truncate flex mr-2 flex-col justify-center items-start">
|
|
||||||
<span className="w-full text truncate font-medium text-gray-600 dark:text-gray-300">
|
|
||||||
{user.nickname || extractUsernameFromName(user.name)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-gray-500 dark:text-gray-400">via memos</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function showShareMemoDialog(memo: Memo): void {
|
|
||||||
generateDialog(
|
|
||||||
{
|
|
||||||
className: "share-memo-dialog",
|
|
||||||
dialogName: "share-memo-dialog",
|
|
||||||
},
|
|
||||||
ShareMemoDialog,
|
|
||||||
{ memo }
|
|
||||||
);
|
|
||||||
}
|
|
@ -5,9 +5,9 @@ import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers
|
|||||||
import * as utils from "@/helpers/utils";
|
import * as utils from "@/helpers/utils";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { useGlobalStore } from "@/store/module";
|
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 { useTranslate, Translations } from "@/utils/i18n";
|
||||||
import { useFilterStore, useMemoStore } from "../store/module";
|
import { useFilterStore } from "../store/module";
|
||||||
import "@/less/usage-heat-map.less";
|
import "@/less/usage-heat-map.less";
|
||||||
|
|
||||||
const tableConfig = {
|
const tableConfig = {
|
||||||
@ -36,7 +36,7 @@ const UsageHeatMap = () => {
|
|||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const userV1Store = useUserV1Store();
|
const userV1Store = useUserV1Store();
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoV1Store();
|
||||||
const todayTimeStamp = getDateStampByDate(Date.now());
|
const todayTimeStamp = getDateStampByDate(Date.now());
|
||||||
const weekDay = new Date(todayTimeStamp).getDay();
|
const weekDay = new Date(todayTimeStamp).getDay();
|
||||||
const weekFromMonday = ["zh-Hans", "ko"].includes(useGlobalStore().state.locale);
|
const weekFromMonday = ["zh-Hans", "ko"].includes(useGlobalStore().state.locale);
|
||||||
@ -45,12 +45,12 @@ const UsageHeatMap = () => {
|
|||||||
const nullCell = new Array(7 - todayDay).fill(0);
|
const nullCell = new Array(7 - todayDay).fill(0);
|
||||||
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
|
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
|
||||||
const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
|
const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
|
||||||
const memos = memoStore.state.memos;
|
|
||||||
const [memoAmount, setMemoAmount] = useState(0);
|
const [memoAmount, setMemoAmount] = useState(0);
|
||||||
const [createdDays, setCreatedDays] = useState(0);
|
const [createdDays, setCreatedDays] = useState(0);
|
||||||
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestamp));
|
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestamp));
|
||||||
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
|
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
|
||||||
const containerElRef = useRef<HTMLDivElement>(null);
|
const containerElRef = useRef<HTMLDivElement>(null);
|
||||||
|
const memos = Array.from(memoStore.getState().memoById.values());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userV1Store.getOrFetchUserByUsername(extractUsernameFromName(user.name)).then((user) => {
|
userV1Store.getOrFetchUserByUsername(extractUsernameFromName(user.name)).then((user) => {
|
||||||
|
@ -56,74 +56,10 @@ export function upsertUserSetting(upsert: UserSettingUpsert) {
|
|||||||
return axios.post<UserSetting>(`/api/v1/user/setting`, upsert);
|
return axios.post<UserSetting>(`/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<Memo[]>(`/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<Memo[]>(`/api/v1/memo?${queryList.join("&")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMemoStats(username: string) {
|
export function getMemoStats(username: string) {
|
||||||
return axios.get<number[]>(`/api/v1/memo/stats?creatorUsername=${username}`);
|
return axios.get<number[]>(`/api/v1/memo/stats?creatorUsername=${username}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMemoById(id: MemoId) {
|
|
||||||
return axios.get<Memo>(`/api/v1/memo/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMemo(memoCreate: MemoCreate) {
|
|
||||||
return axios.post<Memo>("/api/v1/memo", memoCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function patchMemo(memoPatch: MemoPatch) {
|
|
||||||
return axios.patch<Memo>(`/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) {
|
export function createResource(resourceCreate: ResourceCreate) {
|
||||||
return axios.post<Resource>("/api/v1/resource", resourceCreate);
|
return axios.post<Resource>("/api/v1/resource", resourceCreate);
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
"archived-memos": "Archived Memos",
|
"archived-memos": "Archived Memos",
|
||||||
"no-archived-memos": "No archived memos.",
|
"no-archived-memos": "No archived memos.",
|
||||||
"fetching-data": "Fetching data…",
|
"fetching-data": "Fetching data…",
|
||||||
"fetch-more": "Click here to fetch more",
|
"fetch-more": "Click to fetch more",
|
||||||
"archived-at": "Archived at",
|
"archived-at": "Archived at",
|
||||||
"search-placeholder": "Search memos",
|
"search-placeholder": "Search memos",
|
||||||
"visibility": {
|
"visibility": {
|
||||||
|
@ -1,56 +1,42 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import Empty from "@/components/Empty";
|
import Empty from "@/components/Empty";
|
||||||
import Memo from "@/components/Memo";
|
|
||||||
import MemoFilter from "@/components/MemoFilter";
|
import MemoFilter from "@/components/MemoFilter";
|
||||||
|
import MemoViewV1 from "@/components/MemoViewV1";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
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";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
const Explore = () => {
|
const Explore = () => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
|
const user = useCurrentUser();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoV1Store();
|
||||||
const filter = filterStore.state;
|
const [memos, setMemos] = useState<Memo[]>([]);
|
||||||
const { loadingStatus, memos } = memoStore.state;
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
const { text: textQuery } = filter;
|
const [isRequesting, setIsRequesting] = useState(false);
|
||||||
const fetchMoreRef = useRef<HTMLSpanElement>(null);
|
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
memoStore.setLoadingStatus("incomplete");
|
fetchMemos();
|
||||||
}, []);
|
}, [tagQuery, textQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchMemos = async () => {
|
||||||
if (!fetchMoreRef.current) return;
|
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
|
||||||
|
if (tagQuery) filters.push(`tags == "${tagQuery}"`);
|
||||||
const observer = new IntersectionObserver(([entry]) => {
|
if (textQuery) filters.push(`content_search == "${textQuery}"`);
|
||||||
if (!entry.isIntersecting) return;
|
setIsRequesting(true);
|
||||||
observer.disconnect();
|
const data = await memoStore.fetchMemos({
|
||||||
handleFetchMoreClick();
|
limit: DEFAULT_MEMO_LIMIT,
|
||||||
|
offset: memos.length,
|
||||||
|
filter: filters.join(" && "),
|
||||||
});
|
});
|
||||||
observer.observe(fetchMoreRef.current);
|
setIsRequesting(false);
|
||||||
|
setMemos([...memos, ...data]);
|
||||||
return () => observer.disconnect();
|
setIsComplete(data.length < DEFAULT_MEMO_LIMIT);
|
||||||
}, [loadingStatus]);
|
|
||||||
|
|
||||||
const handleFetchMoreClick = async () => {
|
|
||||||
try {
|
|
||||||
await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, sortedMemos.length);
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.response.data.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,30 +44,27 @@ const Explore = () => {
|
|||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
<div className="relative w-full h-auto flex flex-col justify-start items-start px-4 sm:px-6">
|
<div className="relative w-full h-auto flex flex-col justify-start items-start px-4 sm:px-6">
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
{sortedMemos.map((memo) => (
|
{memos.map((memo) => (
|
||||||
<Memo key={memo.id} memo={memo} lazyRendering showCreator showParent />
|
<MemoViewV1 key={memo.id} memo={memo} lazyRendering showCreator showParent />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{loadingStatus === "fetching" ? (
|
{isRequesting && (
|
||||||
<div className="flex flex-col justify-start items-center w-full mt-2 mb-1">
|
<div className="flex flex-col justify-start items-center w-full my-8">
|
||||||
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<div className="flex flex-col justify-start items-center w-full my-6">
|
{isComplete ? (
|
||||||
<div className="text-sm text-gray-400 italic">
|
memos.length === 0 && (
|
||||||
{loadingStatus === "complete" ? (
|
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
||||||
sortedMemos.length === 0 && (
|
<Empty />
|
||||||
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
||||||
<Empty />
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span ref={fetchMoreRef} className="cursor-pointer hover:text-green-600" onClick={handleFetchMoreClick}>
|
|
||||||
{t("memo.fetch-more")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-row justify-center items-center my-2">
|
||||||
|
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
|
||||||
|
{t("memo.fetch-more")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,20 +1,85 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Empty from "@/components/Empty";
|
||||||
import HomeSidebar from "@/components/HomeSidebar";
|
import HomeSidebar from "@/components/HomeSidebar";
|
||||||
import HomeSidebarDrawer from "@/components/HomeSidebarDrawer";
|
import HomeSidebarDrawer from "@/components/HomeSidebarDrawer";
|
||||||
import MemoEditor from "@/components/MemoEditor";
|
import MemoEditorV1 from "@/components/MemoEditorV1";
|
||||||
import MemoList from "@/components/MemoList";
|
import MemoFilter from "@/components/MemoFilter";
|
||||||
|
import MemoViewV1 from "@/components/MemoViewV1";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
|
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
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 Home = () => {
|
||||||
|
const t = useTranslate();
|
||||||
const { md } = useResponsiveWidth();
|
const { md } = useResponsiveWidth();
|
||||||
|
const user = useCurrentUser();
|
||||||
|
const filterStore = useFilterStore();
|
||||||
|
const memoStore = useMemoV1Store();
|
||||||
|
const [memos, setMemos] = useState<Memo[]>([]);
|
||||||
|
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 (
|
return (
|
||||||
<div className="w-full max-w-5xl flex flex-row justify-center items-start">
|
<div className="w-full max-w-5xl flex flex-row justify-center items-start">
|
||||||
<div className="w-full sm:pt-3 md:pt-6">
|
<div className="w-full sm:pt-3 md:pt-6">
|
||||||
<MobileHeader>{!md && <HomeSidebarDrawer />}</MobileHeader>
|
<MobileHeader>{!md && <HomeSidebarDrawer />}</MobileHeader>
|
||||||
<div className="w-full px-4 sm:px-6 md:pr-2">
|
<div className="w-full px-4 sm:px-6 md:pr-2">
|
||||||
<MemoEditor className="mb-2" cacheKey="home-memo-editor" />
|
<MemoEditorV1 className="mb-2" cacheKey="home-memo-editor" onConfirm={handleMemoCreated} />
|
||||||
<MemoList />
|
<div className="flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar">
|
||||||
|
<MemoFilter />
|
||||||
|
{memos.map((memo) => (
|
||||||
|
<MemoViewV1 key={memo.id} memo={memo} lazyRendering showVisibility showPinnedStyle showParent />
|
||||||
|
))}
|
||||||
|
{isRequesting && (
|
||||||
|
<div className="flex flex-col justify-start items-center w-full my-8">
|
||||||
|
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isComplete ? (
|
||||||
|
memos.length === 0 && (
|
||||||
|
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
||||||
|
<Empty />
|
||||||
|
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-row justify-center items-center my-2">
|
||||||
|
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
|
||||||
|
{t("memo.fetch-more")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{md && (
|
{md && (
|
||||||
|
@ -5,8 +5,8 @@ import { toast } from "react-hot-toast";
|
|||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import Icon from "@/components/Icon";
|
import Icon from "@/components/Icon";
|
||||||
import MemoContentV1 from "@/components/MemoContentV1";
|
import MemoContentV1 from "@/components/MemoContentV1";
|
||||||
import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog";
|
|
||||||
import MemoEditorV1 from "@/components/MemoEditorV1";
|
import MemoEditorV1 from "@/components/MemoEditorV1";
|
||||||
|
import showMemoEditorDialog from "@/components/MemoEditorV1/MemoEditorDialog";
|
||||||
import MemoRelationListViewV1 from "@/components/MemoRelationListViewV1";
|
import MemoRelationListViewV1 from "@/components/MemoRelationListViewV1";
|
||||||
import MemoResourceListView from "@/components/MemoResourceListView";
|
import MemoResourceListView from "@/components/MemoResourceListView";
|
||||||
import MemoViewV1 from "@/components/MemoViewV1";
|
import MemoViewV1 from "@/components/MemoViewV1";
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useParams } from "react-router-dom";
|
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 MobileHeader from "@/components/MobileHeader";
|
||||||
import UserAvatar from "@/components/UserAvatar";
|
import UserAvatar from "@/components/UserAvatar";
|
||||||
|
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
import useLoading from "@/hooks/useLoading";
|
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 { User } from "@/types/proto/api/v2/user_service";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
@ -15,6 +19,12 @@ const UserProfile = () => {
|
|||||||
const userV1Store = useUserV1Store();
|
const userV1Store = useUserV1Store();
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
|
const filterStore = useFilterStore();
|
||||||
|
const memoStore = useMemoV1Store();
|
||||||
|
const [memos, setMemos] = useState<Memo[]>([]);
|
||||||
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
|
const [isRequesting, setIsRequesting] = useState(false);
|
||||||
|
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const username = params.username;
|
const username = params.username;
|
||||||
@ -34,6 +44,29 @@ const UserProfile = () => {
|
|||||||
});
|
});
|
||||||
}, [params.username]);
|
}, [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 (
|
return (
|
||||||
<section className="w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
|
<section className="w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
|
||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
@ -45,7 +78,28 @@ const UserProfile = () => {
|
|||||||
<UserAvatar className="!w-20 !h-20 mb-2 drop-shadow" avatarUrl={user?.avatarUrl} />
|
<UserAvatar className="!w-20 !h-20 mb-2 drop-shadow" avatarUrl={user?.avatarUrl} />
|
||||||
<p className="text-3xl text-black opacity-80 dark:text-gray-200">{user?.nickname}</p>
|
<p className="text-3xl text-black opacity-80 dark:text-gray-200">{user?.nickname}</p>
|
||||||
</div>
|
</div>
|
||||||
<MemoList />
|
{memos.map((memo) => (
|
||||||
|
<MemoViewV1 key={memo.id} memo={memo} lazyRendering showVisibility showPinnedStyle showParent />
|
||||||
|
))}
|
||||||
|
{isRequesting && (
|
||||||
|
<div className="flex flex-col justify-start items-center w-full my-8">
|
||||||
|
<p className="text-sm text-gray-400 italic">{t("memo.fetching-data")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isComplete ? (
|
||||||
|
memos.length === 0 && (
|
||||||
|
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
||||||
|
<Empty />
|
||||||
|
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-row justify-center items-center my-2">
|
||||||
|
<span className="cursor-pointer text-sm italic text-gray-500 hover:text-green-600" onClick={fetchMemos}>
|
||||||
|
{t("memo.fetch-more")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p>Not found</p>
|
<p>Not found</p>
|
||||||
|
@ -3,14 +3,12 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
|||||||
import dialogReducer from "./reducer/dialog";
|
import dialogReducer from "./reducer/dialog";
|
||||||
import filterReducer from "./reducer/filter";
|
import filterReducer from "./reducer/filter";
|
||||||
import globalReducer from "./reducer/global";
|
import globalReducer from "./reducer/global";
|
||||||
import memoReducer from "./reducer/memo";
|
|
||||||
import resourceReducer from "./reducer/resource";
|
import resourceReducer from "./reducer/resource";
|
||||||
import tagReducer from "./reducer/tag";
|
import tagReducer from "./reducer/tag";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
global: globalReducer,
|
global: globalReducer,
|
||||||
memo: memoReducer,
|
|
||||||
tag: tagReducer,
|
tag: tagReducer,
|
||||||
filter: filterReducer,
|
filter: filterReducer,
|
||||||
resource: resourceReducer,
|
resource: resourceReducer,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
export * from "./global";
|
export * from "./global";
|
||||||
export * from "./filter";
|
export * from "./filter";
|
||||||
export * from "./memo";
|
|
||||||
export * from "./tag";
|
export * from "./tag";
|
||||||
export * from "./resource";
|
export * from "./resource";
|
||||||
export * from "./dialog";
|
export * from "./dialog";
|
||||||
|
@ -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<Memo[]> => {
|
|
||||||
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<Memo> => {
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
@ -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<LoadingStatus>) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loadingStatus: action.payload,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
upsertMemos: (state, action: PayloadAction<Memo[]>) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
memos: uniqBy([...action.payload, ...state.memos], "id"),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
createMemo: (state, action: PayloadAction<Memo>) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
memos: state.memos.concat(action.payload),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
patchMemo: (state, action: PayloadAction<Partial<Memo>>) => {
|
|
||||||
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<MemoId>) => {
|
|
||||||
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;
|
|
@ -1,5 +1,4 @@
|
|||||||
export * from "./user";
|
export * from "./user";
|
||||||
export * from "./memo";
|
export * from "./memo";
|
||||||
export * from "./memoCache";
|
|
||||||
export * from "./inbox";
|
export * from "./inbox";
|
||||||
export * from "./resourceName";
|
export * from "./resourceName";
|
||||||
|
@ -10,7 +10,7 @@ export const useMemoV1Store = create(
|
|||||||
const { memos } = await memoServiceClient.listMemos(request);
|
const { memos } = await memoServiceClient.listMemos(request);
|
||||||
return memos;
|
return memos;
|
||||||
},
|
},
|
||||||
getOrFetchMemoById: async (id: MemoId) => {
|
getOrFetchMemoById: async (id: number) => {
|
||||||
const memo = get().memoById.get(id);
|
const memo = get().memoById.get(id);
|
||||||
if (memo) {
|
if (memo) {
|
||||||
return memo;
|
return memo;
|
||||||
|
@ -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<MemoId, Memo>() }, (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;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
);
|
|
1
web/src/types/memo.d.ts
vendored
1
web/src/types/memo.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
type MemoSpecType = "NOT_TAGGED" | "LINKED" | "IMAGED" | "CONNECTED";
|
|
2
web/src/types/modules/common.d.ts
vendored
2
web/src/types/modules/common.d.ts
vendored
@ -1 +1,3 @@
|
|||||||
type RowStatus = "NORMAL" | "ARCHIVED";
|
type RowStatus = "NORMAL" | "ARCHIVED";
|
||||||
|
|
||||||
|
type Visibility = "PUBLIC" | "PROTECTED" | "PRIVATE";
|
||||||
|
48
web/src/types/modules/memo.d.ts
vendored
48
web/src/types/modules/memo.d.ts
vendored
@ -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;
|
|
||||||
}
|
|
12
web/src/types/modules/memoRelation.d.ts
vendored
12
web/src/types/modules/memoRelation.d.ts
vendored
@ -1,12 +0,0 @@
|
|||||||
type MemoRelationType = "REFERENCE" | "COMMENT";
|
|
||||||
|
|
||||||
interface MemoRelation {
|
|
||||||
memoId: MemoId;
|
|
||||||
relatedMemoId: MemoId;
|
|
||||||
type: MemoRelationType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MemoRelationUpsert {
|
|
||||||
relatedMemoId: MemoId;
|
|
||||||
type: MemoRelationType;
|
|
||||||
}
|
|
Reference in New Issue
Block a user