parent
b7a6301c2e
commit
dd87d26bdc
|
@ -3,6 +3,7 @@
|
|||
### General
|
||||
- Feat: ノートの下書き機能
|
||||
- Feat: クリップ内でノートを検索できるように
|
||||
- Feat: Playを検索できるように
|
||||
|
||||
### Client
|
||||
- Feat: モデログを検索できるように
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { type FlashsRepository } from '@/models/_.js';
|
||||
import { type FlashLikesRepository, MiUser, type FlashsRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
|
||||
/**
|
||||
* MisskeyPlay関係のService
|
||||
|
@ -15,6 +18,11 @@ export class FlashService {
|
|||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -37,4 +45,43 @@ export class FlashService {
|
|||
|
||||
return await builder.getMany();
|
||||
}
|
||||
|
||||
public async myLikes(meId: MiUser['id'], opts: { sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number, limit?: number, search?: string | null }) {
|
||||
const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), opts.sinceId, opts.untilId, opts.sinceDate, opts.untilDate)
|
||||
.andWhere('like.userId = :meId', { meId })
|
||||
.leftJoinAndSelect('like.flash', 'flash');
|
||||
|
||||
if (opts.search != null) {
|
||||
for (const word of opts.search.trim().split(' ')) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('flash.title ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
qb.orWhere('flash.summary ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const likes = await query
|
||||
.limit(opts.limit)
|
||||
.getMany();
|
||||
|
||||
return likes;
|
||||
}
|
||||
|
||||
public async search(searchQuery: string, opts: { sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number, limit?: number }) {
|
||||
const query = this.queryService.makePaginationQuery(this.flashRepository.createQueryBuilder('flash'), opts.sinceId, opts.untilId, opts.sinceDate, opts.untilDate)
|
||||
.andWhere('flash.visibility = \'public\'');
|
||||
|
||||
for (const word of searchQuery.trim().split(' ')) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('flash.title ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
qb.orWhere('flash.summary ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
}));
|
||||
}
|
||||
|
||||
const result = await query
|
||||
.limit(opts.limit)
|
||||
.getMany();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,6 +208,7 @@ export * as 'flash/my-likes' from './endpoints/flash/my-likes.js';
|
|||
export * as 'flash/show' from './endpoints/flash/show.js';
|
||||
export * as 'flash/unlike' from './endpoints/flash/unlike.js';
|
||||
export * as 'flash/update' from './endpoints/flash/update.js';
|
||||
export * as 'flash/search' from './endpoints/flash/search.js';
|
||||
export * as 'following/create' from './endpoints/following/create.js';
|
||||
export * as 'following/delete' from './endpoints/following/delete.js';
|
||||
export * as 'following/invalidate' from './endpoints/following/invalidate.js';
|
||||
|
|
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
if (ps.search != null) {
|
||||
for (const word of ps.search!.trim().split(' ')) {
|
||||
for (const word of ps.search.trim().split(' ')) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
qb.orWhere('note.cw ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { FlashLikesRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { FlashService } from '@/core/FlashService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'flash'],
|
||||
|
@ -46,6 +45,7 @@ export const paramDef = {
|
|||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
search: { type: 'string', minLength: 1, maxLength: 100, nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -53,20 +53,18 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private flashLikeEntityService: FlashLikeEntityService,
|
||||
private queryService: QueryService,
|
||||
private flashService: FlashService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('like.userId = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('like.flash', 'flash');
|
||||
|
||||
const likes = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
const likes = await this.flashService.myLikes(me.id, {
|
||||
sinceId: ps.sinceId,
|
||||
untilId: ps.untilId,
|
||||
sinceDate: ps.sinceDate,
|
||||
untilDate: ps.untilDate,
|
||||
limit: ps.limit,
|
||||
search: ps.search,
|
||||
});
|
||||
|
||||
return this.flashLikeEntityService.packMany(likes, me);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { FlashService } from '@/core/FlashService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['flash'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Flash',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 },
|
||||
},
|
||||
required: ['query'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private flashService: FlashService,
|
||||
private flashEntityService: FlashEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const result = await this.flashService.search(ps.query, {
|
||||
sinceId: ps.sinceId,
|
||||
untilId: ps.untilId,
|
||||
sinceDate: ps.sinceDate,
|
||||
untilDate: ps.untilDate,
|
||||
limit: ps.limit,
|
||||
});
|
||||
|
||||
return await this.flashEntityService.packMany(result, me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,7 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<div v-if="tab === 'featured'">
|
||||
<div v-if="tab === 'search'">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
<MkPagination v-if="searchPaginator" v-slot="{items}" :key="searchKey" :paginator="searchPaginator">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'featured'">
|
||||
<MkPagination v-slot="{items}" :paginator="featuredFlashsPaginator">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
|
||||
|
@ -26,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<MkPagination v-slot="{items}" :paginator="likedFlashsPaginator">
|
||||
<MkPagination v-slot="{items}" :paginator="likedFlashsPaginator" withControl>
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
|
||||
</div>
|
||||
|
@ -37,10 +51,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
import { computed, markRaw, ref, shallowRef } from 'vue';
|
||||
import type { IPaginator } from '@/utility/paginator.js';
|
||||
import MkFlashPreview from '@/components/MkFlashPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
@ -50,6 +66,10 @@ const router = useRouter();
|
|||
|
||||
const tab = ref('featured');
|
||||
|
||||
const searchQuery = ref('');
|
||||
const searchPaginator = shallowRef<IPaginator | null>(null);
|
||||
const searchKey = ref(0);
|
||||
|
||||
const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', {
|
||||
limit: 5,
|
||||
offsetMode: true,
|
||||
|
@ -59,12 +79,28 @@ const myFlashsPaginator = markRaw(new Paginator('flash/my', {
|
|||
}));
|
||||
const likedFlashsPaginator = markRaw(new Paginator('flash/my-likes', {
|
||||
limit: 5,
|
||||
canSearch: true,
|
||||
searchParamName: 'search',
|
||||
}));
|
||||
|
||||
function create() {
|
||||
router.push('/play/new');
|
||||
}
|
||||
|
||||
function search() {
|
||||
if (searchQuery.value.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
searchPaginator.value = markRaw(new Paginator('flash/search', {
|
||||
params: {
|
||||
query: searchQuery.value,
|
||||
},
|
||||
}));
|
||||
|
||||
searchKey.value++;
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.create,
|
||||
|
@ -72,6 +108,10 @@ const headerActions = computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
}, {
|
||||
key: 'featured',
|
||||
title: i18n.ts._play.featured,
|
||||
icon: 'ti ti-flare',
|
||||
|
|
|
@ -1805,6 +1805,8 @@ declare namespace entities {
|
|||
FlashMyResponse,
|
||||
FlashMyLikesRequest,
|
||||
FlashMyLikesResponse,
|
||||
FlashSearchRequest,
|
||||
FlashSearchResponse,
|
||||
FlashShowRequest,
|
||||
FlashShowResponse,
|
||||
FlashUnlikeRequest,
|
||||
|
@ -2286,6 +2288,12 @@ type FlashMyRequest = operations['flash___my']['requestBody']['content']['applic
|
|||
// @public (undocumented)
|
||||
type FlashMyResponse = operations['flash___my']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type FlashSearchRequest = operations['flash___search']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type FlashSearchResponse = operations['flash___search']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
|
@ -2438,6 +2438,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
request<E extends 'flash/search', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -340,6 +340,8 @@ import type {
|
|||
FlashMyResponse,
|
||||
FlashMyLikesRequest,
|
||||
FlashMyLikesResponse,
|
||||
FlashSearchRequest,
|
||||
FlashSearchResponse,
|
||||
FlashShowRequest,
|
||||
FlashShowResponse,
|
||||
FlashUnlikeRequest,
|
||||
|
@ -869,6 +871,7 @@ export type Endpoints = {
|
|||
'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
|
||||
'flash/my': { req: FlashMyRequest; res: FlashMyResponse };
|
||||
'flash/my-likes': { req: FlashMyLikesRequest; res: FlashMyLikesResponse };
|
||||
'flash/search': { req: FlashSearchRequest; res: FlashSearchResponse };
|
||||
'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
|
||||
'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
|
||||
'flash/update': { req: FlashUpdateRequest; res: EmptyResponse };
|
||||
|
|
|
@ -343,6 +343,8 @@ export type FlashMyRequest = operations['flash___my']['requestBody']['content'][
|
|||
export type FlashMyResponse = operations['flash___my']['responses']['200']['content']['application/json'];
|
||||
export type FlashMyLikesRequest = operations['flash___my-likes']['requestBody']['content']['application/json'];
|
||||
export type FlashMyLikesResponse = operations['flash___my-likes']['responses']['200']['content']['application/json'];
|
||||
export type FlashSearchRequest = operations['flash___search']['requestBody']['content']['application/json'];
|
||||
export type FlashSearchResponse = operations['flash___search']['responses']['200']['content']['application/json'];
|
||||
export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
|
||||
export type FlashShowResponse = operations['flash___show']['responses']['200']['content']['application/json'];
|
||||
export type FlashUnlikeRequest = operations['flash___unlike']['requestBody']['content']['application/json'];
|
||||
|
|
|
@ -1997,6 +1997,15 @@ export type paths = {
|
|||
*/
|
||||
post: operations['flash___my-likes'];
|
||||
};
|
||||
'/flash/search': {
|
||||
/**
|
||||
* flash/search
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
post: operations['flash___search'];
|
||||
};
|
||||
'/flash/show': {
|
||||
/**
|
||||
* flash/show
|
||||
|
@ -21394,6 +21403,7 @@ export interface operations {
|
|||
untilId?: string;
|
||||
sinceDate?: number;
|
||||
untilDate?: number;
|
||||
search?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -21458,6 +21468,79 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
flash___search: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
query: string;
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string;
|
||||
sinceDate?: number;
|
||||
untilDate?: number;
|
||||
/** @default 5 */
|
||||
limit?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Flash'][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
flash___show: {
|
||||
requestBody: {
|
||||
content: {
|
||||
|
|
Loading…
Reference in New Issue