diff --git a/api/v2/memo_service.go b/api/v2/memo_service.go index 0b5dafeb..3ee7ad24 100644 --- a/api/v2/memo_service.go +++ b/api/v2/memo_service.go @@ -56,8 +56,8 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) } - if filter.Visibility != nil { - memoFind.VisibilityList = []store.Visibility{*filter.Visibility} + if len(filter.ContentSearch) > 0 { + memoFind.ContentSearch = filter.ContentSearch } if len(filter.Visibilities) > 0 { memoFind.VisibilityList = filter.Visibilities @@ -84,12 +84,6 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos } memoFind.CreatorID = &user.ID } - if filter.Tag != nil { - memoFind.ContentSearch = append(memoFind.ContentSearch, fmt.Sprintf("#%s", *filter.Tag)) - } - if filter.ContentSearch != nil { - memoFind.ContentSearch = append(memoFind.ContentSearch, *filter.ContentSearch) - } if filter.RowStatus != nil { memoFind.RowStatus = filter.RowStatus } @@ -198,6 +192,14 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe } else if path == "created_ts" { createdTs := request.Memo.CreateTime.AsTime().Unix() update.CreatedTs = &createdTs + } else if path == "pinned" { + if _, err := s.Store.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{ + MemoID: request.Id, + UserID: user.ID, + Pinned: request.Memo.Pinned, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert memo organizer") + } } } @@ -512,22 +514,20 @@ func convertVisibilityToStore(visibility apiv2pb.Visibility) store.Visibility { // ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter. var ListMemosFilterCELAttributes = []cel.EnvOption{ - cel.Variable("visibility", cel.StringType), cel.Variable("visibilities", cel.ListType(cel.StringType)), cel.Variable("created_ts_before", cel.IntType), cel.Variable("created_ts_after", cel.IntType), cel.Variable("creator", cel.StringType), + cel.Variable("content_search", cel.ListType(cel.StringType)), cel.Variable("row_status", cel.StringType), } type ListMemosFilter struct { - Visibility *store.Visibility + ContentSearch []string Visibilities []store.Visibility CreatedTsBefore *int64 CreatedTsAfter *int64 Creator *string - Tag *string - ContentSearch *string RowStatus *store.RowStatus } @@ -554,39 +554,30 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) { if len(callExpr.Args) == 2 { idExpr := callExpr.Args[0].GetIdentExpr() if idExpr != nil { - if idExpr.Name == "visibility" { - visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue()) - filter.Visibility = &visibility - } - if idExpr.Name == "visibilities" { + if idExpr.Name == "content_search" { + contentSearch := []string{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + contentSearch = append(contentSearch, value) + } + filter.ContentSearch = contentSearch + } else 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" { + } else if idExpr.Name == "created_ts_before" { createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value() filter.CreatedTsBefore = &createdTsBefore - } - if idExpr.Name == "created_ts_after" { + } else if idExpr.Name == "created_ts_after" { createdTsAfter := callExpr.Args[1].GetConstExpr().GetInt64Value() filter.CreatedTsAfter = &createdTsAfter - } - if idExpr.Name == "creator" { + } else if idExpr.Name == "creator" { creator := callExpr.Args[1].GetConstExpr().GetStringValue() filter.Creator = &creator - } - if idExpr.Name == "tag" { - tag := callExpr.Args[1].GetConstExpr().GetStringValue() - filter.Tag = &tag - } - if idExpr.Name == "content_search" { - contentSearch := callExpr.Args[1].GetConstExpr().GetStringValue() - filter.ContentSearch = &contentSearch - } - if idExpr.Name == "row_status" { + } else if idExpr.Name == "row_status" { rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue()) filter.RowStatus = &rowStatus } diff --git a/proto/api/v2/memo_service.proto b/proto/api/v2/memo_service.proto index 6c3449d5..a748432c 100644 --- a/proto/api/v2/memo_service.proto +++ b/proto/api/v2/memo_service.proto @@ -135,7 +135,7 @@ message ListMemosRequest { int32 limit = 2; // Filter is used to filter memos returned in the list. - // Format: "creator == users/{username} && visibility == PUBLIC" + // Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']" string filter = 3; } diff --git a/proto/gen/api/v2/README.md b/proto/gen/api/v2/README.md index 43c35232..56b5cf9d 100644 --- a/proto/gen/api/v2/README.md +++ b/proto/gen/api/v2/README.md @@ -1762,7 +1762,7 @@ | ----- | ---- | ----- | ----------- | | offset | [int32](#int32) | | offset is the offset of the first memo to return. | | limit | [int32](#int32) | | limit is the maximum number of memos to return. | -| filter | [string](#string) | | Filter is used to filter memos returned in the list. Format: "creator == users/{username} && visibility == PUBLIC" | +| filter | [string](#string) | | Filter is used to filter memos returned in the list. Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']" | diff --git a/proto/gen/api/v2/memo_service.pb.go b/proto/gen/api/v2/memo_service.pb.go index a8ccdfce..e9cda261 100644 --- a/proto/gen/api/v2/memo_service.pb.go +++ b/proto/gen/api/v2/memo_service.pb.go @@ -316,7 +316,7 @@ type ListMemosRequest struct { // limit is the maximum number of memos to return. Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` // Filter is used to filter memos returned in the list. - // Format: "creator == users/{username} && visibility == PUBLIC" + // Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']" Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` } diff --git a/store/db/sqlite/memo.go b/store/db/sqlite/memo.go index 10351862..1e1a3603 100644 --- a/store/db/sqlite/memo.go +++ b/store/db/sqlite/memo.go @@ -47,7 +47,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo } if v := find.ContentSearch; len(v) != 0 { for _, s := range v { - where, args = append(where, "memo.content LIKE ?"), append(args, "%"+s+"%") + where, args = append(where, "memo.content LIKE ?"), append(args, fmt.Sprintf("%%%s%%", s)) } } if v := find.VisibilityList; len(v) != 0 { diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 2c4ecffb..80dcf0c9 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -1,9 +1,10 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Empty from "@/components/Empty"; import MemoFilter from "@/components/MemoFilter"; import MemoViewV1 from "@/components/MemoViewV1"; import MobileHeader from "@/components/MobileHeader"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; +import { getTimeStampByDate } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useFilterStore } from "@/store/module"; import { useMemoV1Store } from "@/store/v1"; @@ -15,27 +16,37 @@ const Explore = () => { const user = useCurrentUser(); const filterStore = useFilterStore(); const memoStore = useMemoV1Store(); - const [memos, setMemos] = useState([]); const [isComplete, setIsComplete] = useState(false); const [isRequesting, setIsRequesting] = useState(false); + const memosRef = useRef([]); const { tag: tagQuery, text: textQuery } = filterStore.state; + const sortedMemos = memosRef.current.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); useEffect(() => { + memosRef.current = []; fetchMemos(); }, [tagQuery, textQuery]); const fetchMemos = async () => { const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`]; - if (tagQuery) filters.push(`tags == "${tagQuery}"`); - if (textQuery) filters.push(`content_search == "${textQuery}"`); + const contentSearch: string[] = []; + if (tagQuery) { + contentSearch.push(`"#${tagQuery}"`); + } + if (textQuery) { + contentSearch.push(`"${textQuery}"`); + } + if (contentSearch.length > 0) { + filters.push(`content_search == [${contentSearch.join(", ")}]`); + } setIsRequesting(true); const data = await memoStore.fetchMemos({ limit: DEFAULT_MEMO_LIMIT, - offset: memos.length, + offset: memosRef.current.length, filter: filters.join(" && "), }); setIsRequesting(false); - setMemos([...memos, ...data]); + memosRef.current = [...memosRef.current, ...data]; setIsComplete(data.length < DEFAULT_MEMO_LIMIT); }; @@ -44,7 +55,7 @@ const Explore = () => {
- {memos.map((memo) => ( + {sortedMemos.map((memo) => ( ))} @@ -54,7 +65,7 @@ const Explore = () => {
)} {isComplete ? ( - memos.length === 0 && ( + sortedMemos.length === 0 && (

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

diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 8358e1e7..b4228066 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Empty from "@/components/Empty"; import HomeSidebar from "@/components/HomeSidebar"; import HomeSidebarDrawer from "@/components/HomeSidebarDrawer"; @@ -7,6 +7,7 @@ import MemoFilter from "@/components/MemoFilter"; import MemoViewV1 from "@/components/MemoViewV1"; import MobileHeader from "@/components/MobileHeader"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; +import { getTimeStampByDate } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import { useFilterStore } from "@/store/module"; @@ -20,33 +21,45 @@ const Home = () => { const user = useCurrentUser(); const filterStore = useFilterStore(); const memoStore = useMemoV1Store(); - const [memos, setMemos] = useState([]); const [isComplete, setIsComplete] = useState(false); const [isRequesting, setIsRequesting] = useState(false); + const memosRef = useRef([]); const { tag: tagQuery, text: textQuery } = filterStore.state; + const sortedMemos = memosRef.current + .sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)) + .sort((a, b) => Number(b.pinned) - Number(a.pinned)); useEffect(() => { + memosRef.current = []; 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}"`); + const contentSearch: string[] = []; + if (tagQuery) { + contentSearch.push(`"#${tagQuery}"`); + } + if (textQuery) { + contentSearch.push(`"${textQuery}"`); + } + if (contentSearch.length > 0) { + filters.push(`content_search == [${contentSearch.join(", ")}]`); + } setIsRequesting(true); const data = await memoStore.fetchMemos({ limit: DEFAULT_MEMO_LIMIT, - offset: memos.length, + offset: memosRef.current.length, filter: filters.join(" && "), }); setIsRequesting(false); - setMemos([...memos, ...data]); + memosRef.current = [...memosRef.current, ...data]; setIsComplete(data.length < DEFAULT_MEMO_LIMIT); }; const handleMemoCreated = async (memoId: number) => { const memo = await memoStore.getOrFetchMemoById(memoId); - setMemos([memo, ...memos]); + memosRef.current = [memo, ...memosRef.current]; }; return ( @@ -57,7 +70,7 @@ const Home = () => {
- {memos.map((memo) => ( + {sortedMemos.map((memo) => ( ))} {isRequesting && ( @@ -66,7 +79,7 @@ const Home = () => {
)} {isComplete ? ( - memos.length === 0 && ( + sortedMemos.length === 0 && (

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