From 61df9d475202107a81f94eb2fd72ff009d5f1b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=83=BC=E3=81=B3=E3=82=93?= <tar.bin.master@gmail.com> Date: Tue, 11 Apr 2023 07:42:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E6=A4=9C=E7=B4=A2=E7=94=A8=E3=83=9A=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=81=A8API=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add channel search * move channel search to channel list page --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com> Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com> Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com> Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> Co-authored-by: xianon <xianon@hotmail.co.jp> Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com> Co-authored-by: YS <47836716+yszkst@users.noreply.github.com> Co-authored-by: Khsmty <me@khsmty.com> Co-authored-by: Soni L <EnderMoneyMod@gmail.com> Co-authored-by: mei23 <m@m544.net> Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com> Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com> --- locales/en-US.yml | 2 + locales/ja-JP.yml | 2 + packages/backend/src/server/api/endpoints.ts | 2 + .../server/api/endpoints/channels/search.ts | 67 +++++++++++++++++++ .../client/src/components/MkChannelList.vue | 31 +++++++++ packages/client/src/pages/channels.vue | 62 ++++++++++++++++- 6 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/channels/search.ts create mode 100644 packages/client/src/components/MkChannelList.vue diff --git a/locales/en-US.yml b/locales/en-US.yml index 8da0af981..7a23d2c99 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1282,6 +1282,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/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 3f82eb7a7..3baa560a1 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..a954ba224 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import type { ChannelsRepository } from '@/models/index.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.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; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private channelEntityService: ChannelEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.channelsRepository.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 => this.channelEntityService.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..408eab739 --- /dev/null +++ b/packages/client/src/components/MkChannelList.vue @@ -0,0 +1,31 @@ +<template> +<MkPagination :pagination="pagination"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ i18n.ts.notFound }}</div> + </div> + </template> + + <template #default="{ items }"> + <MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/> + </template> +</MkPagination> +</template> + +<script lang="ts" setup> +import MkChannelPreview from '@/components/MkChannelPreview.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import { i18n } from '@/i18n'; + +const props = withDefaults(defineProps<{ + pagination: Paging; + noGap?: boolean; + extractor?: (item: any) => any; +}>(), { + extractor: (item) => item, +}); +</script> + +<style lang="scss" scoped> +</style> diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index 2d8eeefcf..f243f9345 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -20,6 +20,24 @@ @swiper="setSwiperRef" @slide-change="onSlideChange" > + <swiper-slide> + <div class="_content grwlizim search"> + <div class="gaps"> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search"> + <template #prefix><i class="ti ti-search"></i></template> + </MkInput> + <MkRadios v-model="searchType" @update:model-value="search()"> + <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> + <option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option> + </MkRadios> + <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> + </div> + <MkFoldableSection v-if="channelPagination"> + <template #header>{{ i18n.ts.searchResult }}</template> + <MkChannelList :key="key" :pagination="channelPagination"/> + </MkFoldableSection> + </div> + </swiper-slide> <swiper-slide> <div class="_content grwlizim featured"> <MkPagination @@ -74,12 +92,16 @@ </template> <script lang="ts" setup> -import { computed, defineComponent, inject, watch } from "vue"; +import { computed, onMounted, defineComponent, inject, watch } from "vue"; import { Virtual } from "swiper"; import { Swiper, SwiperSlide } from "swiper/vue"; import MkChannelPreview from "@/components/MkChannelPreview.vue"; +import MkChannelList from '@/components/MkChannelList.vue'; import MkPagination from "@/components/MkPagination.vue"; +import MkInput from '@/components/MkInput.vue'; +import MkRadios from '@/components/MkRadios.vue'; import MkButton from "@/components/MkButton.vue"; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { useRouter } from "@/router"; import { definePageMetadata } from "@/scripts/page-metadata"; import { deviceKind } from "@/scripts/device-kind"; @@ -90,10 +112,24 @@ import "swiper/scss/virtual"; const router = useRouter(); -const tabs = ["featured", "following", "owned"]; -let tab = $ref(tabs[0]); +const tabs = ["search", "featured", "following", "owned"]; +let tab = $ref(tabs[1]); watch($$(tab), () => syncSlide(tabs.indexOf(tab))); +const props = defineProps<{ + query: string; + type?: string; +}>(); +let key = $ref(''); +let tab = $ref('search'); +let searchQuery = $ref(''); +let searchType = $ref('nameAndDescription'); +let channelPagination = $ref(); +onMounted(() => { + searchQuery = props.query ?? ''; + searchType = props.type ?? 'nameAndDescription'; +}); + const featuredPagination = { endpoint: "channels/featured" as const, limit: 10, @@ -108,6 +144,21 @@ const ownedPagination = { limit: 10, }; +async function search() { + const query = searchQuery.toString().trim(); + if (query == null || query === '') return; + const type = searchType.toString().trim(); + channelPagination = { + endpoint: 'channels/search', + limit: 10, + params: { + query: searchQuery, + type: type, + }, + }; + key = query + type; +} + function create() { router.push("/channels/new"); } @@ -121,6 +172,11 @@ const headerActions = $computed(() => [ ]); const headerTabs = $computed(() => [ + { + key: 'search', + title: i18n.ts.search, + icon: 'ti ti-search', + }, { key: "featured", title: i18n.ts._channel.featured,