diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0583a66960..e409adf644 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "version": "8.9.2" }, "ghcr.io/devcontainers/features/node:1": { - "version": "20.5.1" + "version": "20.10.0" } }, "forwardPorts": [3000], diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index e6de226e6b..281e2058a6 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] api-json-name: [api-base.json, api-head.json] include: - api-json-name: api-base.json diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index cbf0275620..6e8327ca07 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] services: postgres: diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 9575e22d02..50c225189d 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] steps: - uses: actions/checkout@v4.1.1 @@ -51,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.5.1] + node-version: [20.10.0] browser: [chrome] services: diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index ae1ddf31d1..76e170b3e3 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index d9bfb12be5..694fa1a8f5 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] steps: - uses: actions/checkout@v4.1.1 diff --git a/.node-version b/.node-version index 7cc2069986..d5a159609d 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.5.1 +20.10.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e1a79dd66..72707469cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,15 +17,19 @@ ### General - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) +- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client - Feat: コントロールパネルのアイコンデコレーションページから直接ファイルをアップロードおよび設定できるように - Enhance: 絵文字のオートコンプリート機能強化 #12364 +- Enhance: ユーザーのRawデータを表示するページが復活 +- Enhance: リアクション選択時に音を鳴らせるように - Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 - Fix: コードエディタが正しく表示されない問題を修正 - Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 +- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正 ### Server - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように @@ -33,6 +37,8 @@ - Fix: ロールタイムラインが保存されない問題を修正 - Fix: api.jsonの生成ロジックを改善 #12402 - Fix: 招待コードが使い回せる問題を修正 +- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正 +- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正 ## 2023.11.1 @@ -49,6 +55,7 @@ - 例: `$[unixtime 1701356400]` - Enhance: プラグインでエラーが発生した場合のハンドリングを強化 - Enhance: 細かなUIのブラッシュアップ +- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加 - Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339 - Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236 - Fix: プラグインでノートの表示を書き換えられない問題を修正 diff --git a/Dockerfile b/Dockerfile index d397fe01cd..028a3976d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20.5.1-bullseye +ARG NODE_VERSION=20.10.0-bullseye # build assets & compile TypeScript diff --git a/locales/index.d.ts b/locales/index.d.ts index 3ed2706298..6097ae130e 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; @@ -1040,6 +1042,7 @@ export interface Locale { "enableChartsForFederatedInstances": string; "showClipButtonInNoteFooter": string; "reactionsDisplaySize": string; + "limitWidthOfReaction": string; "noteIdOrUrl": string; "video": string; "videos": string; @@ -1642,7 +1645,9 @@ export interface Locale { "assignTarget": string; "descriptionOfAssignTarget": string; "manual": string; + "manualRoles": string; "conditional": string; + "conditionalRoles": string; "condition": string; "isConditionalRole": string; "isPublic": string; @@ -1940,6 +1945,7 @@ export interface Locale { "notification": string; "antenna": string; "channel": string; + "reaction": string; }; "_ago": { "future": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9b5dfce63d..1f6695b3e3 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: "操作を完了できません" @@ -1037,6 +1039,7 @@ enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" reactionsDisplaySize: "リアクションの表示サイズ" +limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する" noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" @@ -1552,7 +1555,9 @@ _role: assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" manual: "マニュアル" + manualRoles: "マニュアルロール" conditional: "コンディショナル" + conditionalRoles: "コンディショナルロール" condition: "条件" isConditionalRole: "これはコンディショナルロールです。" isPublic: "公開ロール" @@ -1845,6 +1850,7 @@ _sfx: notification: "通知" antenna: "アンテナ受信" channel: "チャンネル通知" + reaction: "リアクション選択時" _ago: future: "未来" diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 2815d24734..2c27a02559 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 9fb29e0e68..bf6f0ef879 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 c633c329e5..6a1b0aa879 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 9617f83880..507fc464ff 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 86f220abd0..fd87edc28e 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 432887b3b7..29e48aa8ca 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 bd7f298021..3062999c08 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 29e56b1085..0bf2688b4a 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 fae4249c8a..f9207199d6 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -12,9 +12,10 @@ 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'; import { ApiError } from '../../error.js'; export const meta = { @@ -69,15 +70,18 @@ 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, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const isRangeSpecified = untilId != null && sinceId != null; + const serverSettings = await this.metaService.fetch(); + const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, }); @@ -88,14 +92,14 @@ export default class extends Endpoint { // eslint- if (me) this.activeUsersChart.read(me); - if (isRangeSpecified || sinceId == null) { + if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) { const [ userIdsWhoMeMuting, ] = me ? await Promise.all([ 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 cbab3a83a4..cea4234065 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 561b2492ab..cc424261b4 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 832b62282f..caa4d45553 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/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 8ba29c5658..b045c01189 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -243,7 +243,7 @@ export default class extends Endpoint { // eslint- function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える - const length = JSON.stringify(ps.mutedWords).length; + const length = JSON.stringify(mutedWords).length; if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 513b77b98a..c5d42dabe4 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -262,7 +262,7 @@ export default class extends Endpoint { // eslint- if (renote.channelId && renote.channelId !== ps.channelId) { // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する - const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); if (renoteChannel == null) { // リノートしたいノートが書き込まれているチャンネルが無い throw new ApiError(meta.errors.noSuchChannel); diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index c456874309..bc6cbe242f 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 408c2fa371..372199844d 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 79baa6b285..7d8dec7b8f 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 8037d4862f..470abe0b14 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 dbc3875597..1ac1d37f48 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 daa9affc20..7010df22c9 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 a43e572922..a775b58f03 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -14,7 +14,8 @@ 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'; export const meta = { @@ -70,7 +71,8 @@ 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) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -78,7 +80,9 @@ export default class extends Endpoint { // eslint- const isRangeSpecified = untilId != null && sinceId != null; const isSelf = me && (me.id === ps.userId); - if (isRangeSpecified || sinceId == null) { + const serverSettings = await this.metaService.fetch(); + + if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) { const [ userIdsWhoMeMuting, ] = me ? await Promise.all([ @@ -86,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 3ba26ad34a..dd4304e6ef 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 1cbfec3e5f..251d662760 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 0000000000..05b8ef8b10 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 0000000000..8b4f8df6e9 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/bubble2.mp3 differ diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index 66114b8734..2c7be319cb 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
{{ i18n.ts.reporter }}:
+
{{ i18n.ts.reporter }}: @{{ report.reporter.username }}
{{ i18n.ts.moderator }}: diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index a0f4961116..c1fcbd7ac1 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -265,7 +265,7 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): // 前方一致(エイリアスなし) emojiDb.some(x => { if (x.name.startsWith(query) && !x.aliasOf) { - matched.set(x.name, { emoji: x, score: query.length }); + matched.set(x.name, { emoji: x, score: query.length + 1 }); } return matched.size === max; }); @@ -273,8 +273,8 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): // 前方一致(エイリアス込み) if (matched.size < max) { emojiDb.some(x => { - if (x.name.startsWith(query)) { - matched.set(x.name, { emoji: x, score: query.length }); + if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); } return matched.size === max; }); @@ -283,36 +283,32 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): // 部分一致(エイリアス込み) if (matched.size < max) { emojiDb.some(x => { - if (x.name.includes(query)) { - matched.set(x.name, { emoji: x, score: query.length }); + if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); } return matched.size === max; }); } - // 簡易あいまい検索 - if (matched.size < max) { + // 簡易あいまい検索(3文字以上) + if (matched.size < max && query.length > 3) { const queryChars = [...query]; const hitEmojis = new Map(); for (const x of emojiDb) { - // クエリ文字列の1文字単位で絵文字名にヒットするかを見る - // ただし、過剰に検出されるのを防ぐためクエリ文字列に登場する順番で絵文字名を走査する + // 文字列の位置を進めながら、クエリの文字を順番に探す - let queryCharHitPos = 0; - let queryCharHitCount = 0; - for (let idx = 0; idx < queryChars.length; idx++) { - queryCharHitPos = x.name.indexOf(queryChars[idx], queryCharHitPos); - if (queryCharHitPos <= -1) { - break; - } - - queryCharHitCount++; + let pos = 0; + let hit = 0; + for (const c of queryChars) { + pos = x.name.indexOf(c, pos); + if (pos <= -1) break; + hit++; } - // ヒット数が少なすぎると検索結果が汚れるので調節する - if (queryCharHitCount > 2) { - hitEmojis.set(x.name, { emoji: x, score: queryCharHitCount }); + // 半分以上の文字が含まれていればヒットとする + if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { + hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); } } diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 43c64b4c85..b6e8f1ff22 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 6349df2e30..d047495dc9 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -163,6 +163,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; +import * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; @@ -336,6 +337,8 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.reactionAcceptance === 'likeOnly') { + sound.play('reaction'); + if (props.mock) { return; } @@ -354,6 +357,8 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + sound.play('reaction'); + if (props.mock) { emit('reaction', reaction); return; diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index d1bc3f676f..d8089ac36f 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -210,6 +210,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; +import * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; @@ -369,6 +370,8 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.reactionAcceptance === 'likeOnly') { + sound.play('reaction'); + os.api('notes/reactions/create', { noteId: appearNote.id, reaction: '❤️', @@ -383,6 +386,8 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + sound.play('reaction'); + os.api('notes/reactions/create', { noteId: appearNote.id, reaction: reaction, diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 2b850016c5..65a5c2374e 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" @click="toggleReaction()" > - + {{ count }} @@ -28,6 +28,7 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue'; import { claimAchievement } from '@/scripts/achievements.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ reaction: string; @@ -59,6 +60,10 @@ async function toggleReaction() { }); if (confirm.canceled) return; + if (oldReaction !== props.reaction) { + sound.play('reaction'); + } + if (mock) { emit('reactionToggled', props.reaction, (props.count - 1)); return; @@ -75,6 +80,8 @@ async function toggleReaction() { } }); } else { + sound.play('reaction'); + if (mock) { emit('reactionToggled', props.reaction, (props.count + 1)); return; @@ -188,7 +195,7 @@ if (!mock) { } } -.icon { +.limitWidth { max-width: 150px; } diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index d3f3773564..91cd86485f 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -198,13 +198,13 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._role.new }}
- +
- +
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 06d3789829..313b5efc46 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.ts.limitWidthOfReaction }}
@@ -226,6 +227,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover')); const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); +const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); @@ -290,6 +292,7 @@ watch([ overridedDeviceKind, mediaListWithOneImageAppearance, reactionsDisplaySize, + limitWidthOfReaction, highlightSensitiveMedia, keepScreenOn, disableStreamingTimeline, diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index cd1707a594..05e4b0d14c 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