From 855409332bb047678c4044d136c81b256b36158d Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sun, 19 Nov 2023 00:29:27 +0100 Subject: [PATCH] [backend] Add in:bookmarks and in:favorites postgres FTS filters --- locales/en-US.yml | 1 + .../server/api/common/generate-fts-query.ts | 44 ++++++++++++++++--- packages/client/src/pages/search-filters.vue | 7 +++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 405fbad6d..0b44c50ff 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1487,6 +1487,7 @@ _filters: title: "Advanced search filters" learnMore: "View advanced filters" wordFilters: "Filter by post text" + inFilters: "Filter by bookmark and/or favorite status" miscFilters: "Filter by following relationship and/or note type" userDomain: "Filter by author, mentioned users, reply user or instance domain" postDate: "Filter by post date" diff --git a/packages/backend/src/server/api/common/generate-fts-query.ts b/packages/backend/src/server/api/common/generate-fts-query.ts index 9188bdce5..6f089689d 100644 --- a/packages/backend/src/server/api/common/generate-fts-query.ts +++ b/packages/backend/src/server/api/common/generate-fts-query.ts @@ -1,7 +1,7 @@ import { Brackets, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm"; import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; import { sqlRegexEscape } from "@/misc/sql-regex-escape.js"; -import { Followings, Users } from "@/models/index.js"; +import { Followings, NoteFavorites, NoteReactions, Users } from "@/models/index.js"; const filters = { "from": fromFilter, @@ -24,11 +24,11 @@ const filters = { "-host": instanceFilterInverse, "filter": miscFilter, "-filter": miscFilterInverse, + "in": inFilter, + "-in": inFilterInverse, "has": attachmentFilter, } as Record, search: string, id: number) => any> -//TODO: editing the query should be possible, clicking search again resets it (it should be a twitter-like top of the page kind of deal) - export function generateFtsQuery(query: SelectQueryBuilder, q: string): void { const components = q.trim().split(" "); const terms: string[] = []; @@ -156,11 +156,11 @@ function miscFilter(query: SelectQueryBuilder, filter: string) { if (filter === 'followers') { subQuery = Followings.createQueryBuilder('following') .select('following.followerId') - .where('following.followeeId = :meId') + .where('following.followeeId = :meId'); } else if (filter === 'following') { subQuery = Followings.createQueryBuilder('following') .select('following.followeeId') - .where('following.followerId = :meId') + .where('following.followerId = :meId'); } else if (filter === 'replies' || filter === 'reply') { query.andWhere('note.replyId IS NOT NULL'); } else if (filter === 'boosts' || filter === 'boost' || filter === 'renotes' || filter === 'renote') { @@ -175,11 +175,11 @@ function miscFilterInverse(query: SelectQueryBuilder, filter: string) { if (filter === 'followers') { subQuery = Followings.createQueryBuilder('following') .select('following.followerId') - .where('following.followeeId = :meId') + .where('following.followeeId = :meId'); } else if (filter === 'following') { subQuery = Followings.createQueryBuilder('following') .select('following.followeeId') - .where('following.followerId = :meId') + .where('following.followerId = :meId'); } else if (filter === 'replies' || filter === 'reply') { query.andWhere('note.replyId IS NULL'); } else if (filter === 'boosts' || filter === 'boost' || filter === 'renotes' || filter === 'renote') { @@ -189,6 +189,36 @@ function miscFilterInverse(query: SelectQueryBuilder, filter: string) { if (subQuery !== null) query.andWhere(`note.userId NOT IN (${subQuery.getQuery()})`); } +function inFilter(query: SelectQueryBuilder, filter: string) { + let subQuery: SelectQueryBuilder | null = null; + if (filter === 'bookmarks') { + subQuery = NoteFavorites.createQueryBuilder('bookmark') + .select('bookmark.noteId') + .where('bookmark.userId = :meId'); + } else if (filter === 'favorites' || filter === 'favourites' || filter === 'reactions' || filter === 'likes') { + subQuery = NoteReactions.createQueryBuilder('react') + .select('react.noteId') + .where('react.userId = :meId'); + } + + if (subQuery !== null) query.andWhere(`note.id IN (${subQuery.getQuery()})`); +} + +function inFilterInverse(query: SelectQueryBuilder, filter: string) { + let subQuery: SelectQueryBuilder | null = null; + if (filter === 'bookmarks') { + subQuery = NoteFavorites.createQueryBuilder('bookmark') + .select('bookmark.noteId') + .where('bookmark.userId = :meId'); + } else if (filter === 'favorites' || filter === 'favourites' || filter === 'reactions' || filter === 'likes') { + subQuery = NoteReactions.createQueryBuilder('react') + .select('react.noteId') + .where('react.userId = :meId'); + } + + if (subQuery !== null) query.andWhere(`note.id NOT IN (${subQuery.getQuery()})`); +} + function attachmentFilter(query: SelectQueryBuilder, filter: string) { switch(filter) { case 'image': diff --git a/packages/client/src/pages/search-filters.vue b/packages/client/src/pages/search-filters.vue index 8191a6e6b..1be6042e5 100644 --- a/packages/client/src/pages/search-filters.vue +++ b/packages/client/src/pages/search-filters.vue @@ -34,6 +34,12 @@

[-]instance:local|domain.tld

+
+
{{ i18n.ts._filters._dialog.inFilters }}
+
+

[-]in:bookmarks|favorites

+
+
{{ i18n.ts._filters._dialog.miscFilters }}
@@ -59,6 +65,7 @@
{{ i18n.ts._filters._dialog.infoEnd }}

{{ i18n.ts._filters._dialog.infoEnd1 }}

+

in:likes = in:reactions = in:favourites = in:favorites

filter:reply = filter:replies

search:word[s] = match:word[s]

domain: = host: = instance: