diff --git a/CHANGELOG.md b/CHANGELOG.md index 0215e7e73..bdd082a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,10 +23,13 @@ ### Client - Enhance: 絵文字のオートコンプリート機能強化 #12364 - Enhance: ユーザーのRawデータを表示するページが復活 +- Enhance: リアクション選択時に音を鳴らせるように +- Enhance: サウンドにドライブのファイルを使用できるように - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 - Fix: コードエディタが正しく表示されない問題を修正 - Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 +- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正 ### Server - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように @@ -35,9 +38,13 @@ - Fix: api.jsonの生成ロジックを改善 #12402 - Fix: 招待コードが使い回せる問題を修正 - Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正 +- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正 ## 2023.11.1 +### Note +- 悪意のある第三者がリモートユーザーになりすました任意のアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-3f39-6537-3cgc)をご覧ください。 + ### General - Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました - Enhance: ローカリゼーションの更新 @@ -51,6 +58,7 @@ - 例: `$[unixtime 1701356400]` - Enhance: プラグインでエラーが発生した場合のハンドリングを強化 - Enhance: 細かなUIのブラッシュアップ +- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加 - Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339 - Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236 - Fix: プラグインでノートの表示を書き換えられない問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 042c7750e..64ee30410 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -547,6 +547,8 @@ export interface Locale { "popout": string; "volume": string; "masterVolume": string; + "notUseSound": string; + "useSoundOnlyWhenActive": string; "details": string; "chooseEmoji": string; "unableToProcess": string; @@ -1943,6 +1945,15 @@ export interface Locale { "notification": string; "antenna": string; "channel": string; + "reaction": string; + }; + "_soundSettings": { + "driveFile": string; + "driveFileWarn": string; + "driveFileTypeWarn": string; + "driveFileTypeWarnDescription": string; + "driveFileDurationWarn": string; + "driveFileDurationWarnDescription": string; }; "_ago": { "future": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 58e0dd0b1..f4daefa97 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -544,6 +544,8 @@ showInPage: "ページで表示" popout: "ポップアウト" volume: "音量" masterVolume: "マスター音量" +notUseSound: "サウンドを出力しない" +useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する" details: "詳細" chooseEmoji: "絵文字を選択" unableToProcess: "操作を完了できません" @@ -1848,6 +1850,15 @@ _sfx: notification: "通知" antenna: "アンテナ受信" channel: "チャンネル通知" + reaction: "リアクション選択時" + +_soundSettings: + driveFile: "ドライブの音声を使用" + driveFileWarn: "ドライブのファイルを選択してください" + driveFileTypeWarn: "このファイルは対応していません" + driveFileTypeWarnDescription: "音声ファイルを選択してください" + driveFileDurationWarn: "音声が長すぎます" + driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?" _ago: future: "未来" diff --git a/package.json b/package.json index f51861401..fc328a650 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.11.1", + "version": "2023.12.0-beta.1", "codename": "nasubi", "repository": { "type": "git", @@ -52,11 +52,11 @@ "typescript": "5.3.2" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@typescript-eslint/eslint-plugin": "6.12.0", + "@typescript-eslint/parser": "6.12.0", "cross-env": "7.0.3", - "cypress": "13.5.1", - "eslint": "8.53.0", + "cypress": "13.6.0", + "eslint": "8.54.0", "start-server-and-test": "2.0.3" }, "optionalDependencies": { diff --git a/packages/backend/package.json b/packages/backend/package.json index 18c7818dc..b17ce904f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -60,9 +60,9 @@ "dependencies": { "@aws-sdk/client-s3": "3.412.0", "@aws-sdk/lib-storage": "3.412.0", - "@bull-board/api": "5.9.1", - "@bull-board/fastify": "5.9.1", - "@bull-board/ui": "5.9.1", + "@bull-board/api": "5.9.2", + "@bull-board/fastify": "5.9.2", + "@bull-board/ui": "5.9.2", "@discordapp/twemoji": "14.1.2", "@fastify/accepts": "4.2.0", "@fastify/cookie": "9.2.0", @@ -72,15 +72,15 @@ "@fastify/multipart": "8.0.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@nestjs/common": "10.2.8", - "@nestjs/core": "10.2.8", - "@nestjs/testing": "10.2.8", + "@nestjs/common": "10.2.10", + "@nestjs/core": "10.2.10", + "@nestjs/testing": "10.2.10", "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", - "@smithy/node-http-handler": "2.1.5", + "@smithy/node-http-handler": "2.1.10", "@swc/cli": "0.1.63", - "@swc/core": "1.3.96", + "@swc/core": "1.3.99", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "6.0.1", @@ -88,7 +88,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.13.3", + "bullmq": "4.14.2", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -114,11 +114,11 @@ "ipaddr.js": "2.1.0", "is-svg": "5.0.0", "js-yaml": "4.1.0", - "jsdom": "22.1.0", + "jsdom": "23.0.0", "json5": "2.2.3", "jsonld": "8.3.1", - "jsrsasign": "10.8.6", - "meilisearch": "0.35.0", + "jsrsasign": "10.9.0", + "meilisearch": "0.36.0", "mfm-js": "0.23.3", "microformats-parser": "1.5.2", "mime-types": "2.1.35", @@ -145,7 +145,7 @@ "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.20.8", + "re2": "1.20.9", "redis-lock": "0.1.4", "reflect-metadata": "0.1.13", "rename": "1.0.4", @@ -159,7 +159,7 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.21.17", + "systeminformation": "5.21.18", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.8", @@ -178,7 +178,7 @@ "@simplewebauthn/typescript-types": "8.3.4", "@swc/jest": "0.2.29", "@types/accepts": "1.3.7", - "@types/archiver": "6.0.1", + "@types/archiver": "6.0.2", "@types/bcryptjs": "2.4.6", "@types/body-parser": "1.19.5", "@types/cbor": "6.0.0", @@ -186,28 +186,28 @@ "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", "@types/http-link-header": "1.0.5", - "@types/jest": "29.5.8", + "@types/jest": "29.5.10", "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.5", - "@types/jsonld": "1.5.12", + "@types/jsdom": "21.1.6", + "@types/jsonld": "1.5.13", "@types/jsrsasign": "10.5.12", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.9.1", + "@types/node": "20.10.0", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", "@types/oauth2orize": "1.11.3", "@types/oauth2orize-pkce": "0.1.2", "@types/pg": "8.10.9", - "@types/pug": "2.0.9", - "@types/punycode": "2.1.2", + "@types/pug": "2.0.10", + "@types/punycode": "2.1.3", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.9.4", - "@types/semver": "7.5.5", + "@types/sanitize-html": "2.9.5", + "@types/semver": "7.5.6", "@types/sharp": "0.32.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", @@ -215,12 +215,12 @@ "@types/tmp": "0.2.6", "@types/vary": "1.1.3", "@types/web-push": "3.6.3", - "@types/ws": "8.5.9", - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "6.12.0", + "@typescript-eslint/parser": "6.12.0", "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", - "eslint": "8.53.0", + "eslint": "8.54.0", "eslint-plugin-import": "2.29.0", "execa": "8.0.1", "jest": "29.7.0", diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 2815d2473..2c27a0255 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -16,7 +16,7 @@ import type { AntennasRepository, UserListMembershipsRepository } from '@/models import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -39,7 +39,7 @@ export class AntennaService implements OnApplicationShutdown { private utilityService: UtilityService, private globalEventService: GlobalEventService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { this.antennasFetched = false; this.antennas = []; @@ -94,7 +94,7 @@ export class AntennaService implements OnApplicationShutdown { const redisPipeline = this.redisForTimelines.pipeline(); for (const antenna of matchedAntennas) { - this.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); + this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); this.globalEventService.publishAntennaStream(antenna.id, 'note', note); } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 9fb29e0e6..bf6f0ef87 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -62,7 +62,7 @@ import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; import { ClipService } from './ClipService.js'; import { FeaturedService } from './FeaturedService.js'; -import { FunoutTimelineService } from './FunoutTimelineService.js'; +import { FanoutTimelineService } from './FanoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; import { RegistryApiService } from './RegistryApiService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; @@ -194,7 +194,7 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; -const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService }; +const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; @@ -330,7 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SearchService, ClipService, FeaturedService, - FunoutTimelineService, + FanoutTimelineService, ChannelFollowingService, RegistryApiService, ChartLoggerService, @@ -459,7 +459,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SearchService, $ClipService, $FeaturedService, - $FunoutTimelineService, + $FanoutTimelineService, $ChannelFollowingService, $RegistryApiService, $ChartLoggerService, @@ -589,7 +589,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SearchService, ClipService, FeaturedService, - FunoutTimelineService, + FanoutTimelineService, ChannelFollowingService, RegistryApiService, FederationChart, @@ -717,7 +717,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SearchService, $ClipService, $FeaturedService, - $FunoutTimelineService, + $FanoutTimelineService, $ChannelFollowingService, $RegistryApiService, $FederationChart, diff --git a/packages/backend/src/core/FunoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts similarity index 98% rename from packages/backend/src/core/FunoutTimelineService.ts rename to packages/backend/src/core/FanoutTimelineService.ts index c633c329e..6a1b0aa87 100644 --- a/packages/backend/src/core/FunoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; @Injectable() -export class FunoutTimelineService { +export class FanoutTimelineService { constructor( @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts index 9617f8388..507fc464f 100644 --- a/packages/backend/src/core/FeaturedService.ts +++ b/packages/backend/src/core/FeaturedService.ts @@ -5,11 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { MiNote, MiUser } from '@/models/_.js'; +import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと +export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと @@ -79,6 +80,11 @@ export class FeaturedService { return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); } + @bindThis + public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise { + return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score); + } + @bindThis public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise { return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); @@ -99,6 +105,11 @@ export class FeaturedService { return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold); } + @bindThis + public getGalleryPostsRanking(threshold: number): Promise { + return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold); + } + @bindThis public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise { return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 86f220abd..fd87edc28 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -54,7 +54,7 @@ import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; @@ -194,7 +194,7 @@ export class NoteCreateService implements OnApplicationShutdown { private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, @@ -843,9 +843,9 @@ export class NoteCreateService implements OnApplicationShutdown { const r = this.redisForTimelines.pipeline(); if (note.channelId) { - this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); + this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -855,9 +855,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -895,9 +895,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -913,36 +913,36 @@ export class NoteCreateService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); } } if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL - this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } // 自分自身以外への返信 if (note.replyId && note.replyUserId !== note.userId) { - this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); } } else { - this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimeline', note.id, 1000, r); + this.fanoutTimelineService.push('localTimeline', note.id, 1000, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); + this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 432887b3b..29e48aa8c 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -20,7 +20,7 @@ import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Packed } from '@/misc/json-schema.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; export type RolePolicies = { @@ -108,7 +108,7 @@ export class RoleService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private idService: IdService, private moderationLogService: ModerationLogService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { //this.onMessage = this.onMessage.bind(this); @@ -482,7 +482,7 @@ export class RoleService implements OnApplicationShutdown { const redisPipeline = this.redisForTimelines.pipeline(); for (const role of roles) { - this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); + this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); this.globalEventService.publishRoleTimelineStream(role.id, 'note', note); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index bd7f29802..3062999c0 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -29,7 +29,7 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -84,7 +84,7 @@ export class UserFollowingService implements OnModuleInit { private webhookService: WebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -305,7 +305,7 @@ export class UserFollowingService implements OnModuleInit { } }); - this.funoutTimelineService.purge(`homeTimeline:${follower.id}`); + this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`); } // Publish followed event @@ -374,7 +374,7 @@ export class UserFollowingService implements OnModuleInit { } }); - this.funoutTimelineService.purge(`homeTimeline:${follower.id}`); + this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`); } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 29e56b108..0bf2688b4 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -12,7 +12,7 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApiError } from '../../error.js'; @@ -71,7 +71,7 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private noteReadService: NoteReadService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -98,7 +98,7 @@ export default class extends Endpoint { // eslint- this.globalEventService.publishInternalEvent('antennaUpdated', antenna); } - let noteIds = await this.funoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 4ca7325f3..f9207199d 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -12,7 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -70,7 +70,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, private metaService: MetaService, @@ -99,7 +99,7 @@ export default class extends Endpoint { // eslint- this.cacheService.userMutingsCache.fetch(me.id), ]) : [new Set()]; - let noteIds = await this.funoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); if (noteIds.length > 0) { diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index cbab3a83a..cea423406 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryPostsRepository } from '@/models/_.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; export const meta = { tags: ['gallery'], @@ -27,25 +28,49 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + untilId: { type: 'string', format: 'misskey:id' }, + }, required: [], } as const; @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export + private galleryPostsRankingCache: string[] = []; + private galleryPostsRankingCacheLastFetchedAt = 0; + constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, private galleryPostEntityService: GalleryPostEntityService, + private featuredService: FeaturedService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.galleryPostsRepository.createQueryBuilder('post') - .andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); + let postIds: string[]; + if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) { + postIds = this.galleryPostsRankingCache; + } else { + postIds = await this.featuredService.getGalleryPostsRanking(100); + this.galleryPostsRankingCache = postIds; + this.galleryPostsRankingCacheLastFetchedAt = Date.now(); + } - const posts = await query.limit(10).getMany(); + postIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + postIds = postIds.filter(id => id < ps.untilId!); + } + postIds = postIds.slice(0, ps.limit); + + if (postIds.length === 0) { + return []; + } + + const query = this.galleryPostsRepository.createQueryBuilder('post') + .where('post.id IN (:...postIds)', { postIds: postIds }); + + const posts = await query.getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 561b2492a..cc424261b 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js'; +import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -57,6 +58,7 @@ export default class extends Endpoint { // eslint- @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, + private featuredService: FeaturedService, private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { @@ -88,6 +90,11 @@ export default class extends Endpoint { // eslint- userId: me.id, }); + // ランキング更新 + if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { + await this.featuredService.updateGalleryPostsRanking(post.id, 1); + } + this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index 832b62282..caa4d4555 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -6,6 +6,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js'; +import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; +import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -49,6 +51,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, + + private featuredService: FeaturedService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); @@ -68,6 +73,11 @@ export default class extends Endpoint { // eslint- // Delete like await this.galleryLikesRepository.delete(exist.id); + // ランキング更新 + if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { + await this.featuredService.updateGalleryPostsRanking(post.id, -1); + } + this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index c45687430..bc6cbe242 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -64,16 +64,16 @@ export default class extends Endpoint { // eslint- } } - if (noteIds.length === 0) { - return []; - } - noteIds.sort((a, b) => a > b ? -1 : 1); if (ps.untilId) { noteIds = noteIds.filter(id => id < ps.untilId!); } noteIds = noteIds.slice(0, ps.limit); + if (noteIds.length === 0) { + return []; + } + const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 408c2fa37..372199844 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -77,7 +77,7 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private queryService: QueryService, private userFollowingService: UserFollowingService, private metaService: MetaService, @@ -120,20 +120,20 @@ export default class extends Endpoint { // eslint- let shouldFallbackToDb = false; if (ps.withFiles) { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ + const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.getMulti([ `homeTimelineWithFiles:${me.id}`, 'localTimelineWithFiles', ], untilId, sinceId); noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); } else if (ps.withReplies) { - const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([ + const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.fanoutTimelineService.getMulti([ `homeTimeline:${me.id}`, 'localTimeline', 'localTimelineWithReplies', ], untilId, sinceId); noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds])); } else { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ + const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.getMulti([ `homeTimeline:${me.id}`, 'localTimeline', ], untilId, sinceId); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 79baa6b28..7d8dec7b8 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; @@ -69,7 +69,7 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private queryService: QueryService, private metaService: MetaService, ) { @@ -107,9 +107,9 @@ export default class extends Endpoint { // eslint- let noteIds: string[]; if (ps.withFiles) { - noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId); + noteIds = await this.fanoutTimelineService.get('localTimelineWithFiles', untilId, sinceId); } else { - const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([ + const [nonReplyNoteIds, replyNoteIds] = await this.fanoutTimelineService.getMulti([ 'localTimeline', 'localTimelineWithReplies', ], untilId, sinceId); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 8037d4862..470abe0b1 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -14,7 +14,7 @@ import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; @@ -65,7 +65,7 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private userFollowingService: UserFollowingService, private queryService: QueryService, private metaService: MetaService, @@ -101,7 +101,7 @@ export default class extends Endpoint { // eslint- this.cacheService.userBlockedCache.fetch(me.id), ]); - let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); let redisTimeline: MiNote[] = []; diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index dbc387559..1ac1d37f4 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -13,7 +13,7 @@ import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; @@ -81,7 +81,7 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private cacheService: CacheService, private idService: IdService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private queryService: QueryService, private metaService: MetaService, ) { @@ -123,7 +123,7 @@ export default class extends Endpoint { // eslint- this.cacheService.userBlockedCache.fetch(me.id), ]); - let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); let redisTimeline: MiNote[] = []; diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index daa9affc2..7010df22c 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -11,7 +11,7 @@ import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -66,7 +66,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -84,7 +84,7 @@ export default class extends Endpoint { // eslint- return []; } - let noteIds = await this.funoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 2e7d939b1..a775b58f0 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -14,7 +14,7 @@ import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; @@ -71,7 +71,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private cacheService: CacheService, private idService: IdService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { @@ -90,9 +90,9 @@ export default class extends Endpoint { // eslint- ]) : [new Set()]; const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ - this.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId), - ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), - ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), + this.fanoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId), + ps.withReplies ? this.fanoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), + ps.withChannelNotes ? this.fanoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), ]); let noteIds = Array.from(new Set([ diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index 3ba26ad34..dd4304e6e 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -58,7 +58,7 @@ export class FeedService { const feed = new Feed({ id: author.link, title: `${author.name} (@${user.username}@${this.config.host})`, - updated: this.idService.parse(notes[0].id).date, + updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined, generator: 'Misskey', description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, link: author.link, diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 1cbfec3e5..251d66276 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -93,7 +93,7 @@ describe('Webリソース', () => { }); aliceChannel = await channel(alice, {}); - bob = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); afterAll(async () => { @@ -152,6 +152,11 @@ describe('Webリソース', () => { type, })); + test('がGETできる。(ノートが存在しない場合でも。)', async () => await ok({ + path: path(bob.username), + type, + })); + test('は存在しないユーザーはGETできない。', async () => await notOk({ path: path('nonexisting'), status: 404, diff --git a/packages/frontend/assets/sounds/syuilo/bubble1.mp3 b/packages/frontend/assets/sounds/syuilo/bubble1.mp3 new file mode 100644 index 000000000..05b8ef8b1 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/bubble1.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/bubble2.mp3 b/packages/frontend/assets/sounds/syuilo/bubble2.mp3 new file mode 100644 index 000000000..8b4f8df6e Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/bubble2.mp3 differ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index c7736f7ac..fab12df57 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -9,7 +9,7 @@ "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", "build-storybook": "pnpm build-storybook-pre && storybook build", "chromatic": "chromatic", - "test": "vitest --run", + "test": "vitest --run --globals", "test-and-coverage": "vitest --run --coverage --globals", "typecheck": "vue-tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", @@ -18,7 +18,7 @@ "dependencies": { "@discordapp/twemoji": "14.1.2", "@github/webauthn-json": "2.1.1", - "@rollup/plugin-alias": "5.0.1", + "@rollup/plugin-alias": "5.1.0", "@rollup/plugin-json": "6.0.1", "@rollup/plugin-replace": "5.0.5", "@rollup/pluginutils": "5.0.5", @@ -26,7 +26,7 @@ "@tabler/icons-webfont": "2.37.0", "@vitejs/plugin-vue": "4.5.0", "@vue-macros/reactivity-transform": "0.4.0", - "@vue/compiler-sfc": "3.3.8", + "@vue/compiler-sfc": "3.3.9", "astring": "1.8.6", "autosize": "6.0.1", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", @@ -39,7 +39,7 @@ "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "9.0.0", + "chromatic": "9.1.0", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", @@ -57,7 +57,7 @@ "photoswipe": "5.4.2", "punycode": "2.3.1", "querystring": "0.2.1", - "rollup": "4.4.1", + "rollup": "4.6.0", "sanitize-html": "2.11.0", "shiki": "^0.14.5", "sass": "1.69.5", @@ -73,8 +73,8 @@ "uuid": "9.0.1", "v-code-diff": "1.7.2", "vanilla-tilt": "1.8.1", - "vite": "4.5.0", - "vue": "3.3.8", + "vite": "5.0.2", + "vue": "3.3.9", "vuedraggable": "next" }, "devDependencies": { @@ -96,27 +96,27 @@ "@storybook/types": "7.5.3", "@storybook/vue3": "7.5.3", "@storybook/vue3-vite": "7.5.3", - "@testing-library/vue": "8.0.0", + "@testing-library/vue": "8.0.1", "@types/escape-regexp": "0.0.3", "@types/estree": "1.0.5", - "@types/matter-js": "0.19.4", - "@types/micromatch": "4.0.5", - "@types/node": "20.9.1", - "@types/punycode": "2.1.2", - "@types/sanitize-html": "2.9.4", + "@types/matter-js": "0.19.5", + "@types/micromatch": "4.0.6", + "@types/node": "20.10.0", + "@types/punycode": "2.1.3", + "@types/sanitize-html": "2.9.5", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/uuid": "9.0.7", - "@types/websocket": "1.0.9", - "@types/ws": "8.5.9", - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@types/websocket": "1.0.10", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "6.12.0", + "@typescript-eslint/parser": "6.12.0", "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.3.8", + "@vue/runtime-core": "3.3.9", "acorn": "8.11.2", "cross-env": "7.0.3", - "cypress": "13.5.1", - "eslint": "8.53.0", + "cypress": "13.6.0", + "eslint": "8.54.0", "eslint-plugin-import": "2.29.0", "eslint-plugin-vue": "9.18.1", "fast-glob": "3.3.2", diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 12bb56a87..594fe6423 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -204,12 +204,16 @@ export async function common(createVue: () => App) { if (defaultStore.state.keepScreenOn) { if ('wakeLock' in navigator) { - navigator.wakeLock.request('screen'); - - document.addEventListener('visibilitychange', async () => { - if (document.visibilityState === 'visible') { - navigator.wakeLock.request('screen'); - } + navigator.wakeLock.request('screen') + .then(() => { + document.addEventListener('visibilitychange', async () => { + if (document.visibilityState === 'visible') { + navigator.wakeLock.request('screen'); + } + }); + }) + .catch(() => { + // If Permission fails on an AppleDevice such as Safari }); } } diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 43c64b4c8..b6e8f1ff2 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -37,6 +37,7 @@ import * as Misskey from 'misskey-js'; import bytes from '@/filters/bytes.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import hasAudio from '@/scripts/media-has-audio.js'; const props = defineProps<{ video: Misskey.entities.DriveFile; @@ -49,6 +50,12 @@ const videoEl = shallowRef(); watch(videoEl, () => { if (videoEl.value) { videoEl.value.volume = 0.3; + hasAudio(videoEl.value).then(had => { + if (!had) { + videoEl.value.loop = videoEl.value.muted = true; + videoEl.value.play(); + } + }); } }); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 6349df2e3..04fe8d52f 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -142,6 +142,12 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ +
+ + diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index cd1707a59..e549901f0 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -5,6 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only