summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorおさむのひと <46447427+samunohito@users.noreply.github.com>2024-10-05 14:37:52 +0900
committerGitHub <noreply@github.com>2024-10-05 14:37:52 +0900
commit0d7d1091c8970d9979e8efb02f0accd6dcd39422 (patch)
treec76208e6b85a579f414a2fd7cac1bc1bb51f67ef
parent#14675 レビューの修正 (#14705) (diff)
downloadsharkey-0d7d1091c8970d9979e8efb02f0accd6dcd39422.tar.gz
sharkey-0d7d1091c8970d9979e8efb02f0accd6dcd39422.tar.bz2
sharkey-0d7d1091c8970d9979e8efb02f0accd6dcd39422.zip
enhance: 人気のPlayを10件以上表示できるように (#14443)
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
-rw-r--r--CHANGELOG.md1
-rw-r--r--packages/backend/src/core/CoreModule.ts5
-rw-r--r--packages/backend/src/core/FlashService.ts40
-rw-r--r--packages/backend/src/core/entities/FlashEntityService.ts41
-rw-r--r--packages/backend/src/models/Flash.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/flash/featured.ts22
-rw-r--r--packages/backend/test/unit/FlashService.ts152
-rw-r--r--packages/frontend/src/pages/flash/flash-index.vue3
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md4
-rw-r--r--packages/misskey-js/src/autogen/endpoint.ts3
-rw-r--r--packages/misskey-js/src/autogen/entities.ts1
-rw-r--r--packages/misskey-js/src/autogen/types.ts10
12 files changed, 262 insertions, 25 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04acc11ac3..6a9143ea1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
- Enhance: 依存関係の更新
- Enhance: l10nの更新
+- Enhance: Playの「人気」タブで10件以上表示可能に #14399
- Fix: 連合のホワイトリストが正常に登録されない問題を修正
### Client
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 3b3c35f976..734d135648 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { FlashService } from '@/core/FlashService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
+const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
@@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookTestService,
$UtilityService,
$FileInfoService,
+ $FlashService,
$SearchService,
$ClipService,
$FeaturedService,
@@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts
new file mode 100644
index 0000000000..2a98225382
--- /dev/null
+++ b/packages/backend/src/core/FlashService.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { type FlashsRepository } from '@/models/_.js';
+
+/**
+ * MisskeyPlay関係のService
+ */
+@Injectable()
+export class FlashService {
+ constructor(
+ @Inject(DI.flashsRepository)
+ private flashRepository: FlashsRepository,
+ ) {
+ }
+
+ /**
+ * 人気のあるPlay一覧を取得する.
+ */
+ public async featured(opts?: { offset?: number, limit: number }) {
+ const builder = this.flashRepository.createQueryBuilder('flash')
+ .andWhere('flash.likedCount > 0')
+ .andWhere('flash.visibility = :visibility', { visibility: 'public' })
+ .addOrderBy('flash.likedCount', 'DESC')
+ .addOrderBy('flash.updatedAt', 'DESC')
+ .addOrderBy('flash.id', 'DESC');
+
+ if (opts?.offset) {
+ builder.skip(opts.offset);
+ }
+
+ builder.take(opts?.limit ?? 10);
+
+ return await builder.getMany();
+ }
+}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index 4aa7104c1e..0cdcf3310a 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -5,10 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
-import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFlash } from '@/models/Flash.js';
import { bindThis } from '@/decorators.js';
@@ -20,10 +18,8 @@ export class FlashEntityService {
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
-
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
-
private userEntityService: UserEntityService,
private idService: IdService,
) {
@@ -34,25 +30,36 @@ export class FlashEntityService {
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
- packedUser?: Packed<'UserLite'>
+ packedUser?: Packed<'UserLite'>,
+ likedFlashIds?: MiFlash['id'][],
},
): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
- return await awaitAll({
+ // { schema: 'UserDetailed' } すると無限ループするので注意
+ const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
+
+ let isLiked = false;
+ if (meId) {
+ isLiked = hint?.likedFlashIds
+ ? hint.likedFlashIds.includes(flash.id)
+ : await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
+ }
+
+ return {
id: flash.id,
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
- user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: user,
title: flash.title,
summary: flash.summary,
script: flash.script,
visibility: flash.visibility,
likedCount: flash.likedCount,
- isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
- });
+ isLiked: isLiked,
+ };
}
@bindThis
@@ -63,7 +70,19 @@ export class FlashEntityService {
const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
- return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
+ const _likedFlashIds = me
+ ? await this.flashLikesRepository.createQueryBuilder('flashLike')
+ .select('flashLike.flashId')
+ .where('flashLike.userId = :userId', { userId: me.id })
+ .getRawMany<{ flashLike_flashId: string }>()
+ .then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
+ : [];
+ return Promise.all(
+ flashes.map(flash => this.pack(flash, me, {
+ packedUser: _userMap.get(flash.userId),
+ likedFlashIds: _likedFlashIds,
+ })),
+ );
}
}
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index a1469a0d94..5db7dca992 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
import { id } from './util/id.js';
import { MiUser } from './User.js';
+export const flashVisibility = ['public', 'private'] as const;
+export type FlashVisibility = typeof flashVisibility[number];
+
@Entity('flash')
export class MiFlash {
@PrimaryColumn(id())
@@ -63,5 +66,5 @@ export class MiFlash {
@Column('varchar', {
length: 512, default: 'public',
})
- public visibility: 'public' | 'private';
+ public visibility: FlashVisibility;
}
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index c2d6ab5085..9a0cb461f2 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
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'],
@@ -27,26 +28,25 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ offset: { type: 'integer', minimum: 0, default: 0 },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.flashsRepository)
- private flashsRepository: FlashsRepository,
-
+ private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.flashsRepository.createQueryBuilder('flash')
- .andWhere('flash.likedCount > 0')
- .orderBy('flash.likedCount', 'DESC');
-
- const flashs = await query.limit(10).getMany();
-
- return await this.flashEntityService.packMany(flashs, me);
+ const result = await this.flashService.featured({
+ offset: ps.offset,
+ limit: ps.limit,
+ });
+ return await this.flashEntityService.packMany(result, me);
});
}
}
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
new file mode 100644
index 0000000000..12ffaf3421
--- /dev/null
+++ b/packages/backend/test/unit/FlashService.ts
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { FlashService } from '@/core/FlashService.js';
+import { IdService } from '@/core/IdService.js';
+import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+
+describe('FlashService', () => {
+ let app: TestingModule;
+ let service: FlashService;
+
+ // --------------------------------------------------------------------------------------
+
+ let flashsRepository: FlashsRepository;
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let idService: IdService;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+ let alice: MiUser;
+ let bob: MiUser;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createFlash(data: Partial<MiFlash>) {
+ return flashsRepository.insert({
+ id: idService.gen(),
+ updatedAt: new Date(),
+ userId: root.id,
+ title: 'title',
+ summary: 'summary',
+ script: 'script',
+ permissions: [],
+ likedCount: 0,
+ ...data,
+ }).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ async function createUser(data: Partial<MiUser> = {}) {
+ const user = await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeEach(async () => {
+ app = await Test.createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ FlashService,
+ IdService,
+ ],
+ }).compile();
+
+ service = app.get(FlashService);
+
+ flashsRepository = app.get(DI.flashsRepository);
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+ idService = app.get(IdService);
+
+ root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+ bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+ });
+
+ afterEach(async () => {
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ await flashsRepository.delete({});
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('featured', () => {
+ test('should return featured flashes', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash3, flash2, flash1]);
+ });
+
+ test('should return featured flashes public visibility only', async () => {
+ const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
+ const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
+ const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with offset', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 1,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with limit', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 2,
+ });
+
+ expect(result).toEqual([flash3, flash2]);
+ });
+ });
+});
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index f63a799365..2b85489706 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -55,7 +55,8 @@ const tab = ref('featured');
const featuredFlashsPagination = {
endpoint: 'flash/featured' as const,
- noPaging: true,
+ limit: 5,
+ offsetMode: true,
};
const myFlashsPagination = {
endpoint: 'flash/my' as const,
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 732352abd8..de52be3a61 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1680,6 +1680,7 @@ declare namespace entities {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -1930,6 +1931,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
// @public (undocumented)
+type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
// @public (undocumented)
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 42c74599a5..bf61c20628 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -465,6 +465,7 @@ import type {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -889,7 +890,7 @@ export type Endpoints = {
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
- 'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse };
+ 'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 87ed653d44..72c7c35ed4 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -468,6 +468,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 3876a0bfe5..0938973481 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -23799,6 +23799,16 @@ export type operations = {
* **Credential required**: *No*
*/
flash___featured: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 0 */
+ offset?: number;
+ /** @default 10 */
+ limit?: number;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {