diff --git a/locales/en-US.yml b/locales/en-US.yml index 1ade50c7f..f34855fcd 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1283,6 +1283,8 @@ _channel: following: "Followed" usersCount: "{n} Participants" notesCount: "{n} Posts" + nameAndDescription: "Name and description" + nameOnly: "Name only" _messaging: dms: "Private" groups: "Groups" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index cb01fb564..0eb17e5af 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1147,6 +1147,8 @@ _channel: following: "フォロー中" usersCount: "{n}人が参加中" notesCount: "{n}投稿があります" + nameAndDescription: "名前と説明" + nameOnly: "名前のみ" _messaging: dms: "プライベート" groups: "グループ" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 645f11f56..8ca19e596 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1070,6 +1070,8 @@ _channel: following: "正在关注" usersCount: "有{n}人参与" notesCount: "有{n}个帖子" + nameAndDescription: "名称与描述" + nameOnly: "仅名称" _menuDisplay: sideFull: "横向" sideIcon: "横向(图标)" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 477b8ceb3..42fec8279 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1073,6 +1073,8 @@ _channel: following: "關注中" usersCount: "有{n}人參與" notesCount: "有{n}個貼文" + nameAndDescription: "名稱與說明" + nameOnly: "僅名稱" _menuDisplay: sideFull: "側向" sideIcon: "側向(圖示)" diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts new file mode 100644 index 000000000..453947d6e --- /dev/null +++ b/packages/backend/src/misc/sql-like-escape.ts @@ -0,0 +1,3 @@ +export function sqlLikeEscape(s: string) { + return s.replace(/([%_])/g, "\\$1"); +} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 3f82eb7a7..e6f8f7ee6 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -89,6 +89,7 @@ import * as ep___channels_featured from "./endpoints/channels/featured.js"; import * as ep___channels_follow from "./endpoints/channels/follow.js"; import * as ep___channels_followed from "./endpoints/channels/followed.js"; import * as ep___channels_owned from "./endpoints/channels/owned.js"; +import * as ep___channels_search from "./endpoints/channels/search.js"; import * as ep___channels_show from "./endpoints/channels/show.js"; import * as ep___channels_timeline from "./endpoints/channels/timeline.js"; import * as ep___channels_unfollow from "./endpoints/channels/unfollow.js"; @@ -438,6 +439,7 @@ const eps = [ ["channels/follow", ep___channels_follow], ["channels/followed", ep___channels_followed], ["channels/owned", ep___channels_owned], + ["channels/search", ep___channels_search], ["channels/show", ep___channels_show], ["channels/timeline", ep___channels_timeline], ["channels/unfollow", ep___channels_unfollow], diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts new file mode 100644 index 000000000..b21fa7620 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -0,0 +1,69 @@ +import define from "../../define.js"; +import { Brackets } from "typeorm"; +import { Endpoint } from "@/server/api/endpoint-base.js"; +import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; +import { Channels } from "@/models/index.js"; +import { DI } from "@/di-symbols.js"; +import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; + +export const meta = { + tags: ["channels"], + + requireCredential: false, + + res: { + type: "array", + optional: false, + nullable: false, + items: { + type: "object", + optional: false, + nullable: false, + ref: "Channel", + }, + }, +} as const; + +export const paramDef = { + type: "object", + properties: { + query: { type: "string" }, + type: { + type: "string", + enum: ["nameAndDescription", "nameOnly"], + default: "nameAndDescription", + }, + sinceId: { type: "string", format: "misskey:id" }, + untilId: { type: "string", format: "misskey:id" }, + limit: { type: "integer", minimum: 1, maximum: 100, default: 5 }, + }, + required: ["query"], +} as const; + +export default define(meta, paramDef, async (ps, me) => { + const query = makePaginationQuery( + Channels.createQueryBuilder("channel"), + ps.sinceId, + ps.untilId, + ); + + if (ps.type === "nameAndDescription") { + query.andWhere( + new Brackets((qb) => { + qb.where("channel.name ILIKE :q", { + q: `%${sqlLikeEscape(ps.query)}%`, + }).orWhere("channel.description ILIKE :q", { + q: `%${sqlLikeEscape(ps.query)}%`, + }); + }), + ); + } else { + query.andWhere("channel.name ILIKE :q", { + q: `%${sqlLikeEscape(ps.query)}%`, + }); + } + + const channels = await query.take(ps.limit).getMany(); + + return await Promise.all(channels.map((x) => Channels.pack(x, me))); +}); diff --git a/packages/client/src/components/MkChannelList.vue b/packages/client/src/components/MkChannelList.vue new file mode 100644 index 000000000..0379c8103 --- /dev/null +++ b/packages/client/src/components/MkChannelList.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index 2d8eeefcf..705b99671 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -20,6 +20,52 @@ @swiper="setSwiperRef" @slide-change="onSlideChange" > + + +