diff --git a/.config/docker_example.env b/.config/docker_example.env index 7a0261524b..4fe8e76b78 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -2,3 +2,4 @@ POSTGRES_PASSWORD=example-misskey-pass POSTGRES_USER=example-misskey-user POSTGRES_DB=misskey +DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c5755315fc..d4678ec5e0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,7 +17,7 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 5 + open-pull-requests-limit: 10 # List dependencies required to be updated together, sharing the same version numbers. # Those who simply have the common owner (e.g. @fastify) don't need to be listed. groups: diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e1ac6a78..474fcad674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,14 +14,21 @@ ## 202x.x.x (Unreleased) +### General +- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加 +- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正 + ### Client +- Feat: 新しいゲームを追加 - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように +- Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Enhance: チャンネルノートのピン留めをノートのメニューからできるよ ### Server - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) +- Enhance: クリップをエクスポートできるように ## 2023.12.2 diff --git a/docker-compose_example.yml b/docker-compose_example.yml index 60ba4dc8ca..5cebbe4164 100644 --- a/docker-compose_example.yml +++ b/docker-compose_example.yml @@ -7,6 +7,7 @@ services: links: - db - redis +# - mcaptcha # - meilisearch depends_on: db: @@ -48,6 +49,36 @@ services: interval: 5s retries: 20 +# mcaptcha: +# restart: always +# image: mcaptcha/mcaptcha:latest +# networks: +# internal_network: +# external_network: +# aliases: +# - localhost +# ports: +# - 7493:7493 +# env_file: +# - .config/docker.env +# environment: +# PORT: 7493 +# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/" +# depends_on: +# db: +# condition: service_healthy +# mcaptcha_redis: +# condition: service_healthy +# +# mcaptcha_redis: +# image: mcaptcha/cache:latest +# networks: +# - internal_network +# healthcheck: +# test: "redis-cli ping" +# interval: 5s +# retries: 20 + # meilisearch: # restart: always # image: getmeili/meilisearch:v1.3.4 diff --git a/locales/index.d.ts b/locales/index.d.ts index 3937784153..7c73caaac9 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -382,6 +382,11 @@ export interface Locale { "enableHcaptcha": string; "hcaptchaSiteKey": string; "hcaptchaSecretKey": string; + "mcaptcha": string; + "enableMcaptcha": string; + "mcaptchaSiteKey": string; + "mcaptchaSecretKey": string; + "mcaptchaInstanceUrl": string; "recaptcha": string; "enableRecaptcha": string; "recaptchaSiteKey": string; @@ -1187,6 +1192,7 @@ export interface Locale { "decorate": string; "addMfmFunction": string; "enableQuickAddMfmFunction": string; + "bubbleGame": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1651,6 +1657,15 @@ export interface Locale { "title": string; "description": string; }; + "_bubbleGameExplodingHead": { + "title": string; + "description": string; + }; + "_bubbleGameDoubleExplodingHead": { + "title": string; + "description": string; + "flavor": string; + }; }; }; "_role": { @@ -2251,6 +2266,7 @@ export interface Locale { "_exportOrImport": { "allNotes": string; "favoritedNotes": string; + "clips": string; "followingList": string; "muteList": string; "blockingList": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 77f9a9ec0f..55ff3201f0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -379,6 +379,11 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptchaを有効にする" hcaptchaSiteKey: "サイトキー" hcaptchaSecretKey: "シークレットキー" +mcaptcha: "mCaptcha" +enableMcaptcha: "mCaptchaを有効にする" +mcaptchaSiteKey: "サイトキー" +mcaptchaSecretKey: "シークレットキー" +mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" @@ -1184,6 +1189,7 @@ seasonalScreenEffect: "季節に応じた画面の演出" decorate: "デコる" addMfmFunction: "装飾を追加" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" +bubbleGame: "バブルゲーム" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -1562,6 +1568,13 @@ _achievements: _tutorialCompleted: title: "Misskey初心者講座 修了証" description: "チュートリアルを完了した" + _bubbleGameExplodingHead: + title: "🤯" + description: "バブルゲームで最も大きいモノを出した" + _bubbleGameDoubleExplodingHead: + title: "ダブル🤯" + description: "バブルゲームで最も大きいモノを2つ同時に出した" + flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて" _role: new: "ロールの作成" @@ -2154,6 +2167,7 @@ _profile: _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" + clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" diff --git a/packages/backend/migration/1704373210054-support-mcaptcha.js b/packages/backend/migration/1704373210054-support-mcaptcha.js new file mode 100644 index 0000000000..ce42b90716 --- /dev/null +++ b/packages/backend/migration/1704373210054-support-mcaptcha.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SupportMcaptcha1704373210054 { + name = 'SupportMcaptcha1704373210054' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`); + } +} diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 3e9d19f825..c83845b94c 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { setTimeout } from 'node:timers/promises'; import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; @@ -12,6 +11,7 @@ import { DI } from './di-symbols.js'; import { Config, loadConfig } from './config.js'; import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; +import { allSettled } from './misc/promise-tracker.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; const $config: Provider = { @@ -33,7 +33,7 @@ const $meilisearch: Provider = { useFactory: (config: Config) => { if (config.meilisearch) { return new MeiliSearch({ - host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`, + host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.apiKey, }); } else { @@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, - ) {} + ) { } public async dispose(): Promise<void> { - if (process.env.NODE_ENV === 'test') { - // XXX: - // Shutting down the existing connections causes errors on Jest as - // Misskey has asynchronous postgres/redis connections that are not - // awaited. - // Let's wait for some random time for them to finish. - await setTimeout(5000); - } + // Wait for all potential DB queries + await allSettled(); + // And then disconnect from DB await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 88fc033859..a28b68ee86 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', + 'bubbleGameExplodingHead', + 'bubbleGameDoubleExplodingHead', ] as const; @Injectable() diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index f64196f4fc..6c5ee4835d 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -73,6 +73,37 @@ export class CaptchaService { } } + // https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go + @bindThis + public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> { + if (response == null) { + throw new Error('mcaptcha-failed: no response provided'); + } + + const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost); + const result = await this.httpRequestService.send(endpointUrl.toString(), { + method: 'POST', + body: JSON.stringify({ + key: siteKey, + secret: secret, + token: response, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (result.status !== 200) { + throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK'); + } + + const resp = (await result.json()) as { valid: boolean }; + + if (!resp.valid) { + throw new Error('mcaptcha-request-failed'); + } + } + @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index ed8d51df16..97fb80ab39 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { isReply } from '@/misc/is-reply.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -676,7 +677,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.relayService.deliverToRelays(user, noteActivity); } - dm.execute(); + trackPromise(dm.execute()); })(); } //#endregion diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 03c1735e04..c73cf76592 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; @Injectable() export class NoteReadService implements OnApplicationShutdown { @@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown { // TODO: ↓まとめてクエリしたい - this.noteUnreadsRepository.countBy({ + trackPromise(this.noteUnreadsRepository.countBy({ userId: userId, isMentioned: true, }).then(mentionsCount => { @@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown { // 全て既読になったイベントを発行 this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); } - }); + })); - this.noteUnreadsRepository.countBy({ + trackPromise(this.noteUnreadsRepository.countBy({ userId: userId, isSpecified: true, }).then(specifiedCount => { @@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown { // 全て既読になったイベントを発行 this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); } - }); + })); } } diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ad7be83e5b..765fcae063 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { UserListService } from '@/core/UserListService.js'; import type { FilterUnionByProperty } from '@/types.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; @Injectable() export class NotificationService implements OnApplicationShutdown { @@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - public async createNotification<T extends MiNotification['type']>( + public createNotification<T extends MiNotification['type']>( + notifieeId: MiUser['id'], + type: T, + data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>, + notifierId?: MiUser['id'] | null, + ) { + trackPromise( + this.#createNotificationInternal(notifieeId, type, data, notifierId), + ); + } + + async #createNotificationInternal<T extends MiNotification['type']>( notifieeId: MiUser['id'], type: T, data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>, diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 4444dc9787..20a53ff282 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { setTimeout } from 'node:timers/promises'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { QUEUE, baseQueueOptions } from '@/queue/const.js'; +import { allSettled } from '@/misc/promise-tracker.js'; import type { Provider } from '@nestjs/common'; import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; @@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown { ) {} public async dispose(): Promise<void> { - if (process.env.NODE_ENV === 'test') { - // XXX: - // Shutting down the existing connections causes errors on Jest as - // Misskey has asynchronous postgres/redis connections that are not - // awaited. - // Let's wait for some random time for them to finish. - await setTimeout(5000); - } + // Wait for all potential queue jobs + await allSettled(); + // And then close all queues await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 4f99dee64e..dc3f248da4 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -182,6 +182,16 @@ export class QueueService { }); } + @bindThis + public createExportClipsJob(user: ThinUser) { + return this.dbQueue.add('exportClips', { + user: { id: user.id }, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + @bindThis public createExportFavoritesJob(user: ThinUser) { return this.dbQueue.add('exportFavorites', { diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 3ca12551b1..2e8f76fa8a 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { RoleService } from '@/core/RoleService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; const FALLBACK = '❤'; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; @@ -268,7 +269,7 @@ export class ReactionService { } } - dm.execute(); + trackPromise(dm.execute()); } //#endregion } @@ -316,7 +317,7 @@ export class ReactionService { dm.addDirectRecipe(reactee as MiRemoteUser); } dm.addFollowersRecipe(); - dm.execute(); + trackPromise(dm.execute()); } //#endregion } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 81003bcf1c..d7414e9c99 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -144,7 +144,7 @@ class DeliverManager { } // deliver - this.queueService.deliverMany(this.actor, this.activity, inboxes); + await this.queueService.deliverMany(this.actor, this.activity, inboxes); } } diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index c5ef9b2fa3..4c55acea5a 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown { const log = [] as any[]; ev.on('requestServerStatsLog', x => { - ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50)); + ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length)); }); const tick = async () => { diff --git a/packages/backend/src/misc/promise-tracker.ts b/packages/backend/src/misc/promise-tracker.ts new file mode 100644 index 0000000000..c7166c6de9 --- /dev/null +++ b/packages/backend/src/misc/promise-tracker.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set(); + +/** + * This tracks promises that other modules decided not to wait for, + * and makes sure they are all settled before fully closing down the server. + */ +export function trackPromise(promise: Promise<unknown>) { + if (process.env.NODE_ENV !== 'test') { + return; + } + const ref = new WeakRef(promise); + promiseRefs.add(ref); + promise.finally(() => promiseRefs.delete(ref)); +} + +export async function allSettled(): Promise<void> { + await Promise.allSettled([...promiseRefs].map(r => r.deref())); +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index f5a75ed28a..3265e85dd7 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -191,6 +191,29 @@ export class MiMeta { }) public hcaptchaSecretKey: string | null; + @Column('boolean', { + default: false, + }) + public enableMcaptcha: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public mcaptchaSitekey: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public mcaptchaSecretKey: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public mcaptchaInstanceUrl: string | null; + @Column('boolean', { default: false, }) @@ -467,7 +490,7 @@ export class MiMeta { nullable: true, }) public truemailInstance: string | null; - + @Column('varchar', { length: 1024, nullable: true, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index e6327002c5..9c52c7d76a 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -24,6 +24,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; @@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor DeleteDriveFilesProcessorService, ExportCustomEmojisProcessorService, ExportNotesProcessorService, + ExportClipsProcessorService, ExportFavoritesProcessorService, ExportFollowingProcessorService, ExportMutingProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index b872dd65f7..bcc1a69f80 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -16,6 +16,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; +import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; @@ -91,6 +92,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, private exportNotesProcessorService: ExportNotesProcessorService, + private exportClipsProcessorService: ExportClipsProcessorService, private exportFavoritesProcessorService: ExportFavoritesProcessorService, private exportFollowingProcessorService: ExportFollowingProcessorService, private exportMutingProcessorService: ExportMutingProcessorService, @@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportClips': return this.exportClipsProcessorService.process(job); case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); case 'exportFollowing': return this.exportFollowingProcessorService.process(job); case 'exportMuting': return this.exportMutingProcessorService.process(job); diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts new file mode 100644 index 0000000000..5221497bd3 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as fs from 'node:fs'; +import { Writable } from 'node:stream'; +import { Inject, Injectable, StreamableFile } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { format as dateFormat } from 'date-fns'; +import { DI } from '@/di-symbols.js'; +import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { createTemp } from '@/misc/create-temp.js'; +import type { MiPoll } from '@/models/Poll.js'; +import type { MiNote } from '@/models/Note.js'; +import { bindThis } from '@/decorators.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { Packed } from '@/misc/json-schema.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbJobDataWithUser } from '../types.js'; + +@Injectable() +export class ExportClipsProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + private idService: IdService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('export-clips'); + } + + @bindThis + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { + this.logger.info(`Exporting clips of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + return; + } + + // Create temp file + const [path, cleanup] = await createTemp(); + + this.logger.info(`Temp file is ${path}`); + + try { + const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' })); + const writer = stream.getWriter(); + writer.closed.catch(this.logger.error); + + await writer.write('['); + + await this.processClips(writer, user, job); + + await writer.write(']'); + await writer.close(); + + this.logger.succ(`Exported to: ${path}`); + + const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); + + this.logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); + } + } + + async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) { + let exportedClipsCount = 0; + let cursor: MiClip['id'] | null = null; + + while (true) { + const clips = await this.clipsRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); + + if (clips.length === 0) { + job.updateProgress(100); + break; + } + + cursor = clips.at(-1)?.id ?? null; + + for (const clip of clips) { + // Stringify but remove the last `]}` + const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2); + const isFirst = exportedClipsCount === 0; + await writer.write(isFirst ? content : ',\n' + content); + + await this.processClipNotes(writer, clip.id); + + await writer.write(']}'); + exportedClipsCount++; + } + + const total = await this.clipsRepository.countBy({ + userId: user.id, + }); + + job.updateProgress(exportedClipsCount / total); + } + } + + async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> { + let exportedClipNotesCount = 0; + let cursor: MiClipNote['id'] | null = null; + + while (true) { + const clipNotes = await this.clipNotesRepository.find({ + where: { + clipId, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + relations: ['note', 'note.user'], + }) as (MiClipNote & { note: MiNote & { user: MiUser } })[]; + + if (clipNotes.length === 0) { + break; + } + + cursor = clipNotes.at(-1)?.id ?? null; + + for (const clipNote of clipNotes) { + let poll: MiPoll | undefined; + if (clipNote.note.hasPoll) { + poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id }); + } + const content = JSON.stringify(this.serializeClipNote(clipNote, poll)); + const isFirst = exportedClipNotesCount === 0; + await writer.write(isFirst ? content : ',\n' + content); + + exportedClipNotesCount++; + } + } + } + + private serializeClip(clip: MiClip): Record<string, unknown> { + return { + id: clip.id, + name: clip.name, + description: clip.description, + lastClippedAt: clip.lastClippedAt?.toISOString(), + clipNotes: [], + }; + } + + private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> { + return { + id: clip.id, + createdAt: this.idService.parse(clip.id).date.toISOString(), + note: { + id: clip.note.id, + text: clip.note.text, + createdAt: this.idService.parse(clip.note.id).date.toISOString(), + fileIds: clip.note.fileIds, + replyId: clip.note.replyId, + renoteId: clip.note.renoteId, + poll: poll, + cw: clip.note.cw, + visibility: clip.note.visibility, + visibleUserIds: clip.note.visibleUserIds, + localOnly: clip.note.localOnly, + reactionAcceptance: clip.note.reactionAcceptance, + uri: clip.note.uri, + url: clip.note.url, + user: { + id: clip.note.user.id, + name: clip.note.user.name, + username: clip.note.user.username, + host: clip.note.user.host, + uri: clip.note.user.uri, + }, + }, + }; + } +} diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 86a64d7121..a3a9805444 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -208,6 +208,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportClips from './endpoints/i/export-clips.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; @@ -569,6 +570,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; +const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default }; const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default }; const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default }; @@ -934,6 +936,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1293,6 +1296,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 753984ef52..6b4d9d9f70 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -65,6 +65,7 @@ export class SignupApiService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; + 'm-captcha-response'?: string; } }>, reply: FastifyReply, @@ -82,6 +83,12 @@ export class SignupApiService { }); } + if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) { + await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 41232091c6..bd8aa4af72 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Schema } from '@/misc/json-schema.js'; import { permissions } from 'misskey-js'; +import type { Schema } from '@/misc/json-schema.js'; import { RolePolicies } from '@/core/RoleService.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; @@ -209,6 +209,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; +import * as ep___i_exportClips from './endpoints/i/export-clips.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; @@ -568,6 +569,7 @@ const eps = [ ['i/export-following', ep___i_exportFollowing], ['i/export-mute', ep___i_exportMute], ['i/export-notes', ep___i_exportNotes], + ['i/export-clips', ep___i_exportClips], ['i/export-favorites', ep___i_exportFavorites], ['i/export-user-lists', ep___i_exportUserLists], ['i/export-antennas', ep___i_exportAntennas], diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 281f6c484c..0627c5055c 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -41,6 +41,18 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableMcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + mcaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + mcaptchaInstanceUrl: { + type: 'string', + optional: false, nullable: true, + }, enableRecaptcha: { type: 'boolean', optional: false, nullable: false, @@ -163,6 +175,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + mcaptchaSecretKey: { + type: 'string', + optional: false, nullable: true, + }, recaptchaSecretKey: { type: 'string', optional: false, nullable: true, @@ -468,6 +484,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableMcaptcha: instance.enableMcaptcha, + mcaptchaSiteKey: instance.mcaptchaSitekey, + mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, @@ -498,6 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- sensitiveWords: instance.sensitiveWords, preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, + mcaptchaSecretKey: instance.mcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, sensitiveMediaDetection: instance.sensitiveMediaDetection, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 3a6426435d..d76d3dfeea 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -63,6 +63,10 @@ export const paramDef = { enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true }, + enableMcaptcha: { type: 'boolean' }, + mcaptchaSiteKey: { type: 'string', nullable: true }, + mcaptchaInstanceUrl: { type: 'string', nullable: true }, + mcaptchaSecretKey: { type: 'string', nullable: true }, enableRecaptcha: { type: 'boolean' }, recaptchaSiteKey: { type: 'string', nullable: true }, recaptchaSecretKey: { type: 'string', nullable: true }, @@ -269,6 +273,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.hcaptchaSecretKey = ps.hcaptchaSecretKey; } + if (ps.enableMcaptcha !== undefined) { + set.enableMcaptcha = ps.enableMcaptcha; + } + + if (ps.mcaptchaSiteKey !== undefined) { + set.mcaptchaSitekey = ps.mcaptchaSiteKey; + } + + if (ps.mcaptchaInstanceUrl !== undefined) { + set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl; + } + + if (ps.mcaptchaSecretKey !== undefined) { + set.mcaptchaSecretKey = ps.mcaptchaSecretKey; + } + if (ps.enableRecaptcha !== undefined) { set.enableRecaptcha = ps.enableRecaptcha; } @@ -472,7 +492,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.verifymailAuthKey = ps.verifymailAuthKey; } } - + if (ps.enableTruemailApi !== undefined) { set.enableTruemailApi = ps.enableTruemailApi; } diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 0bf2688b4a..7293c2e39b 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- antenna.isActive = true; antenna.lastUsedAt = new Date(); - this.antennasRepository.update(antenna.id, antenna); + trackPromise(this.antennasRepository.update(antenna.id, antenna)); if (needPublishEvent) { this.globalEventService.publishInternalEvent('antennaUpdated', antenna); diff --git a/packages/backend/src/server/api/endpoints/i/export-clips.ts b/packages/backend/src/server/api/endpoints/i/export-clips.ts new file mode 100644 index 0000000000..9435a2b23c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/export-clips.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + secure: true, + requireCredential: true, + limit: { + duration: ms('1day'), + max: 1, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportClipsJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index f7c2962bc2..529e82678d 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -108,6 +108,18 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableMcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + mcaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + mcaptchaInstanceUrl: { + type: 'string', + optional: false, nullable: true, + }, enableRecaptcha: { type: 'boolean', optional: false, nullable: false, @@ -351,6 +363,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableMcaptcha: instance.enableMcaptcha, + mcaptchaSiteKey: instance.mcaptchaSitekey, + mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 909b5a5e03..e0245814c4 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -21,6 +21,7 @@ class UserListChannel extends Channel { private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {}; private listUsersClock: NodeJS.Timeout; private withFiles: boolean; + private withRenotes: boolean; constructor( private userListsRepository: UserListsRepository, @@ -39,6 +40,7 @@ class UserListChannel extends Channel { public async init(params: any) { this.listId = params.listId as string; this.withFiles = params.withFiles ?? false; + this.withRenotes = params.withRenotes ?? true; // Check existence and owner const listExist = await this.userListsRepository.exist({ @@ -104,6 +106,8 @@ class UserListChannel extends Channel { } } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts new file mode 100644 index 0000000000..9686f2b7fd --- /dev/null +++ b/packages/backend/test/e2e/exports.ts @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, startServer, startJobQueue, port, post } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; + +describe('export-clips', () => { + let app: INestApplicationContext; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + + // XXX: Any better way to get the result? + async function pollFirstDriveFile() { + while (true) { + const files = (await api('/drive/files', {}, alice)).body; + if (!files.length) { + await new Promise(r => setTimeout(r, 100)); + continue; + } + if (files.length > 1) { + throw new Error('Too many files?'); + } + const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body; + const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`)); + return await res.json(); + } + } + + beforeAll(async () => { + app = await startServer(); + await startJobQueue(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + // Clean all clips and files of alice + const clips = (await api('/clips/list', {}, alice)).body; + for (const clip of clips) { + const res = await api('/clips/delete', { clipId: clip.id }, alice); + if (res.status !== 204) { + throw new Error('Failed to delete clip'); + } + } + const files = (await api('/drive/files', {}, alice)).body; + for (const file of files) { + const res = await api('/drive/files/delete', { fileId: file.id }, alice); + if (res.status !== 204) { + throw new Error('Failed to delete file'); + } + } + }); + + test('basic export', async () => { + let res = await api('/clips/create', { + name: 'foo', + description: 'bar', + }, alice); + assert.strictEqual(res.status, 200); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'foo'); + assert.strictEqual(exported[0].description, 'bar'); + assert.strictEqual(exported[0].clipNotes.length, 0); + }); + + test('export with notes', async () => { + let res = await api('/clips/create', { + name: 'foo', + description: 'bar', + }, alice); + assert.strictEqual(res.status, 200); + const clip = res.body; + + const note1 = await post(alice, { + text: 'baz1', + }); + + const note2 = await post(alice, { + text: 'baz2', + poll: { + choices: ['sakura', 'izumi', 'ako'], + }, + }); + + for (const note of [note1, note2]) { + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + } + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'foo'); + assert.strictEqual(exported[0].description, 'bar'); + assert.strictEqual(exported[0].clipNotes.length, 2); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); + assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2'); + assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura'); + }); + + test('multiple clips', async () => { + let res = await api('/clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip1 = res.body; + + res = await api('/clips/create', { + name: 'yuri', + description: 'yuri', + }, alice); + assert.strictEqual(res.status, 200); + const clip2 = res.body; + + const note1 = await post(alice, { + text: 'baz1', + }); + + const note2 = await post(alice, { + text: 'baz2', + }); + + res = await api('/clips/add-note', { + clipId: clip1.id, + noteId: note1.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/clips/add-note', { + clipId: clip2.id, + noteId: note2.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'kawaii'); + assert.strictEqual(exported[0].clipNotes.length, 1); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); + assert.strictEqual(exported[1].name, 'yuri'); + assert.strictEqual(exported[1].clipNotes.length, 1); + assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2'); + }); + + test('Clipping other user\'s note', async () => { + let res = await api('/clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip = res.body; + + const note = await post(bob, { + text: 'baz', + visibility: 'followers', + }); + + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].name, 'kawaii'); + assert.strictEqual(exported[0].clipNotes.length, 1); + assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz'); + assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob'); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 46b8ea9cdd..7c9428d476 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; -export { server as startServer } from '@/boot/common.js'; +export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; interface UserToken { token: string; diff --git a/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 new file mode 100644 index 0000000000..cafc34ad9c Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/bubble2.mp3 b/packages/frontend/assets/drop-and-fusion/bubble2.mp3 new file mode 100644 index 0000000000..8b4f8df6e9 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/bubble2.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/cold_face.png b/packages/frontend/assets/drop-and-fusion/cold_face.png new file mode 100644 index 0000000000..f5f53e9efc Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/cold_face.png differ diff --git a/packages/frontend/assets/drop-and-fusion/drop-arrow.svg b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg new file mode 100644 index 0000000000..f98bb8a1ac --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> + <path d="M0,0L128,0L64,64L0,0Z" style="fill:rgb(255,61,0);"/> + <path d="M0,0L128,0L64,64L0,0ZM28.971,12L64,47.029C64,47.029 99.029,12 99.029,12L28.971,12Z" style="fill:rgb(255,122,0);"/> +</svg> diff --git a/packages/frontend/assets/drop-and-fusion/dropper.png b/packages/frontend/assets/drop-and-fusion/dropper.png new file mode 100644 index 0000000000..f4300aa5c0 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/dropper.png differ diff --git a/packages/frontend/assets/drop-and-fusion/exploding_head.png b/packages/frontend/assets/drop-and-fusion/exploding_head.png new file mode 100644 index 0000000000..e8ec5182c8 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/exploding_head.png differ diff --git a/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png b/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png new file mode 100644 index 0000000000..c523020f62 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png differ diff --git a/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png b/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png new file mode 100644 index 0000000000..db9e839c84 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png differ diff --git a/packages/frontend/assets/drop-and-fusion/frame-dark.svg b/packages/frontend/assets/drop-and-fusion/frame-dark.svg new file mode 100644 index 0000000000..3fa7c0da81 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-dark.svg @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg width="100%" height="100%" viewBox="0 0 450 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"> + <g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600"/> + </g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600" style="fill:rgb(255,147,2);fill-opacity:0.15;"/> + </g> + <use xlink:href="#_Image1" x="0" y="49.048" width="450px" height="551px"/> + </g> + <g transform="matrix(0.755719,0.654896,-0.654896,0.755719,383.517,-217.265)"> + <g transform="matrix(0.755719,-0.654896,0.654896,0.755719,-147.545,415.355)"> + <use xlink:href="#_Image2" x="0" y="49" width="450px" height="551px"/> + </g> + </g> + <use xlink:href="#_Image3" x="25" y="99.5" width="400px" height="475px"/> + <g transform="matrix(1,0,0,2,1.13687e-13,25)"> + <rect x="25" y="37.5" width="400" height="12.5" style="fill:url(#_Linear4);"/> + </g> + <defs> + <image id="_Image1" width="450px" height="551px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAAInCAYAAAALeVnpAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAWlElEQVR4nO3df6yd9X3Y8c/znHN/+Ae2uRgMhFB+pCy1PasqatQVJZFGEkpImmmjycRSb5OiKZPWStMmrVs3Tauqav9N6zqWbFVWodTaukGaLm2TaM3UdYqSLZMaAiYjIRQUfti+GGN8r88953me7/54zv0BCVQh59rYn9fLMgbjc557zx+8+X6f7/P9RvyAjh09svMHfQ0AXAhvpFHV6/3LBz56+MDp1eaX/uz06K88fWZ0/alzk7lR01WLw7pcvXtucuO+xWdvWlr83aWdw3959NOPnHjjXzoA/GBm1ajXDOGv/9W3/+rvf/P0P15emdRdF1GiRCkl6hLRVRFVVUUVVdR1xP5dc909b1/6tV986Jv/bHu+XQDYNMtGfU8IH/jokR/50++e/aM/fvKlW9uuxK1LO+Kdt+6LQ9fuip07BjGYq6ObdLFyvo1Hn1+JP3niTDxx+nwM6ireffPeJ378hj13Hv30w09t/8cAQDbb0ahXhPATHz74sc8eX/7EibOTQR0Rf+P2a+Inf3Qp1iZtjCclulKiROkrW1UxP1fFwtwg/s+3Tsdv/9+T0UXEgT1z7YcO7v/4x3/n+G9eyA8HgMvbdjVqI4QPfPTwW/7zn5586tmXJoPbrl6Mj/2lt0Q9X8fqqI21to2mKdGWiNKVqOoqBlXEcFjFwmAQOxcHUda6+A9feSYePzWK6/cuNB/58f03Hf30I89cjA8LgMvLdjZqsH6RW/fv/trDz527+pardsTff+/NMeraePl8EyvjNkZrbYwmJcZNF5O2xGT6a9N00ZSIpi0xnK/j3bftj//3/Ll4+syo3jU3/MCXn3zxNy7exwbA5WI7GzWIiPiNew/+8888cuqvVVHFL9351ljrSpxbbeP8uI3za22M2y4mTRdNF9F0XbRdRNt20ZYSbVuiRETpIqKKuP0tu+JLj5+JP3txdNUvv/eW7g+On/qfF/PDA+DStt2NGhw7emTff/36yd8/u9bWP3/7gbhu/844e77pLzBu+7I2/dxr15UoJabzsBH9Sp2IrvQXKBGxc8dc7N8xjK8/ey6eO7v2rl+5+9Zff/DrJ0YX80ME4NJ0IRpVL58b/8NTK5PBjVcuxDvethSrozZGky5G0ws07eYFui62/Ox/v5kOQ0fj/nXnx2381NuW4sYrF+LUymSwfG78Dy7uxwjApepCNKp++sz459ou4o6b98a47WKtbWMyaaPptl4gopSqf05j/Uep+otNL9R0XUwmbYwm/TD1jpv2RttFPH1m/OGL/UECcGm6EI2qnzk7emuJEgev2R1rky4mkxJNF9N51f4CEf3Dilut//P6g4xt279uMimxNuni4IHdUaLEM2dHN1zoDw6Ay8OFaFR9eqVZKF3E3h2DaEsXTdf1hY2IUr7/BV59oVIiuujL23Yl2tLF3h2DKF3E6ZVmcTs+HAAufxeiUfULq01dIqKaH0a7fqOxK/0Km9e5wPdcaMucbNuVqObrKBHxwmpT//AfBQAZXYhG1aWU6R+drrjZ+sLy+hfYuND0z5VYX6nTP9sf073fAOCNuBCNqqPqh42l9DcXS/fDhat00/cpfblf/3wLAHgdF6BRG9OWJdZX3FRRyhurV79qZ/N9AGAWtrNR9Z8zvfrDMzMKwBt1ARplIQsAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCp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"/> + <image id="_Image2" width="450px" height="551px" xlink:href="data:image/png;base64,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"/> + <image id="_Image3" width="400px" height="475px" xlink:href="data:image/png;base64,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"/> + <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7.65404e-16,12.5,-0.390625,2.39189e-17,225,37.5)"><stop offset="0" style="stop-color:rgb(255,14,0);stop-opacity:0.5"/><stop offset="1" style="stop-color:rgb(255,13,0);stop-opacity:0"/></linearGradient> + </defs> +</svg> diff --git a/packages/frontend/assets/drop-and-fusion/frame-light.svg b/packages/frontend/assets/drop-and-fusion/frame-light.svg new file mode 100644 index 0000000000..6052ccbaa0 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-light.svg @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg width="100%" height="100%" viewBox="0 0 450 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"> + <g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600" style="fill:white;"/> + </g> + <g transform="matrix(0.944444,0,0,0.8125,12.5,100)"> + <rect x="0" y="0" width="450" height="600" style="fill:rgb(255,147,2);fill-opacity:0.15;"/> + </g> + <use xlink:href="#_Image1" x="0" y="49.048" width="450px" height="551px"/> + </g> + <g transform="matrix(0.755719,0.654896,-0.654896,0.755719,383.517,-217.265)"> + <g transform="matrix(0.755719,-0.654896,0.654896,0.755719,-147.545,415.355)"> + <use xlink:href="#_Image2" x="0" y="49" width="450px" height="551px"/> + </g> + </g> + <use xlink:href="#_Image3" x="25" y="99.5" width="400px" height="475px"/> + <g transform="matrix(1,0,0,2,1.13687e-13,25)"> + <rect x="25" y="37.5" width="400" height="12.5" style="fill:url(#_Linear4);"/> + </g> + <defs> + <image id="_Image1" width="450px" height="551px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAAInCAYAAAALeVnpAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAWlElEQVR4nO3df6yd9X3Y8c/znHN/+Ae2uRgMhFB+pCy1PasqatQVJZFGEkpImmmjycRSb5OiKZPWStMmrVs3Tauqav9N6zqWbFVWodTaukGaLm2TaM3UdYqSLZMaAiYjIRQUfti+GGN8r88953me7/54zv0BCVQh59rYn9fLMgbjc557zx+8+X6f7/P9RvyAjh09svMHfQ0AXAhvpFHV6/3LBz56+MDp1eaX/uz06K88fWZ0/alzk7lR01WLw7pcvXtucuO+xWdvWlr83aWdw3959NOPnHjjXzoA/GBm1ajXDOGv/9W3/+rvf/P0P15emdRdF1GiRCkl6hLRVRFVVUUVVdR1xP5dc909b1/6tV986Jv/bHu+XQDYNMtGfU8IH/jokR/50++e/aM/fvKlW9uuxK1LO+Kdt+6LQ9fuip07BjGYq6ObdLFyvo1Hn1+JP3niTDxx+nwM6ireffPeJ378hj13Hv30w09t/8cAQDbb0ahXhPATHz74sc8eX/7EibOTQR0Rf+P2a+Inf3Qp1iZtjCclulKiROkrW1UxP1fFwtwg/s+3Tsdv/9+T0UXEgT1z7YcO7v/4x3/n+G9eyA8HgMvbdjVqI4QPfPTwW/7zn5586tmXJoPbrl6Mj/2lt0Q9X8fqqI21to2mKdGWiNKVqOoqBlXEcFjFwmAQOxcHUda6+A9feSYePzWK6/cuNB/58f03Hf30I89cjA8LgMvLdjZqsH6RW/fv/trDz527+pardsTff+/NMeraePl8EyvjNkZrbYwmJcZNF5O2xGT6a9N00ZSIpi0xnK/j3bftj//3/Ll4+syo3jU3/MCXn3zxNy7exwbA5WI7GzWIiPiNew/+8888cuqvVVHFL9351ljrSpxbbeP8uI3za22M2y4mTRdNF9F0XbRdRNt20ZYSbVuiRETpIqKKuP0tu+JLj5+JP3txdNUvv/eW7g+On/qfF/PDA+DStt2NGhw7emTff/36yd8/u9bWP3/7gbhu/844e77pLzBu+7I2/dxr15UoJabzsBH9Sp2IrvQXKBGxc8dc7N8xjK8/ey6eO7v2rl+5+9Zff/DrJ0YX80ME4NJ0IRpVL58b/8NTK5PBjVcuxDvethSrozZGky5G0ws07eYFui62/Ox/v5kOQ0fj/nXnx2381NuW4sYrF+LUymSwfG78Dy7uxwjApepCNKp++sz459ou4o6b98a47WKtbWMyaaPptl4gopSqf05j/Uep+otNL9R0XUwmbYwm/TD1jpv2RttFPH1m/OGL/UECcGm6EI2qnzk7emuJEgev2R1rky4mkxJNF9N51f4CEf3Dilut//P6g4xt279uMimxNuni4IHdUaLEM2dHN1zoDw6Ay8OFaFR9eqVZKF3E3h2DaEsXTdf1hY2IUr7/BV59oVIiuujL23Yl2tLF3h2DKF3E6ZVmcTs+HAAufxeiUfULq01dIqKaH0a7fqOxK/0Km9e5wPdcaMucbNuVqObrKBHxwmpT//AfBQAZXYhG1aWU6R+drrjZ+sLy+hfYuND0z5VYX6nTP9sf073fAOCNuBCNqqPqh42l9DcXS/fDhat00/cpfblf/3wLAHgdF6BRG9OWJdZX3FRRyhurV79qZ/N9AGAWtrNR9Z8zvfrDMzMKwBt1ARplIQsAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCp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"/> + <image id="_Image2" width="450px" height="551px" xlink:href="data:image/png;base64,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"/> + <image id="_Image3" width="400px" height="475px" xlink:href="data:image/png;base64,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"/> + <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7.65404e-16,12.5,-0.390625,2.39189e-17,225,37.5)"><stop offset="0" style="stop-color:rgb(255,14,0);stop-opacity:0.5"/><stop offset="1" style="stop-color:rgb(255,13,0);stop-opacity:0"/></linearGradient> + </defs> +</svg> diff --git a/packages/frontend/assets/drop-and-fusion/gameover.png b/packages/frontend/assets/drop-and-fusion/gameover.png new file mode 100644 index 0000000000..8b622577ca Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/gameover.png differ diff --git a/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png b/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png new file mode 100644 index 0000000000..fd72d749a1 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png differ diff --git a/packages/frontend/assets/drop-and-fusion/heart_suit.png b/packages/frontend/assets/drop-and-fusion/heart_suit.png new file mode 100644 index 0000000000..b0105f8582 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/heart_suit.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_1.png b/packages/frontend/assets/drop-and-fusion/keycap_1.png new file mode 100644 index 0000000000..d672f2854a Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_1.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_10.png b/packages/frontend/assets/drop-and-fusion/keycap_10.png new file mode 100644 index 0000000000..32cf193540 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_10.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_2.png b/packages/frontend/assets/drop-and-fusion/keycap_2.png new file mode 100644 index 0000000000..81c3f58e6e Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_2.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_3.png b/packages/frontend/assets/drop-and-fusion/keycap_3.png new file mode 100644 index 0000000000..424d8c123d Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_3.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_4.png b/packages/frontend/assets/drop-and-fusion/keycap_4.png new file mode 100644 index 0000000000..ea6ae50531 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_4.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_5.png b/packages/frontend/assets/drop-and-fusion/keycap_5.png new file mode 100644 index 0000000000..ad435da69a Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_5.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_6.png b/packages/frontend/assets/drop-and-fusion/keycap_6.png new file mode 100644 index 0000000000..70c9522b43 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_6.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_7.png b/packages/frontend/assets/drop-and-fusion/keycap_7.png new file mode 100644 index 0000000000..5a24307487 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_7.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_8.png b/packages/frontend/assets/drop-and-fusion/keycap_8.png new file mode 100644 index 0000000000..9689d8ecfb Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_8.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_9.png b/packages/frontend/assets/drop-and-fusion/keycap_9.png new file mode 100644 index 0000000000..ac3f638841 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_9.png differ diff --git a/packages/frontend/assets/drop-and-fusion/logo.png b/packages/frontend/assets/drop-and-fusion/logo.png new file mode 100644 index 0000000000..c6725bea88 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/logo.png differ diff --git a/packages/frontend/assets/drop-and-fusion/pleading_face.png b/packages/frontend/assets/drop-and-fusion/pleading_face.png new file mode 100644 index 0000000000..42f58d411c Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/pleading_face.png differ diff --git a/packages/frontend/assets/drop-and-fusion/poi1.mp3 b/packages/frontend/assets/drop-and-fusion/poi1.mp3 new file mode 100644 index 0000000000..59dae90965 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/poi1.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/poi2.mp3 b/packages/frontend/assets/drop-and-fusion/poi2.mp3 new file mode 100644 index 0000000000..a65c653891 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/poi2.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png b/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png new file mode 100644 index 0000000000..416ef0410a Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png differ diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png b/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png new file mode 100644 index 0000000000..c0f72254c2 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png differ diff --git a/packages/frontend/assets/drop-and-fusion/zany_face.png b/packages/frontend/assets/drop-and-fusion/zany_face.png new file mode 100644 index 0000000000..f14f9db20b Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/zany_face.png differ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 864779fd9d..9ef18a56a7 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "watch": "vite", - "dev": "vite --config vite.config.local-dev.ts", + "dev": "vite --config vite.config.local-dev.ts --debug hmr", "build": "vite build", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", @@ -19,6 +19,7 @@ "dependencies": { "@discordapp/twemoji": "15.0.2", "@github/webauthn-json": "2.1.1", + "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@misskey-dev/browser-image-resizer": "2.2.1-misskey.10", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index ef69eff764..c67911c9c3 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; +import { setupRouter } from '@/global/router/definition.js'; export async function common(createVue: () => App<Element>) { console.info(`Misskey v${version}`); @@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) { const app = createVue(); + setupRouter(app); + if (_DEV_) { app.config.performance = true; } diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 0159d0c032..5011ce9e74 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -3,23 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { createApp, markRaw, defineAsyncComponent } from 'vue'; +import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { common } from './common.js'; import { ui } from '@/config.js'; import { i18n } from '@/i18n.js'; -import { confirm, alert, post, popup, toast } from '@/os.js'; +import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; -import { $i, updateAccount, signout } from '@/account.js'; -import { defaultStore, ColdDeviceStorage } from '@/store.js'; +import { $i, signout, updateAccount } from '@/account.js'; +import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { makeHotkey } from '@/scripts/hotkey.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; -import { mainRouter } from '@/router.js'; import { initializeSw } from '@/scripts/initialize-sw.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; +import { mainRouter } from '@/global/router/main.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 328215c791..7aa08cf51f 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -6,12 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <span v-if="!available">Loading<MkEllipsis/></span> - <div ref="captchaEl"></div> + <div v-if="props.provider == 'mcaptcha'"> + <div id="mcaptcha__widget-container" class="m-captcha-style"></div> + <div ref="captchaEl"></div> + </div> + <div v-else ref="captchaEl"></div> </div> </template> <script lang="ts" setup> -import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from 'vue'; +import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; import { defaultStore } from '@/store.js'; // APIs provided by Captcha services @@ -25,7 +29,7 @@ export type Captcha = { getResponse(id: string): string; }; -export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; @@ -38,6 +42,7 @@ declare global { const props = defineProps<{ provider: CaptchaProvider; sitekey: string | null; // null will show error on request + instanceUrl?: string | null; modelValue?: string | null; }>(); @@ -54,6 +59,7 @@ const variable = computed(() => { case 'hcaptcha': return 'hcaptcha'; case 'recaptcha': return 'grecaptcha'; case 'turnstile': return 'turnstile'; + case 'mcaptcha': return 'mcaptcha'; } }); @@ -64,6 +70,7 @@ const src = computed(() => { case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off'; case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit'; case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + case 'mcaptcha': return null; } }); @@ -71,9 +78,9 @@ const scriptId = computed(() => `script-${props.provider}`); const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); -if (loaded) { +if (loaded || props.provider === 'mcaptcha') { available.value = true; -} else { +} else if (src.value !== null) { (document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), { async: true, id: scriptId.value, @@ -86,7 +93,7 @@ function reset() { if (captcha.value.reset) captcha.value.reset(); } -function requestRender() { +async function requestRender() { if (captcha.value.render && captchaEl.value instanceof Element) { captcha.value.render(captchaEl.value, { sitekey: props.sitekey, @@ -95,6 +102,15 @@ function requestRender() { 'expired-callback': callback, 'error-callback': callback, }); + } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { + const { default: Widget } = await import('@mcaptcha/vanilla-glue'); + // @ts-expect-error avoid typecheck error + new Widget({ + siteKey: { + instanceUrl: new URL(props.instanceUrl), + key: props.sitekey, + }, + }); } else { window.setTimeout(requestRender, 1); } @@ -104,14 +120,27 @@ function callback(response?: string) { emit('update:modelValue', typeof response === 'string' ? response : null); } +function onReceivedMessage(message: MessageEvent) { + if (message.data.token) { + if (props.instanceUrl && new URL(message.origin).host === new URL(props.instanceUrl).host) { + callback(<string>message.data.token); + } + } +} + onMounted(() => { if (available.value) { + window.addEventListener('message', onReceivedMessage); requestRender(); } else { watch(available, requestRender); } }); +onUnmounted(() => { + window.removeEventListener('message', onReceivedMessage); +}); + onBeforeUnmount(() => { reset(); }); diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index b46b25eba2..8a74319f29 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -45,9 +45,9 @@ import bytes from '@/filters/bytes.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { useRouter } from '@/router.js'; import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; import { deviceKind } from '@/scripts/device-kind.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 2647ace7db..28058c338b 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -23,26 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div ref="contents" :class="$style.root" style="container-type: inline-size;"> - <RouterView :key="reloadCount" :router="router"/> + <RouterView :key="reloadCount" :router="windowRouter"/> </div> </MkWindow> </template> <script lang="ts" setup> -import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue'; +import { computed, ComputedRef, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'; import RouterView from '@/components/global/RouterView.vue'; import MkWindow from '@/components/MkWindow.vue'; import { popout as _popout } from '@/scripts/popout.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; -import { mainRouter, routes, page } from '@/router.js'; -import { $i } from '@/account.js'; -import { Router, useScrollPositionManager } from '@/nirax.js'; +import { useScrollPositionManager } from '@/nirax.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { openingWindowsCount } from '@/os.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getScrollContainer } from '@/scripts/scroll.js'; +import { useRouterFactory } from '@/global/router/supplier.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ initialPath: string; @@ -52,14 +52,15 @@ defineEmits<{ (ev: 'closed'): void; }>(); -const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue'))); +const routerFactory = useRouterFactory(); +const windowRouter = routerFactory(props.initialPath); const contents = shallowRef<HTMLElement>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); const history = ref<{ path: string; key: any; }[]>([{ - path: router.getCurrentPath(), - key: router.getCurrentKey(), + path: windowRouter.getCurrentPath(), + key: windowRouter.getCurrentKey(), }]); const buttonsLeft = computed(() => { const buttons = []; @@ -88,11 +89,11 @@ const buttonsRight = computed(() => { }); const reloadCount = ref(0); -router.addListener('push', ctx => { +windowRouter.addListener('push', ctx => { history.value.push({ path: ctx.path, key: ctx.key }); }); -provide('router', router); +provide('router', windowRouter); provideMetadataReceiver((info) => { pageMetadata.value = info; }); @@ -112,20 +113,20 @@ const contextmenu = computed(() => ([{ icon: 'ti ti-external-link', text: i18n.ts.openInNewTab, action: () => { - window.open(url + router.getCurrentPath(), '_blank', 'noopener'); + window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener'); windowEl.value.close(); }, }, { icon: 'ti ti-link', text: i18n.ts.copyLink, action: () => { - copyToClipboard(url + router.getCurrentPath()); + copyToClipboard(url + windowRouter.getCurrentPath()); }, }])); function back() { history.value.pop(); - router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key); + windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key); } function reload() { @@ -137,16 +138,16 @@ function close() { } function expand() { - mainRouter.push(router.getCurrentPath(), 'forcePage'); + mainRouter.push(windowRouter.getCurrentPath(), 'forcePage'); windowEl.value.close(); } function popout() { - _popout(router.getCurrentPath(), windowEl.value.$el); + _popout(windowRouter.getCurrentPath(), windowEl.value.$el); windowEl.value.close(); } -useScrollPositionManager(() => getScrollContainer(contents.value), router); +useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter); onMounted(() => { openingWindowsCount.value++; diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue index a741a3f7a8..e5e5a9edf4 100644 --- a/packages/frontend/src/components/MkPlusOneEffect.vue +++ b/packages/frontend/src/components/MkPlusOneEffect.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> - <span class="text" :class="{ up }">+1</span> + <span class="text" :class="{ up }">+{{ value }}</span> </div> </template> @@ -16,7 +16,9 @@ import * as os from '@/os.js'; const props = withDefaults(defineProps<{ x: number; y: number; + value?: number; }>(), { + value: 1, }); const emit = defineEmits<{ @@ -40,6 +42,7 @@ onMounted(() => { <style lang="scss" module> .root { + user-select: none; pointer-events: none; position: fixed; width: 128px; diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index c71330d62c..79e17c9aef 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -63,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </MkInput> <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;"> @@ -117,6 +118,7 @@ const passwordStrength = ref<'' | 'low' | 'medium' | 'high'>(''); const passwordRetypeState = ref<null | 'match' | 'not-match'>(null); const submitting = ref<boolean>(false); const hCaptchaResponse = ref<string | null>(null); +const mCaptchaResponse = ref<string | null>(null); const reCaptchaResponse = ref<string | null>(null); const turnstileResponse = ref<string | null>(null); const usernameAbortController = ref<null | AbortController>(null); @@ -125,6 +127,7 @@ const emailAbortController = ref<null | AbortController>(null); const shouldDisableSubmitting = computed((): boolean => { return submitting.value || instance.enableHcaptcha && !hCaptchaResponse.value || + instance.enableMcaptcha && !mCaptchaResponse.value || instance.enableRecaptcha && !reCaptchaResponse.value || instance.enableTurnstile && !turnstileResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || @@ -252,6 +255,7 @@ async function onSubmit(): Promise<void> { emailAddress: email.value, invitationCode: invitationCode.value, 'hcaptcha-response': hCaptchaResponse.value, + 'm-captcha-response': mCaptchaResponse.value, 'g-recaptcha-response': reCaptchaResponse.value, 'turnstile-response': turnstileResponse.value, }); diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index d5adc02ca7..63f779dbde 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -132,6 +132,7 @@ function connectChannel() { connection.on('mention', onNote); } else if (props.src === 'list') { connection = stream.useChannel('userList', { + withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }); @@ -198,6 +199,7 @@ function updatePaginationQuery() { } else if (props.src === 'list') { endpoint = 'notes/user-list-timeline'; query = { + withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list, }; @@ -236,8 +238,9 @@ function refreshEndpointAndChannel() { updatePaginationQuery(); } +// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる // IDが切り替わったら切り替え先のTLを表示させたい -watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel); +watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); // 初回表示用 refreshEndpointAndChannel(); diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index d34f47a68a..fbea279dbe 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -15,7 +15,7 @@ import * as os from '@/os.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const props = withDefaults(defineProps<{ to: string; diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index 76ca8688d1..f6b21343b6 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/> -<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span> -<span v-else>{{ emoji }}</span> +<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span> </template> <script lang="ts" setup> import { computed, inject } from 'vue'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js'; import { defaultStore } from '@/store.js'; -import { getEmojiName } from '@/scripts/emojilist.js'; +import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js'; import * as os from '@/os.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; @@ -30,9 +29,8 @@ const react = inject<((name: string) => void) | null>('react', null); const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native'); -const url = computed(() => { - return char2path(props.emoji); -}); +const url = computed(() => char2path(props.emoji)); +const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji)); // Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter function computeTitle(event: PointerEvent): void { diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 99ed8adbef..dc7474835d 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue'; -import { Resolved, Router } from '@/nirax.js'; +import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue'; +import { IRouter, Resolved } from '@/nirax.js'; import { defaultStore } from '@/store.js'; const props = defineProps<{ - router?: Router; + router?: IRouter; }>(); const router = props.router ?? inject('router'); diff --git a/packages/frontend/src/global/router/definition.ts b/packages/frontend/src/global/router/definition.ts new file mode 100644 index 0000000000..727d6b1bb2 --- /dev/null +++ b/packages/frontend/src/global/router/definition.ts @@ -0,0 +1,571 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue'; +import { IRouter, Router } from '@/nirax.js'; +import { $i, iAmModerator } from '@/account.js'; +import MkLoading from '@/pages/_loading_.vue'; +import MkError from '@/pages/_error_.vue'; +import { setMainRouter } from '@/global/router/main.js'; + +const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ + loader: loader, + loadingComponent: MkLoading, + errorComponent: MkError, +}); +const routes = [{ + path: '/@:initUser/pages/:initPageName/view-source', + component: page(() => import('@/pages/page-editor/page-editor.vue')), +}, { + path: '/@:username/pages/:pageName', + component: page(() => import('@/pages/page.vue')), +}, { + path: '/@:acct/following', + component: page(() => import('@/pages/user/following.vue')), +}, { + path: '/@:acct/followers', + component: page(() => import('@/pages/user/followers.vue')), +}, { + name: 'user', + path: '/@:acct/:page?', + component: page(() => import('@/pages/user/index.vue')), +}, { + name: 'note', + path: '/notes/:noteId', + component: page(() => import('@/pages/note.vue')), +}, { + name: 'list', + path: '/list/:listId', + component: page(() => import('@/pages/list.vue')), +}, { + path: '/clips/:clipId', + component: page(() => import('@/pages/clip.vue')), +}, { + path: '/instance-info/:host', + component: page(() => import('@/pages/instance-info.vue')), +}, { + name: 'settings', + path: '/settings', + component: page(() => import('@/pages/settings/index.vue')), + loginRequired: true, + children: [{ + path: '/profile', + name: 'profile', + component: page(() => import('@/pages/settings/profile.vue')), + }, { + path: '/avatar-decoration', + name: 'avatarDecoration', + component: page(() => import('@/pages/settings/avatar-decoration.vue')), + }, { + path: '/roles', + name: 'roles', + component: page(() => import('@/pages/settings/roles.vue')), + }, { + path: '/privacy', + name: 'privacy', + component: page(() => import('@/pages/settings/privacy.vue')), + }, { + path: '/emoji-picker', + name: 'emojiPicker', + component: page(() => import('@/pages/settings/emoji-picker.vue')), + }, { + path: '/drive', + name: 'drive', + component: page(() => import('@/pages/settings/drive.vue')), + }, { + path: '/drive/cleaner', + name: 'drive', + component: page(() => import('@/pages/settings/drive-cleaner.vue')), + }, { + path: '/notifications', + name: 'notifications', + component: page(() => import('@/pages/settings/notifications.vue')), + }, { + path: '/email', + name: 'email', + component: page(() => import('@/pages/settings/email.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('@/pages/settings/security.vue')), + }, { + path: '/general', + name: 'general', + component: page(() => import('@/pages/settings/general.vue')), + }, { + path: '/theme/install', + name: 'theme', + component: page(() => import('@/pages/settings/theme.install.vue')), + }, { + path: '/theme/manage', + name: 'theme', + component: page(() => import('@/pages/settings/theme.manage.vue')), + }, { + path: '/theme', + name: 'theme', + component: page(() => import('@/pages/settings/theme.vue')), + }, { + path: '/navbar', + name: 'navbar', + component: page(() => import('@/pages/settings/navbar.vue')), + }, { + path: '/statusbar', + name: 'statusbar', + component: page(() => import('@/pages/settings/statusbar.vue')), + }, { + path: '/sounds', + name: 'sounds', + component: page(() => import('@/pages/settings/sounds.vue')), + }, { + path: '/plugin/install', + name: 'plugin', + component: page(() => import('@/pages/settings/plugin.install.vue')), + }, { + path: '/plugin', + name: 'plugin', + component: page(() => import('@/pages/settings/plugin.vue')), + }, { + path: '/import-export', + name: 'import-export', + component: page(() => import('@/pages/settings/import-export.vue')), + }, { + path: '/mute-block', + name: 'mute-block', + component: page(() => import('@/pages/settings/mute-block.vue')), + }, { + path: '/api', + name: 'api', + component: page(() => import('@/pages/settings/api.vue')), + }, { + path: '/apps', + name: 'api', + component: page(() => import('@/pages/settings/apps.vue')), + }, { + path: '/webhook/edit/:webhookId', + name: 'webhook', + component: page(() => import('@/pages/settings/webhook.edit.vue')), + }, { + path: '/webhook/new', + name: 'webhook', + component: page(() => import('@/pages/settings/webhook.new.vue')), + }, { + path: '/webhook', + name: 'webhook', + component: page(() => import('@/pages/settings/webhook.vue')), + }, { + path: '/deck', + name: 'deck', + component: page(() => import('@/pages/settings/deck.vue')), + }, { + path: '/preferences-backups', + name: 'preferences-backups', + component: page(() => import('@/pages/settings/preferences-backups.vue')), + }, { + path: '/migration', + name: 'migration', + component: page(() => import('@/pages/settings/migration.vue')), + }, { + path: '/custom-css', + name: 'general', + component: page(() => import('@/pages/settings/custom-css.vue')), + }, { + path: '/accounts', + name: 'profile', + component: page(() => import('@/pages/settings/accounts.vue')), + }, { + path: '/other', + name: 'other', + component: page(() => import('@/pages/settings/other.vue')), + }, { + path: '/', + component: page(() => import('@/pages/_empty_.vue')), + }], +}, { + path: '/reset-password/:token?', + component: page(() => import('@/pages/reset-password.vue')), +}, { + path: '/signup-complete/:code', + component: page(() => import('@/pages/signup-complete.vue')), +}, { + path: '/announcements', + component: page(() => import('@/pages/announcements.vue')), +}, { + path: '/about', + component: page(() => import('@/pages/about.vue')), + hash: 'initialTab', +}, { + path: '/about-misskey', + component: page(() => import('@/pages/about-misskey.vue')), +}, { + path: '/invite', + name: 'invite', + component: page(() => import('@/pages/invite.vue')), +}, { + path: '/ads', + component: page(() => import('@/pages/ads.vue')), +}, { + path: '/theme-editor', + component: page(() => import('@/pages/theme-editor.vue')), + loginRequired: true, +}, { + path: '/roles/:role', + component: page(() => import('@/pages/role.vue')), +}, { + path: '/user-tags/:tag', + component: page(() => import('@/pages/user-tag.vue')), +}, { + path: '/explore', + component: page(() => import('@/pages/explore.vue')), + hash: 'initialTab', +}, { + path: '/search', + component: page(() => import('@/pages/search.vue')), + query: { + q: 'query', + channel: 'channel', + type: 'type', + origin: 'origin', + }, +}, { + path: '/authorize-follow', + component: page(() => import('@/pages/follow.vue')), + loginRequired: true, +}, { + path: '/share', + component: page(() => import('@/pages/share.vue')), + loginRequired: true, +}, { + path: '/api-console', + component: page(() => import('@/pages/api-console.vue')), + loginRequired: true, +}, { + path: '/scratchpad', + component: page(() => import('@/pages/scratchpad.vue')), +}, { + path: '/auth/:token', + component: page(() => import('@/pages/auth.vue')), +}, { + path: '/miauth/:session', + component: page(() => import('@/pages/miauth.vue')), + query: { + callback: 'callback', + name: 'name', + icon: 'icon', + permission: 'permission', + }, +}, { + path: '/oauth/authorize', + component: page(() => import('@/pages/oauth.vue')), +}, { + path: '/tags/:tag', + component: page(() => import('@/pages/tag.vue')), +}, { + path: '/pages/new', + component: page(() => import('@/pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages/edit/:initPageId', + component: page(() => import('@/pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages', + component: page(() => import('@/pages/pages.vue')), +}, { + path: '/play/:id/edit', + component: page(() => import('@/pages/flash/flash-edit.vue')), + loginRequired: true, +}, { + path: '/play/new', + component: page(() => import('@/pages/flash/flash-edit.vue')), + loginRequired: true, +}, { + path: '/play/:id', + component: page(() => import('@/pages/flash/flash.vue')), +}, { + path: '/play', + component: page(() => import('@/pages/flash/flash-index.vue')), +}, { + path: '/gallery/:postId/edit', + component: page(() => import('@/pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/new', + component: page(() => import('@/pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/:postId', + component: page(() => import('@/pages/gallery/post.vue')), +}, { + path: '/gallery', + component: page(() => import('@/pages/gallery/index.vue')), +}, { + path: '/channels/:channelId/edit', + component: page(() => import('@/pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/new', + component: page(() => import('@/pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/:channelId', + component: page(() => import('@/pages/channel.vue')), +}, { + path: '/channels', + component: page(() => import('@/pages/channels.vue')), +}, { + path: '/custom-emojis-manager', + component: page(() => import('@/pages/custom-emojis-manager.vue')), +}, { + path: '/avatar-decorations', + name: 'avatarDecorations', + component: page(() => import('@/pages/avatar-decorations.vue')), +}, { + path: '/registry/keys/:domain/:path(*)?', + component: page(() => import('@/pages/registry.keys.vue')), +}, { + path: '/registry/value/:domain/:path(*)?', + component: page(() => import('@/pages/registry.value.vue')), +}, { + path: '/registry', + component: page(() => import('@/pages/registry.vue')), +}, { + path: '/install-extentions', + component: page(() => import('@/pages/install-extentions.vue')), + loginRequired: true, +}, { + path: '/admin/user/:userId', + component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.vue')), +}, { + path: '/admin/file/:fileId', + component: iAmModerator ? page(() => import('@/pages/admin-file.vue')) : page(() => import('@/pages/not-found.vue')), +}, { + path: '/admin', + component: iAmModerator ? page(() => import('@/pages/admin/index.vue')) : page(() => import('@/pages/not-found.vue')), + children: [{ + path: '/overview', + name: 'overview', + component: page(() => import('@/pages/admin/overview.vue')), + }, { + path: '/users', + name: 'users', + component: page(() => import('@/pages/admin/users.vue')), + }, { + path: '/emojis', + name: 'emojis', + component: page(() => import('@/pages/custom-emojis-manager.vue')), + }, { + path: '/avatar-decorations', + name: 'avatarDecorations', + component: page(() => import('@/pages/avatar-decorations.vue')), + }, { + path: '/queue', + name: 'queue', + component: page(() => import('@/pages/admin/queue.vue')), + }, { + path: '/files', + name: 'files', + component: page(() => import('@/pages/admin/files.vue')), + }, { + path: '/federation', + name: 'federation', + component: page(() => import('@/pages/admin/federation.vue')), + }, { + path: '/announcements', + name: 'announcements', + component: page(() => import('@/pages/admin/announcements.vue')), + }, { + path: '/ads', + name: 'ads', + component: page(() => import('@/pages/admin/ads.vue')), + }, { + path: '/roles/:id/edit', + name: 'roles', + component: page(() => import('@/pages/admin/roles.edit.vue')), + }, { + path: '/roles/new', + name: 'roles', + component: page(() => import('@/pages/admin/roles.edit.vue')), + }, { + path: '/roles/:id', + name: 'roles', + component: page(() => import('@/pages/admin/roles.role.vue')), + }, { + path: '/roles', + name: 'roles', + component: page(() => import('@/pages/admin/roles.vue')), + }, { + path: '/database', + name: 'database', + component: page(() => import('@/pages/admin/database.vue')), + }, { + path: '/abuses', + name: 'abuses', + component: page(() => import('@/pages/admin/abuses.vue')), + }, { + path: '/modlog', + name: 'modlog', + component: page(() => import('@/pages/admin/modlog.vue')), + }, { + path: '/settings', + name: 'settings', + component: page(() => import('@/pages/admin/settings.vue')), + }, { + path: '/branding', + name: 'branding', + component: page(() => import('@/pages/admin/branding.vue')), + }, { + path: '/moderation', + name: 'moderation', + component: page(() => import('@/pages/admin/moderation.vue')), + }, { + path: '/email-settings', + name: 'email-settings', + component: page(() => import('@/pages/admin/email-settings.vue')), + }, { + path: '/object-storage', + name: 'object-storage', + component: page(() => import('@/pages/admin/object-storage.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('@/pages/admin/security.vue')), + }, { + path: '/relays', + name: 'relays', + component: page(() => import('@/pages/admin/relays.vue')), + }, { + path: '/instance-block', + name: 'instance-block', + component: page(() => import('@/pages/admin/instance-block.vue')), + }, { + path: '/proxy-account', + name: 'proxy-account', + component: page(() => import('@/pages/admin/proxy-account.vue')), + }, { + path: '/external-services', + name: 'external-services', + component: page(() => import('@/pages/admin/external-services.vue')), + }, { + path: '/other-settings', + name: 'other-settings', + component: page(() => import('@/pages/admin/other-settings.vue')), + }, { + path: '/server-rules', + name: 'server-rules', + component: page(() => import('@/pages/admin/server-rules.vue')), + }, { + path: '/invites', + name: 'invites', + component: page(() => import('@/pages/admin/invites.vue')), + }, { + path: '/', + component: page(() => import('@/pages/_empty_.vue')), + }], +}, { + path: '/my/notifications', + component: page(() => import('@/pages/notifications.vue')), + loginRequired: true, +}, { + path: '/my/favorites', + component: page(() => import('@/pages/favorites.vue')), + loginRequired: true, +}, { + path: '/my/achievements', + component: page(() => import('@/pages/achievements.vue')), + loginRequired: true, +}, { + path: '/my/drive/folder/:folder', + component: page(() => import('@/pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/drive', + component: page(() => import('@/pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/drive/file/:fileId', + component: page(() => import('@/pages/drive.file.vue')), + loginRequired: true, +}, { + path: '/my/follow-requests', + component: page(() => import('@/pages/follow-requests.vue')), + loginRequired: true, +}, { + path: '/my/lists/:listId', + component: page(() => import('@/pages/my-lists/list.vue')), + loginRequired: true, +}, { + path: '/my/lists', + component: page(() => import('@/pages/my-lists/index.vue')), + loginRequired: true, +}, { + path: '/my/clips', + component: page(() => import('@/pages/my-clips/index.vue')), + loginRequired: true, +}, { + path: '/my/antennas/create', + component: page(() => import('@/pages/my-antennas/create.vue')), + loginRequired: true, +}, { + path: '/my/antennas/:antennaId', + component: page(() => import('@/pages/my-antennas/edit.vue')), + loginRequired: true, +}, { + path: '/my/antennas', + component: page(() => import('@/pages/my-antennas/index.vue')), + loginRequired: true, +}, { + path: '/timeline/list/:listId', + component: page(() => import('@/pages/user-list-timeline.vue')), + loginRequired: true, +}, { + path: '/timeline/antenna/:antennaId', + component: page(() => import('@/pages/antenna-timeline.vue')), + loginRequired: true, +}, { + path: '/clicker', + component: page(() => import('@/pages/clicker.vue')), + loginRequired: true, +}, { + path: '/bubble-game', + component: page(() => import('@/pages/drop-and-fusion.vue')), + loginRequired: true, +}, { + path: '/timeline', + component: page(() => import('@/pages/timeline.vue')), +}, { + name: 'index', + path: '/', + component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')), + globalCacheKey: 'index', +}, { + path: '/:(*)', + component: page(() => import('@/pages/not-found.vue')), +}]; + +function createRouterImpl(path: string): IRouter { + return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue'))); +} + +/** + * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 + * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) + */ +export function setupRouter(app: App) { + app.provide('routerFactory', createRouterImpl); + + const mainRouter = createRouterImpl(location.pathname + location.search + location.hash); + + window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); + + window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); + }); + + mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); + }); + + setMainRouter(mainRouter); +} diff --git a/packages/frontend/src/global/router/main.ts b/packages/frontend/src/global/router/main.ts new file mode 100644 index 0000000000..5adb3f606f --- /dev/null +++ b/packages/frontend/src/global/router/main.ts @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ShallowRef } from 'vue'; +import { EventEmitter } from 'eventemitter3'; +import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; + +function getMainRouter(): IRouter { + const router = mainRouterHolder; + if (!router) { + throw new Error('mainRouter is not found.'); + } + + return router; +} + +/** + * メインルータを設定する。一度設定すると、それ以降は変更できない。 + * {@link setupRouter}から呼び出されることのみを想定している。 + */ +export function setMainRouter(router: IRouter) { + if (mainRouterHolder) { + throw new Error('mainRouter is already exists.'); + } + + mainRouterHolder = router; +} + +/** + * {@link mainRouter}用のプロキシ実装。 + * {@link mainRouter}は起動シーケンスの一部にて初期化されるため、僅かにundefinedになる期間がある。 + * その僅かな期間のためだけに型をundefined込みにしたくないのでこのクラスを緩衝材として使用する。 + */ +class MainRouterProxy implements IRouter { + private supplier: () => IRouter; + + constructor(supplier: () => IRouter) { + this.supplier = supplier; + } + + get current(): Resolved { + return this.supplier().current; + } + + get currentRef(): ShallowRef<Resolved> { + return this.supplier().currentRef; + } + + get currentRoute(): ShallowRef<RouteDef> { + return this.supplier().currentRoute; + } + + get navHook(): ((path: string, flag?: any) => boolean) | null { + return this.supplier().navHook; + } + + set navHook(value) { + this.supplier().navHook = value; + } + + getCurrentKey(): string { + return this.supplier().getCurrentKey(); + } + + getCurrentPath(): any { + return this.supplier().getCurrentPath(); + } + + push(path: string, flag?: any): void { + this.supplier().push(path, flag); + } + + replace(path: string, key?: string | null): void { + this.supplier().replace(path, key); + } + + resolve(path: string): Resolved | null { + return this.supplier().resolve(path); + } + + eventNames(): Array<EventEmitter.EventNames<RouterEvent>> { + return this.supplier().eventNames(); + } + + listeners<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + ): Array<EventEmitter.EventListener<RouterEvent, T>> { + return this.supplier().listeners(event); + } + + listenerCount( + event: EventEmitter.EventNames<RouterEvent>, + ): number { + return this.supplier().listenerCount(event); + } + + emit<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + ...args: EventEmitter.EventArgs<RouterEvent, T> + ): boolean { + return this.supplier().emit(event, ...args); + } + + on<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + ): this { + this.supplier().on(event, fn, context); + return this; + } + + addListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + ): this { + this.supplier().addListener(event, fn, context); + return this; + } + + once<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + ): this { + this.supplier().once(event, fn, context); + return this; + } + + removeListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean, + ): this { + this.supplier().removeListener(event, fn, context, once); + return this; + } + + off<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean, + ): this { + this.supplier().off(event, fn, context, once); + return this; + } + + removeAllListeners( + event?: EventEmitter.EventNames<RouterEvent>, + ): this { + this.supplier().removeAllListeners(event); + return this; + } +} + +let mainRouterHolder: IRouter | null = null; + +export const mainRouter: IRouter = new MainRouterProxy(getMainRouter); diff --git a/packages/frontend/src/global/router/supplier.ts b/packages/frontend/src/global/router/supplier.ts new file mode 100644 index 0000000000..1e321ef21f --- /dev/null +++ b/packages/frontend/src/global/router/supplier.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { inject } from 'vue'; +import { IRouter, Router } from '@/nirax.js'; +import { mainRouter } from '@/global/router/main.js'; + +/** + * メインの{@link Router}を取得する。 + * あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない) + */ +export function useRouter(): IRouter { + return inject<Router | null>('router', null) ?? mainRouter; +} + +/** + * 任意の{@link Router}を取得するためのファクトリを取得する。 + * あらかじめ{@link setupRouter}を実行しておく必要がある。 + */ +export function useRouterFactory(): (path: string) => IRouter { + const factory = inject<(path: string) => IRouter>('routerFactory'); + if (!factory) { + console.error('routerFactory is not defined.'); + throw new Error('routerFactory is not defined.'); + } + + return factory; +} diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html index 8de01e4802..11555ea18a 100644 --- a/packages/frontend/src/index.html +++ b/packages/frontend/src/index.html @@ -16,13 +16,13 @@ <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" - content="default-src 'self'; + content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/; worker-src 'self'; - script-src 'self' 'unsafe-eval'; + script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; - img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; + img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; - connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;" + connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;" /> <meta property="og:site_name" content="[DEV BUILD] Misskey" /> <meta name="viewport" content="width=device-width, initial-scale=1"> diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts index 9755bdcb18..a56aa6419e 100644 --- a/packages/frontend/src/nirax.ts +++ b/packages/frontend/src/nirax.ts @@ -5,11 +5,11 @@ // NIRAX --- A lightweight router -import { EventEmitter } from 'eventemitter3'; import { Component, onMounted, shallowRef, ShallowRef } from 'vue'; +import { EventEmitter } from 'eventemitter3'; import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; -type RouteDef = { +export type RouteDef = { path: string; component: Component; query?: Record<string, string>; @@ -27,6 +27,27 @@ type ParsedPath = (string | { optional?: boolean; })[]; +export type RouterEvent = { + change: (ctx: { + beforePath: string; + path: string; + resolved: Resolved; + key: string; + }) => void; + replace: (ctx: { + path: string; + key: string; + }) => void; + push: (ctx: { + beforePath: string; + path: string; + route: RouteDef | null; + props: Map<string, string> | null; + key: string; + }) => void; + same: () => void; +} + export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; }; function parsePath(path: string): ParsedPath { @@ -54,26 +75,85 @@ function parsePath(path: string): ParsedPath { return res; } -export class Router extends EventEmitter<{ - change: (ctx: { - beforePath: string; - path: string; - resolved: Resolved; - key: string; - }) => void; - replace: (ctx: { - path: string; - key: string; - }) => void; - push: (ctx: { - beforePath: string; - path: string; - route: RouteDef | null; - props: Map<string, string> | null; - key: string; - }) => void; - same: () => void; -}> { +export interface IRouter extends EventEmitter<RouterEvent> { + current: Resolved; + currentRef: ShallowRef<Resolved>; + currentRoute: ShallowRef<RouteDef>; + navHook: ((path: string, flag?: any) => boolean) | null; + + resolve(path: string): Resolved | null; + + getCurrentPath(): any; + + getCurrentKey(): string; + + push(path: string, flag?: any): void; + + replace(path: string, key?: string | null): void; + + /** @see EventEmitter */ + eventNames(): Array<EventEmitter.EventNames<RouterEvent>>; + + /** @see EventEmitter */ + listeners<T extends EventEmitter.EventNames<RouterEvent>>( + event: T + ): Array<EventEmitter.EventListener<RouterEvent, T>>; + + /** @see EventEmitter */ + listenerCount( + event: EventEmitter.EventNames<RouterEvent> + ): number; + + /** @see EventEmitter */ + emit<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + ...args: EventEmitter.EventArgs<RouterEvent, T> + ): boolean; + + /** @see EventEmitter */ + on<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any + ): this; + + /** @see EventEmitter */ + addListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any + ): this; + + /** @see EventEmitter */ + once<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn: EventEmitter.EventListener<RouterEvent, T>, + context?: any + ): this; + + /** @see EventEmitter */ + removeListener<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean | undefined + ): this; + + /** @see EventEmitter */ + off<T extends EventEmitter.EventNames<RouterEvent>>( + event: T, + fn?: EventEmitter.EventListener<RouterEvent, T>, + context?: any, + once?: boolean | undefined + ): this; + + /** @see EventEmitter */ + removeAllListeners( + event?: EventEmitter.EventNames<RouterEvent> + ): this; +} + +export class Router extends EventEmitter<RouterEvent> implements IRouter { private routes: RouteDef[]; public current: Resolved; public currentRef: ShallowRef<Resolved> = shallowRef(); @@ -277,7 +357,7 @@ export class Router extends EventEmitter<{ } } -export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) { +export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: IRouter) { const scrollPosStore = new Map<string, number>(); onMounted(() => { diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 99b8070b71..37f8227485 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="provider"> <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> <option value="hcaptcha">hCaptcha</option> + <option value="mcaptcha">mCaptcha</option> <option value="recaptcha">reCAPTCHA</option> <option value="turnstile">Turnstile</option> </MkRadios> @@ -28,6 +29,24 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> </FormSlot> </template> + <template v-else-if="provider === 'mcaptcha'"> + <MkInput v-model="mcaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="mcaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> + </MkInput> + <MkInput v-model="mcaptchaInstanceUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> + </MkInput> + <FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl"> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="mcaptcha" :sitekey="mcaptchaSiteKey" :instanceUrl="mcaptchaInstanceUrl"/> + </FormSlot> + </template> <template v-else-if="provider === 'recaptcha'"> <MkInput v-model="recaptchaSiteKey"> <template #prefix><i class="ti ti-key"></i></template> @@ -81,6 +100,9 @@ const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue' const provider = ref<CaptchaProvider | null>(null); const hcaptchaSiteKey = ref<string | null>(null); const hcaptchaSecretKey = ref<string | null>(null); +const mcaptchaSiteKey = ref<string | null>(null); +const mcaptchaSecretKey = ref<string | null>(null); +const mcaptchaInstanceUrl = ref<string | null>(null); const recaptchaSiteKey = ref<string | null>(null); const recaptchaSecretKey = ref<string | null>(null); const turnstileSiteKey = ref<string | null>(null); @@ -90,12 +112,18 @@ async function init() { const meta = await misskeyApi('admin/meta'); hcaptchaSiteKey.value = meta.hcaptchaSiteKey; hcaptchaSecretKey.value = meta.hcaptchaSecretKey; + mcaptchaSiteKey.value = meta.mcaptchaSiteKey; + mcaptchaSecretKey.value = meta.mcaptchaSecretKey; + mcaptchaInstanceUrl.value = meta.mcaptchaInstanceUrl; recaptchaSiteKey.value = meta.recaptchaSiteKey; recaptchaSecretKey.value = meta.recaptchaSecretKey; turnstileSiteKey.value = meta.turnstileSiteKey; turnstileSecretKey.value = meta.turnstileSecretKey; - provider.value = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null; + provider.value = meta.enableHcaptcha ? 'hcaptcha' : + meta.enableRecaptcha ? 'recaptcha' : + meta.enableTurnstile ? 'turnstile' : + meta.enableMcaptcha ? 'mcaptcha' : null; } function save() { @@ -103,6 +131,10 @@ function save() { enableHcaptcha: provider.value === 'hcaptcha', hcaptchaSiteKey: hcaptchaSiteKey.value, hcaptchaSecretKey: hcaptchaSecretKey.value, + enableMcaptcha: provider.value === 'mcaptcha', + mcaptchaSiteKey: mcaptchaSiteKey.value, + mcaptchaSecretKey: mcaptchaSecretKey.value, + mcaptchaInstanceUrl: mcaptchaInstanceUrl.value, enableRecaptcha: provider.value === 'recaptcha', recaptchaSiteKey: recaptchaSiteKey.value, recaptchaSecretKey: recaptchaSecretKey.value, diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 333bac724b..7106ed7438 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -36,8 +36,8 @@ import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js'; -import { useRouter } from '@/router.js'; import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const isEmpty = (x: string | null) => x == null || x === ''; diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index db0acae24a..82e230d6a6 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -31,9 +31,9 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; import MkButton from '@/components/MkButton.vue'; import { rolesCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index d5ce190ef2..ff29f4ec1f 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -70,12 +70,12 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; import MkButton from '@/components/MkButton.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPagination from '@/components/MkPagination.vue'; import { infoImageUrl } from '@/instance.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index f7c4048b23..732affd77d 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -237,9 +237,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance } from '@/instance.js'; -import { useRouter } from '@/router.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { ROLE_POLICIES } from '@/const.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); const baseRoleQ = ref(''); diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index ec0c6166d0..a691d8ea1e 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-shield"></i></template> <template #label>{{ i18n.ts.botProtection }}</template> <template v-if="enableHcaptcha" #suffix>hCaptcha</template> + <template v-else-if="enableMcaptcha" #suffix>mCaptcha</template> <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> <template v-else-if="enableTurnstile" #suffix>Turnstile</template> <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> @@ -155,6 +156,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const summalyProxy = ref<string>(''); const enableHcaptcha = ref<boolean>(false); +const enableMcaptcha = ref<boolean>(false); const enableRecaptcha = ref<boolean>(false); const enableTurnstile = ref<boolean>(false); const sensitiveMediaDetection = ref<string>('none'); @@ -174,6 +176,7 @@ async function init() { const meta = await misskeyApi('admin/meta'); summalyProxy.value = meta.summalyProxy; enableHcaptcha.value = meta.enableHcaptcha; + enableMcaptcha.value = meta.enableMcaptcha; enableRecaptcha.value = meta.enableRecaptcha; enableTurnstile.value = meta.enableTurnstile; sensitiveMediaDetection.value = meta.sensitiveMediaDetection; diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index d96ca4208b..7f07ac4987 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -30,9 +30,9 @@ import MkTimeline from '@/components/MkTimeline.vue'; import { scroll } from '@/scripts/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 727778b6e6..99b93444db 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -77,12 +77,12 @@ import MkColorInput from '@/components/MkColorInput.vue'; import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import { useRouter } from '@/global/router/supplier.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 667563bd16..e698098f35 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -75,7 +75,6 @@ import MkTimeline from '@/components/MkTimeline.vue'; import XChannelFollowButton from '@/components/MkChannelFollowButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useRouter } from '@/router.js'; import { $i, iAmModerator } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -92,6 +91,7 @@ import { PageHeaderItem } from '@/types/page-header.js'; import { isSupportShare } from '@/scripts/navigator.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { miLocalStorage } from '@/local-storage.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index b7cc5cd36e..80a401eee7 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -58,9 +58,9 @@ 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.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 4c635028f3..64c3ad70ba 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -80,7 +80,7 @@ import { infoImageUrl } from '@/instance.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue new file mode 100644 index 0000000000..0ddee55f5f --- /dev/null +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -0,0 +1,825 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header><MkPageHeader/></template> + <MkSpacer :contentMax="800"> + <div v-show="!gameStarted" :class="$style.root"> + <div style="text-align: center;" class="_gaps"> + <div :class="$style.frame"> + <div :class="$style.frameInner"> + <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> + </div> + </div> + <div :class="$style.frame"> + <div :class="$style.frameInner"> + <div class="_gaps" style="padding: 16px;"> + <MkSelect v-model="gameMode"> + <option value="normal">NORMAL</option> + <option value="square">SQUARE</option> + </MkSelect> + <MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton> + </div> + </div> + </div> + </div> + </div> + <div v-show="gameStarted" class="_gaps_s" :class="$style.root"> + <div style="display: flex;"> + <div :class="$style.frame" style="flex: 1; margin-right: 10px;"> + <div :class="$style.frameInner"> + <b>BUBBLE GAME</b> + <div>- {{ gameMode }} -</div> + </div> + </div> + <div :class="[$style.frame, $style.stock]" style="margin-left: auto;"> + <div :class="$style.frameInner" style="text-align: center;"> + NEXT >>> + <TransitionGroup + :enterActiveClass="$style.transition_stock_enterActive" + :leaveActiveClass="$style.transition_stock_leaveActive" + :enterFromClass="$style.transition_stock_enterFrom" + :leaveToClass="$style.transition_stock_leaveTo" + :moveClass="$style.transition_stock_move" + > + <div v-for="x in stock" :key="x.id" style="display: inline-block;"> + <img :src="game.getTextureImageUrl(x.mono)" style="width: 32px;"/> + </div> + </TransitionGroup> + </div> + </div> + </div> + <div :class="$style.main" @contextmenu.stop.prevent> + <div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove"> + <img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/> + <img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/> + <canvas ref="canvasEl" :class="$style.canvas"/> + <Transition + :enterActiveClass="$style.transition_combo_enterActive" + :leaveActiveClass="$style.transition_combo_leaveActive" + :enterFromClass="$style.transition_combo_enterFrom" + :leaveToClass="$style.transition_combo_leaveTo" + :moveClass="$style.transition_combo_move" + > + <div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div> + </Transition> + <img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/> + <Transition + :enterActiveClass="$style.transition_picked_enterActive" + :leaveActiveClass="$style.transition_picked_leaveActive" + :enterFromClass="$style.transition_picked_enterFrom" + :leaveToClass="$style.transition_picked_leaveTo" + :moveClass="$style.transition_picked_move" + mode="out-in" + > + <img v-if="currentPick" :key="currentPick.id" :src="game.getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ top: -(currentPick?.mono.size / 2) + 'px', left: (dropperX - (currentPick?.mono.size / 2)) + 'px', width: `${currentPick?.mono.size}px` }"/> + </Transition> + <template v-if="dropReady && currentPick"> + <img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow" :style="{ top: (currentPick.mono.size / 2) + 10 + 'px', left: (dropperX - 10) + 'px', width: `20px` }"/> + <div :class="$style.dropGuide" :style="{ left: (dropperX - 2) + 'px' }"/> + </template> + <div v-if="gameOver" :class="$style.gameOverLabel"> + <div class="_gaps_s"> + <img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/> + <div>SCORE: <MkNumber :value="score"/></div> + <div>MAX CHAIN: <MkNumber :value="maxCombo"/></div> + <div class="_buttonsCenter"> + <MkButton primary rounded @click="restart">Restart</MkButton> + <MkButton primary rounded @click="share">Share</MkButton> + </div> + </div> + </div> + </div> + </div> + <div style="display: flex;"> + <div :class="$style.frame" style="flex: 1; margin-right: 10px;"> + <div :class="$style.frameInner"> + <div>SCORE: <b><MkNumber :value="score"/></b> (MAX CHAIN: <b><MkNumber :value="maxCombo"/></b>)</div> + <div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div> + </div> + </div> + <div :class="[$style.frame]" style="margin-left: auto;"> + <div :class="$style.frameInner" style="text-align: center;"> + <div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div> + </div> + </div> + </div> + <div v-if="showConfig" :class="$style.frame"> + <div :class="$style.frameInner"> + <MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.0025" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true"> + <template #label>BGM {{ i18n.ts.volume }}</template> + </MkRange> + </div> + </div> + <div v-if="showConfig" :class="$style.frame"> + <div :class="$style.frameInner"> + <div>Credit</div> + <div>BGM: @ys@misskey.design</div> + </div> + </div> + <div :class="$style.frame"> + <div :class="$style.frameInner"> + <MkButton @click="restart">Restart</MkButton> + </div> + </div> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { onDeactivated, ref, shallowRef, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import * as os from '@/os.js'; +import MkNumber from '@/components/MkNumber.vue'; +import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; +import MkButton from '@/components/MkButton.vue'; +import { claimAchievement } from '@/scripts/achievements.js'; +import { defaultStore } from '@/store.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { useInterval } from '@/scripts/use-interval.js'; +import MkSelect from '@/components/MkSelect.vue'; +import { apiUrl } from '@/config.js'; +import { $i } from '@/account.js'; +import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js'; +import * as sound from '@/scripts/sound.js'; +import MkRange from '@/components/MkRange.vue'; + +const containerEl = shallowRef<HTMLElement>(); +const canvasEl = shallowRef<HTMLCanvasElement>(); +const dropperX = ref(0); + +const NORMAL_BASE_SIZE = 30; +const NORAML_MONOS: Mono[] = [{ + id: '9377076d-c980-4d83-bdaf-175bc58275b7', + level: 10, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 512, + dropCandidate: false, + sfxPitch: 0.25, + img: '/client-assets/drop-and-fusion/exploding_head.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'be9f38d2-b267-4b1a-b420-904e22e80568', + level: 9, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 256, + dropCandidate: false, + sfxPitch: 0.5, + img: '/client-assets/drop-and-fusion/face_with_symbols_on_mouth.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'beb30459-b064-4888-926b-f572e4e72e0c', + level: 8, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 128, + dropCandidate: false, + sfxPitch: 0.75, + img: '/client-assets/drop-and-fusion/cold_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0', + level: 7, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 64, + dropCandidate: false, + sfxPitch: 1, + img: '/client-assets/drop-and-fusion/zany_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a', + level: 6, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 32, + dropCandidate: false, + sfxPitch: 1.5, + img: '/client-assets/drop-and-fusion/pleading_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '249c728e-230f-4332-bbbf-281c271c75b2', + level: 5, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 16, + dropCandidate: true, + sfxPitch: 2, + img: '/client-assets/drop-and-fusion/face_with_open_mouth.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '23d67613-d484-4a93-b71e-3e81b19d6186', + level: 4, + size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 8, + dropCandidate: true, + sfxPitch: 2.5, + img: '/client-assets/drop-and-fusion/smiling_face_with_sunglasses.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99', + level: 3, + size: NORMAL_BASE_SIZE * 1.25 * 1.25, + shape: 'circle', + score: 4, + dropCandidate: true, + sfxPitch: 3, + img: '/client-assets/drop-and-fusion/grinning_squinting_face.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5', + level: 2, + size: NORMAL_BASE_SIZE * 1.25, + shape: 'circle', + score: 2, + dropCandidate: true, + sfxPitch: 3.5, + img: '/client-assets/drop-and-fusion/smiling_face_with_hearts.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '64ec4add-ce39-42b4-96cb-33908f3f118d', + level: 1, + size: NORMAL_BASE_SIZE, + shape: 'circle', + score: 1, + dropCandidate: true, + sfxPitch: 4, + img: '/client-assets/drop-and-fusion/heart_suit.png', + imgSize: 256, + spriteScale: 1.12, +}]; + +const SQUARE_BASE_SIZE = 28; +const SQUARE_MONOS: Mono[] = [{ + id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525', + level: 10, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 512, + dropCandidate: false, + sfxPitch: 0.25, + img: '/client-assets/drop-and-fusion/keycap_10.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1', + level: 9, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 256, + dropCandidate: false, + sfxPitch: 0.5, + img: '/client-assets/drop-and-fusion/keycap_9.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '41607ef3-b6d6-4829-95b6-3737bf8bb956', + level: 8, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 128, + dropCandidate: false, + sfxPitch: 0.75, + img: '/client-assets/drop-and-fusion/keycap_8.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416', + level: 7, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 64, + dropCandidate: false, + sfxPitch: 1, + img: '/client-assets/drop-and-fusion/keycap_7.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '1092e069-fe1a-450b-be97-b5d477ec398c', + level: 6, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 32, + dropCandidate: false, + sfxPitch: 1.5, + img: '/client-assets/drop-and-fusion/keycap_6.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0', + level: 5, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 16, + dropCandidate: true, + sfxPitch: 2, + img: '/client-assets/drop-and-fusion/keycap_5.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a', + level: 4, + size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 8, + dropCandidate: true, + sfxPitch: 2.5, + img: '/client-assets/drop-and-fusion/keycap_4.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919', + level: 3, + size: SQUARE_BASE_SIZE * 1.25 * 1.25, + shape: 'rectangle', + score: 4, + dropCandidate: true, + sfxPitch: 3, + img: '/client-assets/drop-and-fusion/keycap_3.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d', + level: 2, + size: SQUARE_BASE_SIZE * 1.25, + shape: 'rectangle', + score: 2, + dropCandidate: true, + sfxPitch: 3.5, + img: '/client-assets/drop-and-fusion/keycap_2.png', + imgSize: 256, + spriteScale: 1.12, +}, { + id: '35e476ee-44bd-4711-ad42-87be245d3efd', + level: 1, + size: SQUARE_BASE_SIZE, + shape: 'rectangle', + score: 1, + dropCandidate: true, + sfxPitch: 4, + img: '/client-assets/drop-and-fusion/keycap_1.png', + imgSize: 256, + spriteScale: 1.12, +}]; + +const GAME_WIDTH = 450; +const GAME_HEIGHT = 600; + +let viewScaleX = 1; +let viewScaleY = 1; +const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null); +const stock = shallowRef<{ id: string; mono: Mono }[]>([]); +const score = ref(0); +const combo = ref(0); +const comboPrev = ref(0); +const maxCombo = ref(0); +const dropReady = ref(true); +const gameMode = ref<'normal' | 'square'>('normal'); +const gameOver = ref(false); +const gameStarted = ref(false); +const highScore = ref<number | null>(null); +const showConfig = ref(false); +const bgmVolume = ref(0.1); + +let game: DropAndFusionGame; +let containerElRect: DOMRect | null = null; + +function onClick(ev: MouseEvent) { + if (!containerElRect) return; + const x = (ev.clientX - containerElRect.left) / viewScaleX; + game.drop(x); +} + +function onTouchend(ev: TouchEvent) { + if (!containerElRect) return; + const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScaleX; + game.drop(x); +} + +function onMousemove(ev: MouseEvent) { + if (!containerElRect) return; + const x = (ev.clientX - containerElRect.left); + moveDropper(containerElRect, x); +} + +function onTouchmove(ev: TouchEvent) { + if (!containerElRect) return; + const x = (ev.touches[0].clientX - containerElRect.left); + moveDropper(containerElRect, x); +} + +function moveDropper(rect: DOMRect, x: number) { + dropperX.value = Math.min(rect.width * ((GAME_WIDTH - game.PLAYAREA_MARGIN) / GAME_WIDTH), Math.max(rect.width * (game.PLAYAREA_MARGIN / GAME_WIDTH), x)); +} + +function restart() { + game.dispose(); + gameOver.value = false; + currentPick.value = null; + dropReady.value = true; + stock.value = []; + score.value = 0; + combo.value = 0; + comboPrev.value = 0; + gameStarted.value = false; +} + +function attachGameEvents() { + game.addListener('changeScore', value => { + score.value = value; + }); + + game.addListener('changeCombo', value => { + if (value === 0) { + comboPrev.value = combo.value; + } else { + comboPrev.value = value; + } + maxCombo.value = Math.max(maxCombo.value, value); + combo.value = value; + }); + + game.addListener('changeStock', value => { + currentPick.value = JSON.parse(JSON.stringify(value[0])); + stock.value = JSON.parse(JSON.stringify(value.slice(1))); + }); + + game.addListener('dropped', () => { + dropReady.value = false; + window.setTimeout(() => { + if (!gameOver.value) { + dropReady.value = true; + } + }, game.DROP_INTERVAL); + }); + + game.addListener('fusioned', (x, y, scoreDelta) => { + if (!canvasEl.value) return; + + const rect = canvasEl.value.getBoundingClientRect(); + const domX = rect.left + (x * viewScaleX); + const domY = rect.top + (y * viewScaleY); + os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end'); + os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta }, {}, 'end'); + }); + + game.addListener('monoAdded', (mono) => { + // 実績関連 + if (mono.level === 10) { + claimAchievement('bubbleGameExplodingHead'); + + const monos = game.getActiveMonos(); + if (monos.filter(x => x.level === 10).length >= 2) { + claimAchievement('bubbleGameDoubleExplodingHead'); + } + } + }); + + game.addListener('gameOver', () => { + currentPick.value = null; + dropReady.value = false; + gameOver.value = true; + + if (score.value > (highScore.value ?? 0)) { + highScore.value = score.value; + + misskeyApi('i/registry/set', { + scope: ['dropAndFusionGame'], + key: 'highScore:' + gameMode.value, + value: highScore.value, + }); + } + }); +} + +let bgmNodes: ReturnType<typeof sound.createSourceNode> = null; + +async function start() { + try { + highScore.value = await misskeyApi('i/registry/get', { + scope: ['dropAndFusionGame'], + key: 'highScore:' + gameMode.value, + }); + } catch (err) { + highScore.value = null; + } + + game = new DropAndFusionGame({ + width: GAME_WIDTH, + height: GAME_HEIGHT, + canvas: canvasEl.value!, + ...( + gameMode.value === 'normal' ? { + monoDefinitions: NORAML_MONOS, + } : { + monoDefinitions: SQUARE_MONOS, + } + ), + }); + attachGameEvents(); + os.promiseDialog(game.load(), async () => { + game.start(); + gameStarted.value = true; + + if (bgmNodes) { + bgmNodes.soundSource.stop(); + bgmNodes = null; + } + const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3'); + if (!bgmBuffer) return; + bgmNodes = sound.createSourceNode(bgmBuffer, bgmVolume.value); + if (!bgmNodes) return; + bgmNodes.soundSource.loop = true; + bgmNodes.soundSource.start(); + }); +} + +watch(bgmVolume, (value) => { + if (bgmNodes) { + bgmNodes.gainNode.gain.value = value; + } +}); + +function getGameImageDriveFile() { + return new Promise<Misskey.entities.DriveFile | null>(res => { + const dcanvas = document.createElement('canvas'); + dcanvas.width = GAME_WIDTH; + dcanvas.height = GAME_HEIGHT; + const ctx = dcanvas.getContext('2d'); + if (!ctx || !canvasEl.value) return res(null); + const dimage = new Image(); + dimage.src = '/client-assets/drop-and-fusion/frame-light.svg'; + dimage.addEventListener('load', () => { + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); + ctx.drawImage(dimage, 0, 0, GAME_WIDTH, GAME_HEIGHT); + ctx.drawImage(canvasEl.value!, 0, 0, GAME_WIDTH, GAME_HEIGHT); + + dcanvas.toBlob(blob => { + if (!blob) return res(null); + if ($i == null) return res(null); + const formData = new FormData(); + formData.append('file', blob); + formData.append('name', `bubble-game-${Date.now()}.png`); + formData.append('isSensitive', 'false'); + formData.append('comment', 'null'); + formData.append('i', $i.token); + if (defaultStore.state.uploadFolder) { + formData.append('folderId', defaultStore.state.uploadFolder); + } + + window.fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: formData, + }) + .then(response => response.json()) + .then(f => { + res(f); + }); + }, 'image/png'); + + dcanvas.remove(); + }); + }); +} + +async function share() { + const uploading = getGameImageDriveFile(); + os.promiseDialog(uploading); + const file = await uploading; + if (!file) return; + os.post({ + initialText: `#BubbleGame +MODE: ${gameMode.value} +SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})})`, + initialFiles: [file], + }); +} + +useInterval(() => { + if (!canvasEl.value) return; + const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width; + const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height; + viewScaleX = actualCanvasWidth / GAME_WIDTH; + viewScaleY = actualCanvasHeight / GAME_HEIGHT; + containerElRect = containerEl.value?.getBoundingClientRect() ?? null; +}, 1000, { immediate: false, afterMounted: true }); + +onDeactivated(() => { + game.dispose(); +}); + +definePageMetadata({ + title: i18n.ts.bubbleGame, + icon: 'ti ti-apple', +}); +</script> + +<style lang="scss" module> +.transition_stock_move, +.transition_stock_enterActive, +.transition_stock_leaveActive { + transition: opacity 0.4s cubic-bezier(0,.5,.5,1), transform 0.4s cubic-bezier(0,.5,.5,1) !important; +} +.transition_stock_enterFrom, +.transition_stock_leaveTo { + opacity: 0; + transform: scale(0.7); +} +.transition_stock_leaveActive { + position: absolute; +} + +.transition_picked_move, +.transition_picked_enterActive { + transition: opacity 0.5s cubic-bezier(0,.5,.5,1), transform 0.5s cubic-bezier(0,.5,.5,1) !important; +} +.transition_picked_leaveActive { + transition: all 0s !important; +} +.transition_picked_enterFrom, +.transition_picked_leaveTo { + opacity: 0; + transform: translateY(-50px); +} +.transition_picked_leaveActive { + position: absolute; +} + +.transition_combo_move, +.transition_combo_enterActive { + transition: all 0s !important; +} +.transition_combo_leaveActive { + transition: opacity 0.4s cubic-bezier(0,.5,.5,1), transform 0.4s cubic-bezier(0,.5,.5,1) !important; +} +.transition_combo_enterFrom, +.transition_combo_leaveTo { + opacity: 0; + transform: scale(0.7); +} +.transition_combo_leaveActive { + position: absolute; +} + +.root { + margin: 0 auto; + max-width: 600px; + user-select: none; + + * { + user-select: none; + } +} + +.frame { + padding: 7px; + background: #8C4F26; + box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; + border-radius: 10px; +} +.frameInner { + padding: 4px 8px; + background: #F1E8DC; + box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; + border-radius: 6px; + color: #693410; +} + +.main { + position: relative; +} + +.mainFrameImg { + position: absolute; + top: 0; + left: 0; + width: 100%; + // なんかiOSでちらつく + //filter: drop-shadow(0 6px 16px #0007); + pointer-events: none; + user-select: none; +} + +.canvas { + position: relative; + display: block; + z-index: 1; + margin-top: -50px; + width: 100% !important; + height: auto !important; + pointer-events: none; + user-select: none; +} + +.container { + position: relative; +} + +.stock { + pointer-events: none; + user-select: none; +} + +.combo { + position: absolute; + z-index: 3; + top: 50%; + width: 100%; + text-align: center; + font-weight: bold; + font-style: oblique; + color: #fff; + -webkit-text-stroke: 1px rgb(255, 145, 0); + text-shadow: 0 0 6px #0005; + pointer-events: none; + user-select: none; +} + +.currentMono { + position: absolute; + margin-top: 80px; + z-index: 2; + filter: drop-shadow(0 6px 16px #0007); + pointer-events: none; + user-select: none; +} + +.dropper { + position: absolute; + top: 0; + width: 70px; + margin-top: -10px; + margin-left: -30px; + z-index: 2; + filter: drop-shadow(0 6px 16px #0007); + pointer-events: none; + user-select: none; +} + +.currentMonoArrow { + position: absolute; + margin-top: 100px; + z-index: 3; + animation: currentMonoArrow 2s ease infinite; + pointer-events: none; + user-select: none; +} + +.dropGuide { + position: absolute; + top: 120px; + z-index: 3; + width: 3px; + height: calc(100% - 120px); + background: #f002; + pointer-events: none; + user-select: none; +} + +.gameOverLabel { + position: absolute; + z-index: 10; + top: 50%; + width: 100%; + padding: 16px; + box-sizing: border-box; + background: #0007; + color: #fff; + text-align: center; + font-weight: bold; +} + +.gameOver { + .canvas { + filter: grayscale(1); + } +} + +@keyframes currentMonoArrow { + 0% { transform: translateY(0); } + 25% { transform: translateY(-8px); } + 50% { transform: translateY(0); } + 75% { transform: translateY(-8px); } + 100% { transform: translateY(0); } +} +</style> diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index ce077779c8..8298dc6d79 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -45,7 +45,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const PRESET_DEFAULT = `/// @ 0.16.0 diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index e0b9f87d46..7852018894 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -42,9 +42,9 @@ import { computed, ref } from 'vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue index 5a21604080..eefef828bd 100644 --- a/packages/frontend/src/pages/follow.vue +++ b/packages/frontend/src/pages/follow.vue @@ -13,9 +13,9 @@ import { } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { mainRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; +import { mainRouter } from '@/global/router/main.js'; async function follow(user): Promise<void> { const { canceled } = await os.confirm({ diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index e0c7654531..f7db01ce95 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -48,9 +48,9 @@ import FormSuspense from '@/components/form/suspense.vue'; import { selectFiles } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 8d9ac07805..0198ab9700 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -53,7 +53,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index f71fe0f260..dcd427d6b4 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -72,13 +72,13 @@ import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import { url } from '@/config.js'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index c5b1b54222..61b9424bdd 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -14,8 +14,8 @@ import { ref } from 'vue'; import XAntenna from './editor.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; import { antennasCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index 0648f5340f..b4ca7cc9f8 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -15,9 +15,9 @@ import * as Misskey from 'misskey-js'; import XAntenna from './editor.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 5798070ad8..85775a2fdd 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -58,7 +58,6 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { mainRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { userPage } from '@/filters/user.js'; @@ -70,6 +69,7 @@ import { userListsCache } from '@/cache.js'; import { signinRequired } from '@/account.js'; import { defaultStore } from '@/store.js'; import MkPagination from '@/components/MkPagination.vue'; +import { mainRouter } from '@/global/router/main.js'; const $i = signinRequired(); diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 496a8c3274..6db72dccba 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -73,10 +73,10 @@ import { url } from '@/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFile } from '@/scripts/select-file.js'; -import { mainRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ initPageId?: string; diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index bc51b55c7f..22ab9ced09 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -40,9 +40,9 @@ import { computed, ref } from 'vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index c9d193b787..d8dec27513 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -25,8 +25,8 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ token?: string; diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 1b12910a38..811218faf5 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -51,8 +51,8 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { useRouter } from '@/router.js'; import MkFolder from '@/components/MkFolder.vue'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index 5e9048ee57..82cedc9833 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -34,7 +34,7 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 990eff99c1..70d718f1ab 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -21,6 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> </FormSection> + <FormSection> + <template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.clips }}</template> + <MkFolder> + <template #label>{{ i18n.ts.export }}</template> + <template #icon><i class="ti ti-download"></i></template> + <MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> + </MkFolder> + </FormSection> <FormSection> <template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template> <div class="_gaps_s"> @@ -157,6 +165,10 @@ const exportFavorites = () => { misskeyApi('i/export-favorites', {}).then(onExportSuccess).catch(onError); }; +const exportClips = () => { + misskeyApi('i/export-clips', {}).then(onExportSuccess).catch(onError); +}; + const exportFollowing = () => { misskeyApi('i/export-following', { excludeMuting: excludeMutingUsers.value, diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index ee0188873e..be443033bc 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -35,9 +35,9 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue'; import { signout, $i } from '@/account.js'; import { clearCache } from '@/scripts/clear-cache.js'; import { instance } from '@/instance.js'; -import { useRouter } from '@/router.js'; import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import * as os from '@/os.js'; +import { useRouter } from '@/global/router/supplier.js'; const indexInfo = { title: i18n.ts.settings, diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index 9eb344bd46..a122c4c819 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -51,7 +51,7 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 19c376c77b..10a21ef20d 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -29,9 +29,9 @@ import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; import { scroll } from '@/scripts/scroll.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 5258165d7c..ed9722b7ed 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -166,13 +166,13 @@ import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { $i, iAmModerator } from '@/account.js'; import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { useRouter } from '@/global/router/supplier.js'; function calcAge(birthdate: string): number { const date = new Date(birthdate); diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts deleted file mode 100644 index baee85866c..0000000000 --- a/packages/frontend/src/router.ts +++ /dev/null @@ -1,557 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue'; -import { Router } from '@/nirax.js'; -import { $i, iAmModerator } from '@/account.js'; -import MkLoading from '@/pages/_loading_.vue'; -import MkError from '@/pages/_error_.vue'; - -export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ - loader: loader, - loadingComponent: MkLoading, - errorComponent: MkError, -}); - -export const routes = [{ - path: '/@:initUser/pages/:initPageName/view-source', - component: page(() => import('./pages/page-editor/page-editor.vue')), -}, { - path: '/@:username/pages/:pageName', - component: page(() => import('./pages/page.vue')), -}, { - path: '/@:acct/following', - component: page(() => import('./pages/user/following.vue')), -}, { - path: '/@:acct/followers', - component: page(() => import('./pages/user/followers.vue')), -}, { - name: 'user', - path: '/@:acct/:page?', - component: page(() => import('./pages/user/index.vue')), -}, { - name: 'note', - path: '/notes/:noteId', - component: page(() => import('./pages/note.vue')), -}, { - name: 'list', - path: '/list/:listId', - component: page(() => import('./pages/list.vue')), -}, { - path: '/clips/:clipId', - component: page(() => import('./pages/clip.vue')), -}, { - path: '/instance-info/:host', - component: page(() => import('./pages/instance-info.vue')), -}, { - name: 'settings', - path: '/settings', - component: page(() => import('./pages/settings/index.vue')), - loginRequired: true, - children: [{ - path: '/profile', - name: 'profile', - component: page(() => import('./pages/settings/profile.vue')), - }, { - path: '/avatar-decoration', - name: 'avatarDecoration', - component: page(() => import('./pages/settings/avatar-decoration.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('./pages/settings/roles.vue')), - }, { - path: '/privacy', - name: 'privacy', - component: page(() => import('./pages/settings/privacy.vue')), - }, { - path: '/emoji-picker', - name: 'emojiPicker', - component: page(() => import('./pages/settings/emoji-picker.vue')), - }, { - path: '/drive', - name: 'drive', - component: page(() => import('./pages/settings/drive.vue')), - }, { - path: '/drive/cleaner', - name: 'drive', - component: page(() => import('./pages/settings/drive-cleaner.vue')), - }, { - path: '/notifications', - name: 'notifications', - component: page(() => import('./pages/settings/notifications.vue')), - }, { - path: '/email', - name: 'email', - component: page(() => import('./pages/settings/email.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/settings/security.vue')), - }, { - path: '/general', - name: 'general', - component: page(() => import('./pages/settings/general.vue')), - }, { - path: '/theme/install', - name: 'theme', - component: page(() => import('./pages/settings/theme.install.vue')), - }, { - path: '/theme/manage', - name: 'theme', - component: page(() => import('./pages/settings/theme.manage.vue')), - }, { - path: '/theme', - name: 'theme', - component: page(() => import('./pages/settings/theme.vue')), - }, { - path: '/navbar', - name: 'navbar', - component: page(() => import('./pages/settings/navbar.vue')), - }, { - path: '/statusbar', - name: 'statusbar', - component: page(() => import('./pages/settings/statusbar.vue')), - }, { - path: '/sounds', - name: 'sounds', - component: page(() => import('./pages/settings/sounds.vue')), - }, { - path: '/plugin/install', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.install.vue')), - }, { - path: '/plugin', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.vue')), - }, { - path: '/import-export', - name: 'import-export', - component: page(() => import('./pages/settings/import-export.vue')), - }, { - path: '/mute-block', - name: 'mute-block', - component: page(() => import('./pages/settings/mute-block.vue')), - }, { - path: '/api', - name: 'api', - component: page(() => import('./pages/settings/api.vue')), - }, { - path: '/apps', - name: 'api', - component: page(() => import('./pages/settings/apps.vue')), - }, { - path: '/webhook/edit/:webhookId', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.edit.vue')), - }, { - path: '/webhook/new', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.new.vue')), - }, { - path: '/webhook', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.vue')), - }, { - path: '/deck', - name: 'deck', - component: page(() => import('./pages/settings/deck.vue')), - }, { - path: '/preferences-backups', - name: 'preferences-backups', - component: page(() => import('./pages/settings/preferences-backups.vue')), - }, { - path: '/migration', - name: 'migration', - component: page(() => import('./pages/settings/migration.vue')), - }, { - path: '/custom-css', - name: 'general', - component: page(() => import('./pages/settings/custom-css.vue')), - }, { - path: '/accounts', - name: 'profile', - component: page(() => import('./pages/settings/accounts.vue')), - }, { - path: '/other', - name: 'other', - component: page(() => import('./pages/settings/other.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/reset-password/:token?', - component: page(() => import('./pages/reset-password.vue')), -}, { - path: '/signup-complete/:code', - component: page(() => import('./pages/signup-complete.vue')), -}, { - path: '/announcements', - component: page(() => import('./pages/announcements.vue')), -}, { - path: '/about', - component: page(() => import('./pages/about.vue')), - hash: 'initialTab', -}, { - path: '/about-misskey', - component: page(() => import('./pages/about-misskey.vue')), -}, { - path: '/invite', - name: 'invite', - component: page(() => import('./pages/invite.vue')), -}, { - path: '/ads', - component: page(() => import('./pages/ads.vue')), -}, { - path: '/theme-editor', - component: page(() => import('./pages/theme-editor.vue')), - loginRequired: true, -}, { - path: '/roles/:role', - component: page(() => import('./pages/role.vue')), -}, { - path: '/user-tags/:tag', - component: page(() => import('./pages/user-tag.vue')), -}, { - path: '/explore', - component: page(() => import('./pages/explore.vue')), - hash: 'initialTab', -}, { - path: '/search', - component: page(() => import('./pages/search.vue')), - query: { - q: 'query', - channel: 'channel', - type: 'type', - origin: 'origin', - }, -}, { - path: '/authorize-follow', - component: page(() => import('./pages/follow.vue')), - loginRequired: true, -}, { - path: '/share', - component: page(() => import('./pages/share.vue')), - loginRequired: true, -}, { - path: '/api-console', - component: page(() => import('./pages/api-console.vue')), - loginRequired: true, -}, { - path: '/scratchpad', - component: page(() => import('./pages/scratchpad.vue')), -}, { - path: '/auth/:token', - component: page(() => import('./pages/auth.vue')), -}, { - path: '/miauth/:session', - component: page(() => import('./pages/miauth.vue')), - query: { - callback: 'callback', - name: 'name', - icon: 'icon', - permission: 'permission', - }, -}, { - path: '/oauth/authorize', - component: page(() => import('./pages/oauth.vue')), -}, { - path: '/tags/:tag', - component: page(() => import('./pages/tag.vue')), -}, { - path: '/pages/new', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages/edit/:initPageId', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages', - component: page(() => import('./pages/pages.vue')), -}, { - path: '/play/:id/edit', - component: page(() => import('./pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/new', - component: page(() => import('./pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/:id', - component: page(() => import('./pages/flash/flash.vue')), -}, { - path: '/play', - component: page(() => import('./pages/flash/flash-index.vue')), -}, { - path: '/gallery/:postId/edit', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/new', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/:postId', - component: page(() => import('./pages/gallery/post.vue')), -}, { - path: '/gallery', - component: page(() => import('./pages/gallery/index.vue')), -}, { - path: '/channels/:channelId/edit', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/new', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/:channelId', - component: page(() => import('./pages/channel.vue')), -}, { - path: '/channels', - component: page(() => import('./pages/channels.vue')), -}, { - path: '/custom-emojis-manager', - component: page(() => import('./pages/custom-emojis-manager.vue')), -}, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('./pages/avatar-decorations.vue')), -}, { - path: '/registry/keys/:domain/:path(*)?', - component: page(() => import('./pages/registry.keys.vue')), -}, { - path: '/registry/value/:domain/:path(*)?', - component: page(() => import('./pages/registry.value.vue')), -}, { - path: '/registry', - component: page(() => import('./pages/registry.vue')), -}, { - path: '/install-extentions', - component: page(() => import('./pages/install-extentions.vue')), - loginRequired: true, -}, { - path: '/admin/user/:userId', - component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')), -}, { - path: '/admin/file/:fileId', - component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), -}, { - path: '/admin', - component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')), - children: [{ - path: '/overview', - name: 'overview', - component: page(() => import('./pages/admin/overview.vue')), - }, { - path: '/users', - name: 'users', - component: page(() => import('./pages/admin/users.vue')), - }, { - path: '/emojis', - name: 'emojis', - component: page(() => import('./pages/custom-emojis-manager.vue')), - }, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('./pages/avatar-decorations.vue')), - }, { - path: '/queue', - name: 'queue', - component: page(() => import('./pages/admin/queue.vue')), - }, { - path: '/files', - name: 'files', - component: page(() => import('./pages/admin/files.vue')), - }, { - path: '/federation', - name: 'federation', - component: page(() => import('./pages/admin/federation.vue')), - }, { - path: '/announcements', - name: 'announcements', - component: page(() => import('./pages/admin/announcements.vue')), - }, { - path: '/ads', - name: 'ads', - component: page(() => import('./pages/admin/ads.vue')), - }, { - path: '/roles/:id/edit', - name: 'roles', - component: page(() => import('./pages/admin/roles.edit.vue')), - }, { - path: '/roles/new', - name: 'roles', - component: page(() => import('./pages/admin/roles.edit.vue')), - }, { - path: '/roles/:id', - name: 'roles', - component: page(() => import('./pages/admin/roles.role.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('./pages/admin/roles.vue')), - }, { - path: '/database', - name: 'database', - component: page(() => import('./pages/admin/database.vue')), - }, { - path: '/abuses', - name: 'abuses', - component: page(() => import('./pages/admin/abuses.vue')), - }, { - path: '/modlog', - name: 'modlog', - component: page(() => import('./pages/admin/modlog.vue')), - }, { - path: '/settings', - name: 'settings', - component: page(() => import('./pages/admin/settings.vue')), - }, { - path: '/branding', - name: 'branding', - component: page(() => import('./pages/admin/branding.vue')), - }, { - path: '/moderation', - name: 'moderation', - component: page(() => import('./pages/admin/moderation.vue')), - }, { - path: '/email-settings', - name: 'email-settings', - component: page(() => import('./pages/admin/email-settings.vue')), - }, { - path: '/object-storage', - name: 'object-storage', - component: page(() => import('./pages/admin/object-storage.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/admin/security.vue')), - }, { - path: '/relays', - name: 'relays', - component: page(() => import('./pages/admin/relays.vue')), - }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('./pages/admin/instance-block.vue')), - }, { - path: '/proxy-account', - name: 'proxy-account', - component: page(() => import('./pages/admin/proxy-account.vue')), - }, { - path: '/external-services', - name: 'external-services', - component: page(() => import('./pages/admin/external-services.vue')), - }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('./pages/admin/other-settings.vue')), - }, { - path: '/server-rules', - name: 'server-rules', - component: page(() => import('./pages/admin/server-rules.vue')), - }, { - path: '/invites', - name: 'invites', - component: page(() => import('./pages/admin/invites.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/my/notifications', - component: page(() => import('./pages/notifications.vue')), - loginRequired: true, -}, { - path: '/my/favorites', - component: page(() => import('./pages/favorites.vue')), - loginRequired: true, -}, { - path: '/my/achievements', - component: page(() => import('./pages/achievements.vue')), - loginRequired: true, -}, { - path: '/my/drive/folder/:folder', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive/file/:fileId', - component: page(() => import('./pages/drive.file.vue')), - loginRequired: true, -}, { - path: '/my/follow-requests', - component: page(() => import('./pages/follow-requests.vue')), - loginRequired: true, -}, { - path: '/my/lists/:listId', - component: page(() => import('./pages/my-lists/list.vue')), - loginRequired: true, -}, { - path: '/my/lists', - component: page(() => import('./pages/my-lists/index.vue')), - loginRequired: true, -}, { - path: '/my/clips', - component: page(() => import('./pages/my-clips/index.vue')), - loginRequired: true, -}, { - path: '/my/antennas/create', - component: page(() => import('./pages/my-antennas/create.vue')), - loginRequired: true, -}, { - path: '/my/antennas/:antennaId', - component: page(() => import('./pages/my-antennas/edit.vue')), - loginRequired: true, -}, { - path: '/my/antennas', - component: page(() => import('./pages/my-antennas/index.vue')), - loginRequired: true, -}, { - path: '/timeline/list/:listId', - component: page(() => import('./pages/user-list-timeline.vue')), - loginRequired: true, -}, { - path: '/timeline/antenna/:antennaId', - component: page(() => import('./pages/antenna-timeline.vue')), - loginRequired: true, -}, { - path: '/clicker', - component: page(() => import('./pages/clicker.vue')), - loginRequired: true, -}, { - path: '/timeline', - component: page(() => import('./pages/timeline.vue')), -}, { - name: 'index', - path: '/', - component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')), - globalCacheKey: 'index', -}, { - path: '/:(*)', - component: page(() => import('./pages/not-found.vue')), -}]; - -export const mainRouter = new Router(routes, location.pathname + location.search + location.hash, !!$i, page(() => import('@/pages/not-found.vue'))); - -window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); - -mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); -}); - -window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); -}); - -export function useRouter(): Router { - return inject<Router | null>('router', null) ?? mainRouter; -} diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index 4b6b044d8b..67d997f09b 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', + 'bubbleGameExplodingHead', + 'bubbleGameDoubleExplodingHead', ] as const; export const ACHIEVEMENT_BADGES = { @@ -466,6 +468,16 @@ export const ACHIEVEMENT_BADGES = { bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', frame: 'bronze', }, + 'bubbleGameExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'bronze', + }, + 'bubbleGameDoubleExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'silver', + }, /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107> } as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], { img: string; diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 08ba1e6d9b..215ac4cc69 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -218,7 +218,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't }; } -function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> { +function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> { utils.assertObject(def); const text = def.value.get('text'); @@ -241,7 +241,7 @@ function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'typ color: color?.value, font: font?.value, onClickEv: (evId: string) => { - if (onClickEv) call(onClickEv, values.STR(evId)); + if (onClickEv) call(onClickEv, [values.STR(evId)]); }, }; } diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts new file mode 100644 index 0000000000..b6e735ddf2 --- /dev/null +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -0,0 +1,400 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Matter from 'matter-js'; +import * as sound from '@/scripts/sound.js'; + +export type Mono = { + id: string; + level: number; + size: number; + shape: 'circle' | 'rectangle'; + score: number; + dropCandidate: boolean; + sfxPitch: number; + img: string; + imgSize: number; + spriteScale: number; +}; + +const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる + +export class DropAndFusionGame extends EventEmitter<{ + changeScore: (newScore: number) => void; + changeCombo: (newCombo: number) => void; + changeStock: (newStock: { id: string; mono: Mono }[]) => void; + dropped: () => void; + fusioned: (x: number, y: number, scoreDelta: number) => void; + monoAdded: (mono: Mono) => void; + gameOver: () => void; +}> { + private COMBO_INTERVAL = 1000; + public readonly DROP_INTERVAL = 500; + public readonly PLAYAREA_MARGIN = 25; + private STOCK_MAX = 4; + private loaded = false; + private engine: Matter.Engine; + private render: Matter.Render; + private runner: Matter.Runner; + private overflowCollider: Matter.Body; + private isGameOver = false; + + private gameWidth: number; + private gameHeight: number; + private monoDefinitions: Mono[] = []; + private monoTextures: Record<string, Blob> = {}; + private monoTextureUrls: Record<string, string> = {}; + + /** + * フィールドに出ていて、かつ合体の対象となるアイテム + */ + private activeBodyIds: Matter.Body['id'][] = []; + + private latestDroppedBodyId: Matter.Body['id'] | null = null; + + private latestDroppedAt = 0; + private latestFusionedAt = 0; + private stock: { id: string; mono: Mono }[] = []; + + private _combo = 0; + private get combo() { + return this._combo; + } + private set combo(value: number) { + this._combo = value; + this.emit('changeCombo', value); + } + + private _score = 0; + private get score() { + return this._score; + } + private set score(value: number) { + this._score = value; + this.emit('changeScore', value); + } + + private comboIntervalId: number | null = null; + + constructor(opts: { + canvas: HTMLCanvasElement; + width: number; + height: number; + monoDefinitions: Mono[]; + }) { + super(); + + this.gameWidth = opts.width; + this.gameHeight = opts.height; + this.monoDefinitions = opts.monoDefinitions; + + this.engine = Matter.Engine.create({ + constraintIterations: 2 * PHYSICS_QUALITY_FACTOR, + positionIterations: 6 * PHYSICS_QUALITY_FACTOR, + velocityIterations: 4 * PHYSICS_QUALITY_FACTOR, + gravity: { + x: 0, + y: 1, + }, + timing: { + timeScale: 2, + }, + enableSleeping: false, + }); + + this.render = Matter.Render.create({ + engine: this.engine, + canvas: opts.canvas, + options: { + width: this.gameWidth, + height: this.gameHeight, + background: 'transparent', // transparent to hide + wireframeBackground: 'transparent', // transparent to hide + wireframes: false, + showSleeping: false, + pixelRatio: Math.max(2, window.devicePixelRatio), + }, + }); + + Matter.Render.run(this.render); + + this.runner = Matter.Runner.create(); + Matter.Runner.run(this.runner, this.engine); + + this.engine.world.bodies = []; + + //#region walls + const WALL_OPTIONS: Matter.IChamferableBodyDefinition = { + isStatic: true, + friction: 0.7, + slop: 1.0, + render: { + strokeStyle: 'transparent', + fillStyle: 'transparent', + }, + }; + + const thickness = 100; + Matter.Composite.add(this.engine.world, [ + Matter.Bodies.rectangle(this.gameWidth / 2, this.gameHeight + (thickness / 2) - this.PLAYAREA_MARGIN, this.gameWidth, thickness, WALL_OPTIONS), + Matter.Bodies.rectangle(this.gameWidth + (thickness / 2) - this.PLAYAREA_MARGIN, this.gameHeight / 2, thickness, this.gameHeight, WALL_OPTIONS), + Matter.Bodies.rectangle(-((thickness / 2) - this.PLAYAREA_MARGIN), this.gameHeight / 2, thickness, this.gameHeight, WALL_OPTIONS), + ]); + //#endregion + + this.overflowCollider = Matter.Bodies.rectangle(this.gameWidth / 2, 0, this.gameWidth, 200, { + isStatic: true, + isSensor: true, + render: { + strokeStyle: 'transparent', + fillStyle: 'transparent', + }, + }); + Matter.Composite.add(this.engine.world, this.overflowCollider); + + // fit the render viewport to the scene + Matter.Render.lookAt(this.render, { + min: { x: 0, y: 0 }, + max: { x: this.gameWidth, y: this.gameHeight }, + }); + } + + private createBody(mono: Mono, x: number, y: number) { + const options: Matter.IBodyDefinition = { + label: mono.id, + //density: 0.0005, + density: mono.size / 1000, + restitution: 0.2, + frictionAir: 0.01, + friction: 0.7, + frictionStatic: 5, + slop: 1.0, + //mass: 0, + render: { + sprite: { + texture: mono.img, + xScale: (mono.size / mono.imgSize) * mono.spriteScale, + yScale: (mono.size / mono.imgSize) * mono.spriteScale, + }, + }, + }; + if (mono.shape === 'circle') { + return Matter.Bodies.circle(x, y, mono.size / 2, options); + } else if (mono.shape === 'rectangle') { + return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options); + } else { + throw new Error('unrecognized shape'); + } + } + + private fusion(bodyA: Matter.Body, bodyB: Matter.Body) { + const now = Date.now(); + if (this.latestFusionedAt > now - this.COMBO_INTERVAL) { + this.combo++; + } else { + this.combo = 1; + } + this.latestFusionedAt = now; + + // TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する? + const newX = (bodyA.position.x + bodyB.position.x) / 2; + const newY = (bodyA.position.y + bodyB.position.y) / 2; + + Matter.Composite.remove(this.engine.world, [bodyA, bodyB]); + this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); + + const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!; + const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1); + + if (nextMono) { + const body = this.createBody(nextMono, newX, newY); + Matter.Composite.add(this.engine.world, body); + + // 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする + window.setTimeout(() => { + this.activeBodyIds.push(body.id); + }, 100); + + const comboBonus = 1 + ((this.combo - 1) / 5); + const additionalScore = Math.round(currentMono.score * comboBonus); + this.score += additionalScore; + + // TODO: 効果音再生はコンポーネント側の責務なので移動する + const pan = ((newX / this.gameWidth) - 0.5) * 2; + sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', 1, pan, nextMono.sfxPitch); + + this.emit('monoAdded', nextMono); + this.emit('fusioned', newX, newY, additionalScore); + } else { + //const VELOCITY = 30; + //for (let i = 0; i < 10; i++) { + // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2))); + // Matter.Composite.add(world, body); + // bodies.push(body); + //} + //sound.playUrl({ + // type: 'syuilo/bubble2', + // volume: 1, + //}); + } + } + + private gameOver() { + this.isGameOver = true; + Matter.Runner.stop(this.runner); + this.emit('gameOver'); + } + + /** テクスチャをすべてキャッシュする */ + private async loadMonoTextures() { + async function loadSingleMonoTexture(mono: Mono, game: DropAndFusionGame) { + // Matter-js内にキャッシュがある場合はスキップ + if (game.render.textures[mono.img]) return; + console.log('loading', mono.img); + + let src = mono.img; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (game.monoTextureUrls[mono.img]) { + src = game.monoTextureUrls[mono.img]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (game.monoTextures[mono.img]) { + src = URL.createObjectURL(game.monoTextures[mono.img]); + game.monoTextureUrls[mono.img] = src; + } else { + const res = await fetch(mono.img); + const blob = await res.blob(); + game.monoTextures[mono.img] = blob; + src = URL.createObjectURL(blob); + game.monoTextureUrls[mono.img] = src; + } + + const image = new Image(); + image.src = src; + game.render.textures[mono.img] = image; + } + + return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this))); + } + + public start() { + if (!this.loaded) throw new Error('game is not loaded yet'); + + for (let i = 0; i < this.STOCK_MAX; i++) { + this.stock.push({ + id: Math.random().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + } + this.emit('changeStock', this.stock); + + // TODO: fusion予約状態のアイテムは光らせるなどの演出をすると楽しそう + let fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = []; + + const minCollisionEnergyForSound = 2.5; + const maxCollisionEnergyForSound = 9; + const soundPitchMax = 4; + const soundPitchMin = 0.5; + + Matter.Events.on(this.engine, 'collisionStart', (event) => { + for (const pairs of event.pairs) { + const { bodyA, bodyB } = pairs; + if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) { + if (bodyA.id === this.latestDroppedBodyId || bodyB.id === this.latestDroppedBodyId) { + continue; + } + this.gameOver(); + break; + } + const shouldFusion = (bodyA.label === bodyB.label) && !fusionReservedPairs.some(x => x.bodyA.id === bodyA.id || x.bodyA.id === bodyB.id || x.bodyB.id === bodyA.id || x.bodyB.id === bodyB.id); + if (shouldFusion) { + if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) { + this.fusion(bodyA, bodyB); + } else { + fusionReservedPairs.push({ bodyA, bodyB }); + window.setTimeout(() => { + fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); + this.fusion(bodyA, bodyB); + }, 100); + } + } else { + const energy = pairs.collision.depth; + if (energy > minCollisionEnergyForSound) { + // TODO: 効果音再生はコンポーネント側の責務なので移動する + const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4; + const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / this.gameWidth) - 0.5) * 2; + const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10))); + sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', vol, pan, pitch); + } + } + } + }); + + this.comboIntervalId = window.setInterval(() => { + if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) { + this.combo = 0; + } + }, 500); + } + + public async load() { + await this.loadMonoTextures(); + this.loaded = true; + } + + public getTextureImageUrl(mono: Mono) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.monoTextureUrls[mono.img]) { + return this.monoTextureUrls[mono.img]; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (this.monoTextures[mono.img]) { + // Gameクラス内にキャッシュがある場合はそれを使う + const out = URL.createObjectURL(this.monoTextures[mono.img]); + this.monoTextureUrls[mono.img] = out; + return out; + } else { + return mono.img; + } + } + + public getActiveMonos() { + return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined); + } + + public drop(_x: number) { + if (this.isGameOver) return; + if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) { + return; + } + const st = this.stock.shift()!; + this.stock.push({ + id: Math.random().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + this.emit('changeStock', this.stock); + + const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x)); + const body = this.createBody(st.mono, x, 50 + st.mono.size / 2); + Matter.Composite.add(this.engine.world, body); + this.activeBodyIds.push(body.id); + this.latestDroppedBodyId = body.id; + this.latestDroppedAt = Date.now(); + this.emit('dropped'); + this.emit('monoAdded', st.mono); + + // TODO: 効果音再生はコンポーネント側の責務なので移動する + const pan = ((x / this.gameWidth) - 0.5) * 2; + sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', 1, pan); + } + + public dispose() { + if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); + Matter.Render.stop(this.render); + Matter.Runner.stop(this.runner); + Matter.World.clear(this.engine.world, false); + Matter.Engine.clear(this.engine); + } +} diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts index 8885bf4b7f..4bd8bf94be 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) { export const emojiCharByCategory = _charGroupByCategory; export function getEmojiName(char: string): string | null { - const idx = _indexByChar.get(char); + // Colorize it because emojilist.json assumes that + const idx = _indexByChar.get(colorizeEmoji(char)); if (idx == null) { return null; } else { @@ -44,6 +45,10 @@ export function getEmojiName(char: string): string | null { } } +export function colorizeEmoji(char: string) { + return char.length === 1 ? `${char}\uFE0F` : char; +} + export interface CustomEmojiFolderTree { value: string; category: string; diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 2735253b36..d9a52c3741 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -13,11 +13,11 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; -import { mainRouter } from '@/router.js'; -import { Router } from '@/nirax.js'; +import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; +import { mainRouter } from '@/global/router/main.js'; -export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) { +export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; const cleanups = [] as (() => void)[]; diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index ff438af24f..ddcfd8852e 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -6,8 +6,8 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { Router } from '@/nirax.js'; +import { mainRouter } from '@/global/router/main.js'; export async function lookup(router?: Router) { const _router = router ?? mainRouter; diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index e71c5dd592..337fa15113 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -10,12 +10,17 @@ import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); // Implements Misskey.api.ApiClient.request -export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( +export function misskeyApi< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal, -): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { +): Promise<_ResT> { if (endpoint.includes('://')) throw new Error('invalid endpoint'); pendingApiRequestsCount.value++; @@ -23,7 +28,7 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey. pendingApiRequestsCount.value--; }; - const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Append a credential if ($i) (data as any).i = $i.token; if (token !== undefined) (data as any).i = token; @@ -44,7 +49,7 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey. if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined as _ResT); // void -> undefined } else { reject(body.error); } @@ -57,10 +62,15 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey. } // Implements Misskey.api.ApiClient.request -export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( +export function misskeyApiGet< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( endpoint: E, data: P = {} as any, -): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { +): Promise<_ResT> { pendingApiRequestsCount.value++; const onFinally = () => { @@ -69,7 +79,7 @@ export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Missk const query = new URLSearchParams(data as any); - const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Send request window.fetch(`${apiUrl}/${endpoint}?${query}`, { method: 'GET', @@ -81,7 +91,7 @@ export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Missk if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined as _ResT); // void -> undefined } else { reject(body.error); } diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 0b966ff199..690c342c85 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -5,7 +5,6 @@ import type { SoundStore } from '@/store.js'; import { defaultStore } from '@/store.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; let ctx: AudioContext; const cache = new Map<string, AudioBuffer>(); @@ -89,63 +88,35 @@ export type OperationType = typeof operationTypes[number]; /** * 音声を読み込む - * @param soundStore サウンド設定 + * @param url url * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする */ -export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) { +export async function loadAudio(url: string, options?: { useCache?: boolean; }) { if (_DEV_) console.log('loading audio. opts:', options); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { - return; - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ctx == null) { ctx = new AudioContext(); } if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) { + if (cache.has(url)) { if (_DEV_) console.log('use cache'); - return cache.get(soundStore.fileId) as AudioBuffer; - } else if (cache.has(soundStore.type)) { - if (_DEV_) console.log('use cache'); - return cache.get(soundStore.type) as AudioBuffer; + return cache.get(url) as AudioBuffer; } } let response: Response; - if (soundStore.type === '_driveFile_') { - try { - response = await fetch(soundStore.fileUrl); - } catch (err) { - try { - // URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック - const apiRes = await misskeyApi('drive/files/show', { - fileId: soundStore.fileId, - }); - response = await fetch(apiRes.url); - } catch (fbErr) { - // それでも無理なら諦める - return; - } - } - } else { - try { - response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`); - } catch (err) { - return; - } + try { + response = await fetch(url); + } catch (err) { + return; } const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_') { - cache.set(soundStore.fileId, audioBuffer); - } else { - cache.set(soundStore.type, audioBuffer); - } + cache.set(url, audioBuffer); } return audioBuffer; @@ -174,25 +145,46 @@ export function play(operationType: OperationType) { * @param soundStore サウンド設定 */ export async function playFile(soundStore: SoundStore) { - const buffer = await loadAudio(soundStore); + if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { + return; + } + const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; + const buffer = await loadAudio(url); if (!buffer) return; - createSourceNode(buffer, soundStore.volume)?.start(); + createSourceNode(buffer, soundStore.volume)?.soundSource.start(); } -export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { +export async function playUrl(url: string, volume = 1, pan = 0, playbackRate = 1) { + const buffer = await loadAudio(url); + if (!buffer) return; + createSourceNode(buffer, volume, pan, playbackRate)?.soundSource.start(); +} + +export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1): { + soundSource: AudioBufferSourceNode; + panNode: StereoPannerNode; + gainNode: GainNode; +} | null { const masterVolume = defaultStore.state.sound_masterVolume; if (isMute() || masterVolume === 0 || volume === 0) { return null; } + const panNode = ctx.createStereoPanner(); + panNode.pan.value = pan; + const gainNode = ctx.createGain(); gainNode.gain.value = masterVolume * volume; const soundSource = ctx.createBufferSource(); soundSource.buffer = buffer; - soundSource.connect(gainNode).connect(ctx.destination); + soundSource.playbackRate.value = playbackRate; + soundSource + .connect(panNode) + .connect(gainNode) + .connect(ctx.destination); - return soundSource; + return { soundSource, panNode, gainNode }; } /** diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index b970ff1df4..9930b321f7 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -27,6 +27,11 @@ function toolsMenuItems(): MenuItem[] { to: '/clicker', text: '🍪👈', icon: 'ti ti-cookie', + }, { + type: 'link', + to: '/bubble-game', + text: i18n.ts.bubbleGame, + icon: 'ti ti-apple', }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? { type: 'link', to: '/custom-emojis-manager', diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts index 504484f8de..4c77465eb1 100644 --- a/packages/frontend/src/ui/_common_/sw-inject.ts +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -7,8 +7,8 @@ import { post } from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i, login } from '@/account.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js'; -import { mainRouter } from '@/router.js'; import { deepClone } from '@/scripts/clone.js'; +import { mainRouter } from '@/global/router/main.js'; export function swInject() { navigator.serviceWorker.addEventListener('message', async ev => { diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index e0985fdb11..fdddc0bb69 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -52,11 +52,11 @@ import XCommon from './_common_/common.vue'; import { instanceName } from '@/config.js'; import { StickySidebar } from '@/scripts/sticky-sidebar.js'; import * as os from '@/os.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; +import { mainRouter } from '@/global/router/main.js'; const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index d184764b82..304ebbf0b2 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -103,7 +103,6 @@ import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { defaultStore } from '@/store.js'; @@ -117,6 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue'; import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; +import { mainRouter } from '@/global/router/main.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index c2b8f19079..674132e0d7 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -24,10 +24,10 @@ import XColumn from './column.vue'; import { deckStore, Column } from '@/ui/deck/deck-store.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; import { getScrollContainer } from '@/scripts/scroll.js'; +import { mainRouter } from '@/global/router/main.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index f32f2de3df..b0a2aa35f9 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { provide, ComputedRef, ref } from 'vue'; import XCommon from './_common_/common.vue'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { instanceName } from '@/config.js'; +import { mainRouter } from '@/global/router/main.js'; const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index f46f55d988..6f13f3fe87 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -105,12 +105,12 @@ import { defaultStore } from '@/store.js'; import { navbarItemDef } from '@/navbar.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; import { CURRENT_STICKY_BOTTOM } from '@/const.js'; import { useScrollPositionManager } from '@/nirax.js'; +import { mainRouter } from '@/global/router/main.js'; const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 5af6bc30a8..d97c786d4a 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -79,10 +79,10 @@ import { instance } from '@/instance.js'; import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; +import { mainRouter } from '@/global/router/main.js'; const DESKTOP_THRESHOLD = 1100; diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index b819b6ca0a..957044c52b 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { provide, ComputedRef, ref } from 'vue'; import XCommon from './_common_/common.vue'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { instanceName, ui } from '@/config.js'; import { i18n } from '@/i18n.js'; +import { mainRouter } from '@/global/router/main.js'; const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 91983d8474..89ad3bf323 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -104,10 +104,7 @@ const jammedAudioBuffer = ref<AudioBuffer | null>(null); const jammedSoundNodePlaying = ref<boolean>(false); if (defaultStore.state.sound_masterVolume) { - sound.loadAudio({ - type: 'syuilo/queue-jammed', - volume: 1, - }).then(buf => { + sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => { if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer'); jammedAudioBuffer.value = buf; }); @@ -126,7 +123,7 @@ const onStats = (stats) => { current[domain].delayed = stats[domain].delayed; if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) { - const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1); + const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1)?.soundSource; if (soundNode) { jammedSoundNodePlaying.value = true; soundNode.onended = () => jammedSoundNodePlaying.value = false; diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index f13b6a370d..ee720bd9d7 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -80,13 +80,13 @@ import * as Misskey from 'misskey-js'; import { v4 as uuid } from 'uuid'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); const viewBoxX = ref<number>(50); const viewBoxY = ref<number>(30); -const stats = ref<any[]>([]); +const stats = ref<Misskey.entities.ServerStats[]>([]); const cpuGradientId = uuid(); const cpuMaskId = uuid(); const memGradientId = uuid(); @@ -107,6 +107,7 @@ onMounted(() => { props.connection.on('statsLog', onStatsLog); props.connection.send('requestLog', { id: Math.random().toString().substring(2, 10), + length: 50, }); }); @@ -115,7 +116,7 @@ onBeforeUnmount(() => { props.connection.off('statsLog', onStatsLog); }); -function onStats(connStats) { +function onStats(connStats: Misskey.entities.ServerStats) { stats.value.push(connStats); if (stats.value.length > 50) stats.value.shift(); @@ -136,8 +137,8 @@ function onStats(connStats) { memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); } -function onStatsLog(statsLog) { - for (const revStats of [...statsLog].reverse()) { +function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { + for (const revStats of statsLog.reverse()) { onStats(revStats); } } diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue index c7fd0e9023..3778c4318e 100644 --- a/packages/frontend/src/widgets/server-metric/cpu.vue +++ b/packages/frontend/src/widgets/server-metric/cpu.vue @@ -20,13 +20,13 @@ import * as Misskey from 'misskey-js'; import XPie from './pie.vue'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); const usage = ref<number>(0); -function onStats(stats) { +function onStats(stats: Misskey.entities.ServerStats) { usage.value = stats.cpu; } diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index f5e80b0d21..990590e0d1 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from '../widget.js'; +import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js'; import XCpuMemory from './cpu-mem.vue'; import XNet from './net.vue'; import XCpu from './cpu.vue'; @@ -54,11 +54,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue index f51b2af390..6c9e3efe67 100644 --- a/packages/frontend/src/widgets/server-metric/mem.vue +++ b/packages/frontend/src/widgets/server-metric/mem.vue @@ -22,7 +22,7 @@ import XPie from './pie.vue'; import bytes from '@/filters/bytes.js'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); @@ -31,7 +31,7 @@ const total = ref<number>(0); const used = ref<number>(0); const free = ref<number>(0); -function onStats(stats) { +function onStats(stats: Misskey.entities.ServerStats) { usage.value = stats.mem.active / props.meta.mem.total; total.value = props.meta.mem.total; used.value = stats.mem.active; diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue index 7af88a94eb..d33c2c577d 100644 --- a/packages/frontend/src/widgets/server-metric/net.vue +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -54,13 +54,13 @@ import * as Misskey from 'misskey-js'; import bytes from '@/filters/bytes.js'; const props = defineProps<{ - connection: any, + connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, meta: Misskey.entities.ServerInfoResponse }>(); const viewBoxX = ref<number>(50); const viewBoxY = ref<number>(30); -const stats = ref<any[]>([]); +const stats = ref<Misskey.entities.ServerStats[]>([]); const inPolylinePoints = ref<string>(''); const outPolylinePoints = ref<string>(''); const inPolygonPoints = ref<string>(''); @@ -77,6 +77,7 @@ onMounted(() => { props.connection.on('statsLog', onStatsLog); props.connection.send('requestLog', { id: Math.random().toString().substring(2, 10), + length: 50, }); }); @@ -85,7 +86,7 @@ onBeforeUnmount(() => { props.connection.off('statsLog', onStatsLog); }); -function onStats(connStats) { +function onStats(connStats: Misskey.entities.ServerStats) { stats.value.push(connStats); if (stats.value.length > 50) stats.value.shift(); @@ -109,8 +110,8 @@ function onStats(connStats) { outRecent.value = connStats.net.tx; } -function onStatsLog(statsLog) { - for (const revStats of [...statsLog].reverse()) { +function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { + for (const revStats of statsLog.reverse()) { onStats(revStats); } } diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts new file mode 100644 index 0000000000..a1782a4913 --- /dev/null +++ b/packages/frontend/test/emoji.test.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, test, assert, afterEach } from 'vitest'; +import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import { defaultStoreState } from './init.js'; +import { getEmojiName } from '@/scripts/emojilist.js'; +import { components } from '@/components/index.js'; +import { directives } from '@/directives/index.js'; +import MkEmoji from '@/components/global/MkEmoji.vue'; + +describe('Emoji', () => { + const renderEmoji = (emoji: string): RenderResult => { + return render(MkEmoji, { + props: { emoji }, + global: { directives, components }, + }); + }; + + afterEach(() => { + cleanup(); + defaultStoreState.emojiStyle = ''; + }); + + describe('MkEmoji', () => { + test('Should render selector-less heart with color in native mode', async () => { + defaultStoreState.emojiStyle = 'native'; + const mkEmoji = await renderEmoji('\u2764'); // monochrome heart + assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart + assert.ok(!mkEmoji.queryByText('\u2764')); + }); + }); + + describe('Emoji list', () => { + test('Should get the name of the heart', () => { + assert.strictEqual(getEmojiName('\u2764'), 'heart'); + }); + }); +}); diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts index 6d93ff8cb0..f21248cfee 100644 --- a/packages/frontend/test/init.ts +++ b/packages/frontend/test/init.ts @@ -17,21 +17,23 @@ updateI18n(locales['en-US']); // XXX: misskey-js panics if WebSocket is not defined vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; }); +export const defaultStoreState: Record<string, unknown> = { + + // なんかtestがうまいこと動かないのでここに書く + dataSaver: { + media: false, + avatar: false, + urlPreview: false, + code: false, + }, + +}; + // XXX: defaultStore somehow becomes undefined in vitest? vi.mock('@/store.js', () => { return { defaultStore: { - state: { - - // なんかtestがうまいこと動かないのでここに書く - dataSaver: { - media: false, - avatar: false, - urlPreview: false, - code: false, - }, - - }, + state: defaultStoreState, }, }; }); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index cd099be1ef..b7f597f0c0 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2558,7 +2558,7 @@ type QueueStats = { }; // @public (undocumented) -type QueueStatsLog = string[]; +type QueueStatsLog = QueueStats[]; // @public (undocumented) type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json']; @@ -2632,7 +2632,7 @@ type ServerStats = { }; // @public (undocumented) -type ServerStatsLog = string[]; +type ServerStatsLog = ServerStats[]; // @public (undocumented) type Signin = components['schemas']['Signin']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index d6a2e712df..86e83448ec 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-02T08:53:57.449Z + * generatedAt: 2024-01-07T15:22:15.630Z */ import type { SwitchCaseResponseType } from '../api.js'; @@ -2255,6 +2255,18 @@ declare module '../api.js' { * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ + request<E extends 'i/export-clips', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* + */ request<E extends 'i/export-favorites', P extends Endpoints[E]['req']>( endpoint: E, params: P, diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 192a1a31e0..cc4d251f4d 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-02T08:53:57.445Z + * generatedAt: 2024-01-07T15:22:15.626Z */ import type { @@ -745,6 +745,7 @@ export type Endpoints = { 'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse }; 'i/export-mute': { req: EmptyRequest; res: EmptyResponse }; 'i/export-notes': { req: EmptyRequest; res: EmptyResponse }; + 'i/export-clips': { req: EmptyRequest; res: EmptyResponse }; 'i/export-favorites': { req: EmptyRequest; res: EmptyResponse }; 'i/export-user-lists': { req: EmptyRequest; res: EmptyResponse }; 'i/export-antennas': { req: EmptyRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index fd4d7372cc..1f3f55b2fd 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-02T08:53:57.443Z + * generatedAt: 2024-01-07T15:22:15.624Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 5c07e963b0..1f49debe67 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-06T10:58:05.668Z + * generatedAt: 2024-01-07T15:22:15.623Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index c48ff1d28f..bcfa5ea01b 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3,7 +3,7 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-02T08:53:56.447Z + * generatedAt: 2024-01-07T15:22:15.494Z */ /** @@ -1966,6 +1966,16 @@ export type paths = { */ post: operations['i/export-notes']; }; + '/i/export-clips': { + /** + * i/export-clips + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* + */ + post: operations['i/export-clips']; + }; '/i/export-favorites': { /** * i/export-favorites @@ -4424,6 +4434,9 @@ export type operations = { emailRequiredForSignup: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; + enableMcaptcha: boolean; + mcaptchaSiteKey: string | null; + mcaptchaInstanceUrl: string | null; enableRecaptcha: boolean; recaptchaSiteKey: string | null; enableTurnstile: boolean; @@ -4449,6 +4462,7 @@ export type operations = { bannedEmailDomains?: string[]; preservedUsernames: string[]; hcaptchaSecretKey: string | null; + mcaptchaSecretKey: string | null; recaptchaSecretKey: string | null; turnstileSecretKey: string | null; sensitiveMediaDetection: string; @@ -8221,6 +8235,10 @@ export type operations = { enableHcaptcha?: boolean; hcaptchaSiteKey?: string | null; hcaptchaSecretKey?: string | null; + enableMcaptcha?: boolean; + mcaptchaSiteKey?: string | null; + mcaptchaInstanceUrl?: string | null; + mcaptchaSecretKey?: string | null; enableRecaptcha?: boolean; recaptchaSiteKey?: string | null; recaptchaSecretKey?: string | null; @@ -15949,7 +15967,7 @@ export type operations = { content: { 'application/json': { /** @enum {string} */ - name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted'; + name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead'; }; }; }; @@ -16311,6 +16329,57 @@ export type operations = { }; }; }; + /** + * i/export-clips + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* + */ + 'i/export-clips': { + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * i/export-favorites * @description No description provided. @@ -18780,6 +18849,9 @@ export type operations = { emailRequiredForSignup: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; + enableMcaptcha: boolean; + mcaptchaSiteKey: string | null; + mcaptchaInstanceUrl: string | null; enableRecaptcha: boolean; recaptchaSiteKey: string | null; enableTurnstile: boolean; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 6314c88e0b..e00e192e0d 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -149,7 +149,7 @@ export type ServerStats = { } }; -export type ServerStatsLog = string[]; +export type ServerStatsLog = ServerStats[]; export type QueueStats = { deliver: { @@ -166,7 +166,7 @@ export type QueueStats = { }; }; -export type QueueStatsLog = string[]; +export type QueueStatsLog = QueueStats[]; export type EmojiAdded = { emoji: EmojiDetailed diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 562c90595e..28cfe3222f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -661,6 +661,9 @@ importers: '@github/webauthn-json': specifier: 2.1.1 version: 2.1.1 + '@mcaptcha/vanilla-glue': + specifier: 0.1.0-alpha-3 + version: 0.1.0-alpha-3 '@misskey-dev/browser-image-resizer': specifier: 2.2.1-misskey.10 version: 2.2.1-misskey.10 @@ -1820,7 +1823,7 @@ packages: '@babel/traverse': 7.22.11 '@babel/types': 7.22.17 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1843,7 +1846,7 @@ packages: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1945,7 +1948,7 @@ packages: '@babel/core': 7.23.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -3345,7 +3348,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.22.17 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3363,7 +3366,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.23.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4242,7 +4245,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.19.0 ignore: 5.2.4 @@ -4259,7 +4262,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.19.0 ignore: 5.2.4 @@ -4524,7 +4527,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4902,6 +4905,16 @@ packages: dev: false optional: true + /@mcaptcha/core-glue@0.1.0-alpha-5: + resolution: {integrity: sha512-16qWm5O5X0Y9LXULULaAks8Vf9FNlUUBcR5KDt49aWhFhG5++JzxNmCwQM9EJSHNU7y0U+FdyAWcGmjfKlkRLA==} + dev: false + + /@mcaptcha/vanilla-glue@0.1.0-alpha-3: + resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==} + dependencies: + '@mcaptcha/core-glue': 0.1.0-alpha-5 + dev: false + /@mdx-js/react@2.3.0(react@18.2.0): resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} peerDependencies: @@ -5084,7 +5097,7 @@ packages: '@open-draft/until': 1.0.3 '@types/debug': 4.1.7 '@xmldom/xmldom': 0.8.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) headers-polyfill: 3.2.5 outvariant: 1.4.0 strict-event-emitter: 0.2.8 @@ -7365,7 +7378,7 @@ packages: hasBin: true peerDependencies: '@swc/core': ^1.2.66 - chokidar: 3.5.3 + chokidar: ^3.5.1 peerDependenciesMeta: chokidar: optional: true @@ -8493,7 +8506,7 @@ packages: '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -8522,7 +8535,7 @@ packages: '@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 graphemer: 1.4.0 ignore: 5.2.4 @@ -8548,7 +8561,7 @@ packages: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 typescript: 5.3.3 transitivePeerDependencies: @@ -8569,7 +8582,7 @@ packages: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 typescript: 5.3.3 transitivePeerDependencies: @@ -8604,7 +8617,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.53.0 ts-api-utils: 1.0.1(typescript@5.3.3) typescript: 5.3.3 @@ -8624,7 +8637,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 ts-api-utils: 1.0.1(typescript@5.3.3) typescript: 5.3.3 @@ -8653,7 +8666,7 @@ packages: dependencies: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -8674,7 +8687,7 @@ packages: dependencies: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -9131,7 +9144,7 @@ packages: engines: {node: '>= 6.0.0'} requiresBuild: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -9139,7 +9152,7 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -9514,7 +9527,7 @@ packages: resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} dependencies: archy: 1.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fastq: 1.15.0 transitivePeerDependencies: - supports-color @@ -10948,7 +10961,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 5.5.0 - dev: true /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -10961,6 +10973,7 @@ packages: dependencies: ms: 2.1.2 supports-color: 8.1.1 + dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -11177,7 +11190,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -11501,7 +11514,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -11840,7 +11853,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -11887,7 +11900,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -12491,7 +12504,7 @@ packages: debug: optional: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -13043,7 +13056,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -13181,7 +13193,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13243,7 +13255,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -13253,7 +13265,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -13262,7 +13274,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13272,7 +13284,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -13422,7 +13434,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -13863,7 +13875,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -14541,7 +14553,7 @@ packages: resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) rfdc: 1.3.0 uri-js: 4.4.1 transitivePeerDependencies: @@ -17109,7 +17121,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -18108,7 +18120,7 @@ packages: dependencies: '@hapi/hoek': 10.0.1 '@hapi/wreck': 18.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) joi: 17.7.0 transitivePeerDependencies: - supports-color @@ -18308,7 +18320,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -18461,7 +18473,7 @@ packages: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -18726,7 +18738,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -19343,7 +19354,7 @@ packages: chalk: 4.1.2 cli-highlight: 2.1.11 date-fns: 2.30.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) dotenv: 16.0.3 glob: 8.1.0 ioredis: 5.3.2 @@ -19701,7 +19712,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 @@ -19813,7 +19824,7 @@ packages: acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.10 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) happy-dom: 10.0.3 local-pkg: 0.4.3 magic-string: 0.30.3 @@ -19895,7 +19906,7 @@ packages: peerDependencies: eslint: '>=6.0.0' dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) eslint: 8.56.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3