1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-12-01 00:08:31 +09:00

Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2023-11-27 16:59:33 +09:00
commit 058ddc8c32
43 changed files with 460 additions and 116 deletions

View File

@ -22,10 +22,13 @@
### Client ### Client
- Enhance: 絵文字のオートコンプリート機能強化 #12364 - Enhance: 絵文字のオートコンプリート機能強化 #12364
- Enhance: ユーザーのRawデータを表示するページが復活
- Enhance: リアクション選択時に音を鳴らせるように
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
- Fix: コードエディタが正しく表示されない問題を修正 - Fix: コードエディタが正しく表示されない問題を修正
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 - Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正
### Server ### Server
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
@ -33,6 +36,8 @@
- Fix: ロールタイムラインが保存されない問題を修正 - Fix: ロールタイムラインが保存されない問題を修正
- Fix: api.jsonの生成ロジックを改善 #12402 - Fix: api.jsonの生成ロジックを改善 #12402
- Fix: 招待コードが使い回せる問題を修正 - Fix: 招待コードが使い回せる問題を修正
- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正
- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正
## 2023.11.1 ## 2023.11.1
@ -49,6 +54,7 @@
- 例: `$[unixtime 1701356400]` - 例: `$[unixtime 1701356400]`
- Enhance: プラグインでエラーが発生した場合のハンドリングを強化 - Enhance: プラグインでエラーが発生した場合のハンドリングを強化
- Enhance: 細かなUIのブラッシュアップ - Enhance: 細かなUIのブラッシュアップ
- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
- Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339 - Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339
- Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236 - Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236
- Fix: プラグインでノートの表示を書き換えられない問題を修正 - Fix: プラグインでノートの表示を書き換えられない問題を修正

3
locales/index.d.ts vendored
View File

@ -634,6 +634,8 @@ export interface Locale {
"popout": string; "popout": string;
"volume": string; "volume": string;
"masterVolume": string; "masterVolume": string;
"notUseSound": string;
"useSoundOnlyWhenActive": string;
"details": string; "details": string;
"chooseEmoji": string; "chooseEmoji": string;
"unableToProcess": string; "unableToProcess": string;
@ -2248,6 +2250,7 @@ export interface Locale {
"chatBg": string; "chatBg": string;
"antenna": string; "antenna": string;
"channel": string; "channel": string;
"reaction": string;
}; };
"_ago": { "_ago": {
"future": string; "future": string;

View File

@ -631,6 +631,8 @@ showInPage: "ページで表示"
popout: "ポップアウト" popout: "ポップアウト"
volume: "音量" volume: "音量"
masterVolume: "マスター音量" masterVolume: "マスター音量"
notUseSound: "サウンドを出力しない"
useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する"
details: "詳細" details: "詳細"
chooseEmoji: "絵文字を選択" chooseEmoji: "絵文字を選択"
unableToProcess: "操作を完了できません" unableToProcess: "操作を完了できません"
@ -2150,6 +2152,7 @@ _sfx:
chatBg: "チャット(バックグラウンド)" chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信" antenna: "アンテナ受信"
channel: "チャンネル通知" channel: "チャンネル通知"
reaction: "リアクション選択時"
_ago: _ago:
future: "未来" future: "未来"

View File

@ -16,7 +16,7 @@ import type { AntennasRepository, UserGroupJoiningsRepository, UserListMembershi
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.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'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -42,7 +42,7 @@ export class AntennaService implements OnApplicationShutdown {
private utilityService: UtilityService, private utilityService: UtilityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
) { ) {
this.antennasFetched = false; this.antennasFetched = false;
this.antennas = []; this.antennas = [];
@ -97,7 +97,7 @@ export class AntennaService implements OnApplicationShutdown {
const redisPipeline = this.redisForTimelines.pipeline(); const redisPipeline = this.redisForTimelines.pipeline();
for (const antenna of matchedAntennas) { 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); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
} }

View File

@ -64,7 +64,7 @@ import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js'; import { SearchService } from './SearchService.js';
import { ClipService } from './ClipService.js'; import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js'; import { FeaturedService } from './FeaturedService.js';
import { FunoutTimelineService } from './FunoutTimelineService.js'; import { FanoutTimelineService } from './FanoutTimelineService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js';
import { RegistryApiService } from './RegistryApiService.js'; import { RegistryApiService } from './RegistryApiService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js';
@ -202,7 +202,7 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; 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 $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
@ -344,7 +344,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
SearchService, SearchService,
ClipService, ClipService,
FeaturedService, FeaturedService,
FunoutTimelineService, FanoutTimelineService,
ChannelFollowingService, ChannelFollowingService,
RegistryApiService, RegistryApiService,
ChartLoggerService, ChartLoggerService,
@ -479,7 +479,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$SearchService, $SearchService,
$ClipService, $ClipService,
$FeaturedService, $FeaturedService,
$FunoutTimelineService, $FanoutTimelineService,
$ChannelFollowingService, $ChannelFollowingService,
$RegistryApiService, $RegistryApiService,
$ChartLoggerService, $ChartLoggerService,
@ -615,7 +615,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
SearchService, SearchService,
ClipService, ClipService,
FeaturedService, FeaturedService,
FunoutTimelineService, FanoutTimelineService,
ChannelFollowingService, ChannelFollowingService,
RegistryApiService, RegistryApiService,
FederationChart, FederationChart,
@ -749,7 +749,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$SearchService, $SearchService,
$ClipService, $ClipService,
$FeaturedService, $FeaturedService,
$FunoutTimelineService, $FanoutTimelineService,
$ChannelFollowingService, $ChannelFollowingService,
$RegistryApiService, $RegistryApiService,
$FederationChart, $FederationChart,

View File

@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@Injectable() @Injectable()
export class FunoutTimelineService { export class FanoutTimelineService {
constructor( constructor(
@Inject(DI.redisForTimelines) @Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis, private redisForTimelines: Redis.Redis,

View File

@ -5,11 +5,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; 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 { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと 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 PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 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); return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
} }
@bindThis
public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score);
}
@bindThis @bindThis
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> { public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); 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); return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold);
} }
@bindThis
public getGalleryPostsRanking(threshold: number): Promise<MiGalleryPost['id'][]> {
return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold);
}
@bindThis @bindThis
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> { public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold); return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);

View File

@ -55,7 +55,7 @@ import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.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 { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
@ -198,7 +198,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private notificationService: NotificationService, private notificationService: NotificationService,
private relayService: RelayService, private relayService: RelayService,
@ -867,9 +867,9 @@ export class NoteCreateService implements OnApplicationShutdown {
const r = this.redisForTimelines.pipeline(); const r = this.redisForTimelines.pipeline();
if (note.channelId) { 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({ const channelFollowings = await this.channelFollowingsRepository.find({
where: { where: {
@ -879,9 +879,9 @@ export class NoteCreateService implements OnApplicationShutdown {
}); });
for (const channelFollowing of channelFollowings) { 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) { 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 { } else {
@ -919,9 +919,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!following.withReplies) continue; 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) { 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);
} }
} }
@ -937,36 +937,36 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!userListMembership.withReplies) continue; 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) { 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 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) { 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) { 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) { if (note.visibility === 'public' && note.userHost == null) {
this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
} }
} else { } 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) { 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) { 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) { if (note.fileIds.length > 0) {
this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
} }
} }
} }

View File

@ -20,7 +20,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.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'; import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = { export type RolePolicies = {
@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private idService: IdService, private idService: IdService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
@ -485,7 +485,7 @@ export class RoleService implements OnApplicationShutdown {
const redisPipeline = this.redisForTimelines.pipeline(); const redisPipeline = this.redisForTimelines.pipeline();
for (const role of roles) { 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); this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
} }

View File

@ -29,7 +29,7 @@ import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -84,7 +84,7 @@ export class UserFollowingService implements OnModuleInit {
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService, private accountMoveService: AccountMoveService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart, 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 // 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)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {

View File

@ -12,7 +12,7 @@ import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.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 { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private queryService: QueryService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.globalEventService.publishInternalEvent('antennaUpdated', antenna); 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); noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length === 0) { if (noteIds.length === 0) {
return []; return [];

View File

@ -12,9 +12,10 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.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 { isUserRelated } from '@/misc/is-user-related.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -69,15 +70,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService, private idService: IdService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private queryService: QueryService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private cacheService: CacheService, private cacheService: CacheService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); 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 sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const isRangeSpecified = untilId != null && sinceId != null; const isRangeSpecified = untilId != null && sinceId != null;
const serverSettings = await this.metaService.fetch();
const channel = await this.channelsRepository.findOneBy({ const channel = await this.channelsRepository.findOneBy({
id: ps.channelId, id: ps.channelId,
}); });
@ -88,14 +92,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.activeUsersChart.read(me); if (me) this.activeUsersChart.read(me);
if (isRangeSpecified || sinceId == null) { if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
const [ const [
userIdsWhoMeMuting, userIdsWhoMeMuting,
] = me ? await Promise.all([ ] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id), this.cacheService.userMutingsCache.fetch(me.id),
]) : [new Set<string>()]; ]) : [new Set<string>()];
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); noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length > 0) { if (noteIds.length > 0) {

View File

@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository } from '@/models/_.js'; import type { GalleryPostsRepository } from '@/models/_.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { FeaturedService } from '@/core/FeaturedService.js';
export const meta = { export const meta = {
tags: ['gallery'], tags: ['gallery'],
@ -27,25 +28,49 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: {}, properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [], required: [],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
private galleryPostsRankingCache: string[] = [];
private galleryPostsRankingCacheLastFetchedAt = 0;
constructor( constructor(
@Inject(DI.galleryPostsRepository) @Inject(DI.galleryPostsRepository)
private galleryPostsRepository: GalleryPostsRepository, private galleryPostsRepository: GalleryPostsRepository,
private galleryPostEntityService: GalleryPostEntityService, private galleryPostEntityService: GalleryPostEntityService,
private featuredService: FeaturedService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.galleryPostsRepository.createQueryBuilder('post') let postIds: string[];
.andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) {
.andWhere('post.likedCount > 0') postIds = this.galleryPostsRankingCache;
.orderBy('post.likedCount', 'DESC'); } 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); return await this.galleryPostEntityService.packMany(posts, me);
}); });

View File

@ -6,6 +6,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.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 { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
@ -57,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.galleryLikesRepository) @Inject(DI.galleryLikesRepository)
private galleryLikesRepository: GalleryLikesRepository, private galleryLikesRepository: GalleryLikesRepository,
private featuredService: FeaturedService,
private idService: IdService, private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -88,6 +90,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id, 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); this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1);
}); });
} }

View File

@ -6,6 +6,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.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 { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
@ -49,6 +51,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.galleryLikesRepository) @Inject(DI.galleryLikesRepository)
private galleryLikesRepository: GalleryLikesRepository, private galleryLikesRepository: GalleryLikesRepository,
private featuredService: FeaturedService,
private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
@ -68,6 +73,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Delete like // Delete like
await this.galleryLikesRepository.delete(exist.id); 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); this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1);
}); });
} }

View File

@ -64,16 +64,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
} }
if (noteIds.length === 0) {
return [];
}
noteIds.sort((a, b) => a > b ? -1 : 1); noteIds.sort((a, b) => a > b ? -1 : 1);
if (ps.untilId) { if (ps.untilId) {
noteIds = noteIds.filter(id => id < ps.untilId!); noteIds = noteIds.filter(id => id < ps.untilId!);
} }
noteIds = noteIds.slice(0, ps.limit); noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length === 0) {
return [];
}
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds }) .where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')

View File

@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { CacheService } from '@/core/CacheService.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 { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private idService: IdService, private idService: IdService,
private cacheService: CacheService, private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private queryService: QueryService, private queryService: QueryService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private metaService: MetaService, private metaService: MetaService,
@ -122,20 +122,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let shouldFallbackToDb = false; let shouldFallbackToDb = false;
if (ps.withFiles) { if (ps.withFiles) {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.getMulti([
`homeTimelineWithFiles:${me.id}`, `homeTimelineWithFiles:${me.id}`,
'localTimelineWithFiles', 'localTimelineWithFiles',
], untilId, sinceId); ], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
} else if (ps.withReplies) { } else if (ps.withReplies) {
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([ const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.fanoutTimelineService.getMulti([
`homeTimeline:${me.id}`, `homeTimeline:${me.id}`,
'localTimeline', 'localTimeline',
'localTimelineWithReplies', 'localTimelineWithReplies',
], untilId, sinceId); ], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds])); noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
} else { } else {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.getMulti([
`homeTimeline:${me.id}`, `homeTimeline:${me.id}`,
'localTimeline', 'localTimeline',
], untilId, sinceId); ], untilId, sinceId);

View File

@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.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 { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private idService: IdService, private idService: IdService,
private cacheService: CacheService, private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService, private metaService: MetaService,
) { ) {
@ -109,9 +109,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let noteIds: string[]; let noteIds: string[];
if (ps.withFiles) { if (ps.withFiles) {
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId); noteIds = await this.fanoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
} else { } else {
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([ const [nonReplyNoteIds, replyNoteIds] = await this.fanoutTimelineService.getMulti([
'localTimeline', 'localTimeline',
'localTimelineWithReplies', 'localTimelineWithReplies',
], untilId, sinceId); ], untilId, sinceId);

View File

@ -14,7 +14,7 @@ import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.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 { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private idService: IdService, private idService: IdService,
private cacheService: CacheService, private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService, private metaService: MetaService,
@ -103,7 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.cacheService.userBlockedCache.fetch(me.id), 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); noteIds = noteIds.slice(0, ps.limit);
let redisTimeline: MiNote[] = []; let redisTimeline: MiNote[] = [];

View File

@ -13,7 +13,7 @@ import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.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 { QueryService } from '@/core/QueryService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -82,7 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private cacheService: CacheService, private cacheService: CacheService,
private idService: IdService, private idService: IdService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService, private metaService: MetaService,
) { ) {
@ -125,7 +125,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.cacheService.userBlockedCache.fetch(me.id), 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); noteIds = noteIds.slice(0, ps.limit);
let redisTimeline: MiNote[] = []; let redisTimeline: MiNote[] = [];

View File

@ -11,7 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService, private idService: IdService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private queryService: QueryService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return []; 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); noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length === 0) { if (noteIds.length === 0) {

View File

@ -14,7 +14,8 @@ import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { QueryService } from '@/core/QueryService.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'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -71,7 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService, private queryService: QueryService,
private cacheService: CacheService, private cacheService: CacheService,
private idService: IdService, private idService: IdService,
private funoutTimelineService: FunoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -79,7 +81,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const isRangeSpecified = untilId != null && sinceId != null; const isRangeSpecified = untilId != null && sinceId != null;
const isSelf = me && (me.id === ps.userId); const isSelf = me && (me.id === ps.userId);
if (isRangeSpecified || sinceId == null) { const serverSettings = await this.metaService.fetch();
if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
const [ const [
userIdsWhoMeMuting, userIdsWhoMeMuting,
] = me ? await Promise.all([ ] = me ? await Promise.all([
@ -87,9 +91,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]) : [new Set<string>()]; ]) : [new Set<string>()];
const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([
this.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId), this.fanoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), ps.withReplies ? this.fanoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), ps.withChannelNotes ? this.fanoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
]); ]);
let noteIds = Array.from(new Set([ let noteIds = Array.from(new Set([

View File

@ -58,7 +58,7 @@ export class FeedService {
const feed = new Feed({ const feed = new Feed({
id: author.link, id: author.link,
title: `${author.name} (@${user.username}@${this.config.host})`, 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: 'CherryPick', generator: 'CherryPick',
description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
link: author.link, link: author.link,

View File

@ -93,7 +93,7 @@ describe('Webリソース', () => {
}); });
aliceChannel = await channel(alice, {}); aliceChannel = await channel(alice, {});
bob = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => { afterAll(async () => {
@ -152,6 +152,11 @@ describe('Webリソース', () => {
type, type,
})); }));
test('がGETできる。(ノートが存在しない場合でも。)', async () => await ok({
path: path(bob.username),
type,
}));
test('は存在しないユーザーはGETできない。', async () => await notOk({ test('は存在しないユーザーはGETできない。', async () => await notOk({
path: path('nonexisting'), path: path('nonexisting'),
status: 404, status: 404,

Binary file not shown.

Binary file not shown.

View File

@ -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-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", "build-storybook": "pnpm build-storybook-pre && storybook build",
"chromatic": "chromatic", "chromatic": "chromatic",
"test": "vitest --run", "test": "vitest --run --globals",
"test-and-coverage": "vitest --run --coverage --globals", "test-and-coverage": "vitest --run --coverage --globals",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",

View File

@ -218,12 +218,16 @@ export async function common(createVue: () => App<Element>) {
if (defaultStore.state.keepScreenOn) { if (defaultStore.state.keepScreenOn) {
if ('wakeLock' in navigator) { if ('wakeLock' in navigator) {
navigator.wakeLock.request('screen'); navigator.wakeLock.request('screen')
.then(() => {
document.addEventListener('visibilitychange', async () => { document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
navigator.wakeLock.request('screen'); navigator.wakeLock.request('screen');
} }
});
})
.catch(() => {
// If Permission fails on an AppleDevice such as Safari
}); });
} }
} }

View File

@ -266,7 +266,7 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
// //
emojiDb.some(x => { emojiDb.some(x => {
if (x.name.startsWith(query) && !x.aliasOf) { 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; return matched.size === max;
}); });
@ -274,8 +274,8 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
// //
if (matched.size < max) { if (matched.size < max) {
emojiDb.some(x => { emojiDb.some(x => {
if (x.name.startsWith(query)) { if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.name, { emoji: x, score: query.length }); matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
} }
return matched.size === max; return matched.size === max;
}); });
@ -284,36 +284,32 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
// //
if (matched.size < max) { if (matched.size < max) {
emojiDb.some(x => { emojiDb.some(x => {
if (x.name.includes(query)) { if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.name, { emoji: x, score: query.length }); matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
} }
return matched.size === max; return matched.size === max;
}); });
} }
// // 3
if (matched.size < max) { if (matched.size < max && query.length > 3) {
const queryChars = [...query]; const queryChars = [...query];
const hitEmojis = new Map<string, EmojiScore>(); const hitEmojis = new Map<string, EmojiScore>();
for (const x of emojiDb) { for (const x of emojiDb) {
// 1 //
//
let queryCharHitPos = 0; let pos = 0;
let queryCharHitCount = 0; let hit = 0;
for (let idx = 0; idx < queryChars.length; idx++) { for (const c of queryChars) {
queryCharHitPos = x.name.indexOf(queryChars[idx], queryCharHitPos); pos = x.name.indexOf(c, pos);
if (queryCharHitPos <= -1) { if (pos <= -1) break;
break; hit++;
}
queryCharHitCount++;
} }
// 調 //
if (queryCharHitCount > 2) { if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
hitEmojis.set(x.name, { emoji: x, score: queryCharHitCount }); hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
} }
} }

View File

@ -39,6 +39,7 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import hasAudio from '@/scripts/media-has-audio.js';
const props = defineProps<{ const props = defineProps<{
video: Misskey.entities.DriveFile; video: Misskey.entities.DriveFile;
@ -67,6 +68,12 @@ const videoEl = shallowRef<HTMLVideoElement>();
watch(videoEl, () => { watch(videoEl, () => {
if (videoEl.value) { if (videoEl.value) {
videoEl.value.volume = 0.3; videoEl.value.volume = 0.3;
hasAudio(videoEl.value).then(had => {
if (!had) {
videoEl.value.loop = videoEl.value.muted = true;
videoEl.value.play();
}
});
} }
}); });
</script> </script>

View File

@ -189,6 +189,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
</I18n> </I18n>
</div> </div>
<div v-else>
<!--
MkDateSeparatedList uses TransitionGroup which requires single element in the child elements
so MkNote create empty div instead of no elements
-->
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -210,6 +216,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js'; import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@ -437,6 +444,8 @@ function react(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') { if (appearNote.reactionAcceptance === 'likeOnly') {
sound.play('reaction');
if (props.mock) { if (props.mock) {
return; return;
} }
@ -474,6 +483,8 @@ async function toggleReaction(reaction) {
}); });
if (confirm.canceled) return; if (confirm.canceled) return;
sound.play('reaction');
os.api('notes/reactions/delete', { os.api('notes/reactions/delete', {
noteId: note.id, noteId: note.id,
}).then(() => { }).then(() => {
@ -485,6 +496,8 @@ async function toggleReaction(reaction) {
} }
}); });
} else { } else {
sound.play('reaction');
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction, reaction: reaction,
@ -499,6 +512,8 @@ function heartReact(): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
sound.play('reaction');
if (props.mock) { if (props.mock) {
return; return;
} }

View File

@ -292,6 +292,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js'; import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@ -487,6 +488,8 @@ function react(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') { if (appearNote.reactionAcceptance === 'likeOnly') {
sound.play('reaction');
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.id,
reaction: '❤️', reaction: '❤️',
@ -517,6 +520,8 @@ async function toggleReaction(reaction) {
}); });
if (confirm.canceled) return; if (confirm.canceled) return;
sound.play('reaction');
os.api('notes/reactions/delete', { os.api('notes/reactions/delete', {
noteId: note.id, noteId: note.id,
}).then(() => { }).then(() => {
@ -528,6 +533,8 @@ async function toggleReaction(reaction) {
} }
}); });
} else { } else {
sound.play('reaction');
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction, reaction: reaction,
@ -541,6 +548,9 @@ async function toggleReaction(reaction) {
function heartReact(): void { function heartReact(): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
sound.play('reaction');
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.id,
reaction: '❤️', reaction: '❤️',

View File

@ -30,6 +30,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js'; import { customEmojis } from '@/custom-emojis.js';
import * as sound from '@/scripts/sound.js';
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
@ -71,6 +72,10 @@ async function toggleReaction(ev) {
}); });
if (confirm.canceled) return; if (confirm.canceled) return;
if (oldReaction !== props.reaction) {
sound.play('reaction');
}
if (mock) { if (mock) {
emit('reactionToggled', props.reaction, (props.count - 1)); emit('reactionToggled', props.reaction, (props.count - 1));
return; return;
@ -87,6 +92,8 @@ async function toggleReaction(ev) {
} }
}); });
} else { } else {
sound.play('reaction');
if (mock) { if (mock) {
emit('reactionToggled', props.reaction, (props.count + 1)); emit('reactionToggled', props.reaction, (props.count + 1));
return; return;

View File

@ -120,6 +120,7 @@ import { computed, defineAsyncComponent, inject, provide, Ref, ref, shallowRef,
import * as mfm from 'cherrypick-mfm-js'; import * as mfm from 'cherrypick-mfm-js';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import * as sound from '@/scripts/sound.js';
import MkMediaList from '@/components/MkMediaList.vue'; import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue'; import MkPoll from '@/components/MkPoll.vue';
import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
@ -279,6 +280,8 @@ function react(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
if (props.note.reactionAcceptance === 'likeOnly') { if (props.note.reactionAcceptance === 'likeOnly') {
sound.play('reaction');
if (props.mock) { if (props.mock) {
return; return;
} }
@ -316,6 +319,8 @@ async function toggleReaction(reaction) {
}); });
if (confirm.canceled) return; if (confirm.canceled) return;
sound.play('reaction');
os.api('notes/reactions/delete', { os.api('notes/reactions/delete', {
noteId: note.id, noteId: note.id,
}).then(() => { }).then(() => {
@ -327,6 +332,8 @@ async function toggleReaction(reaction) {
} }
}); });
} else { } else {
sound.play('reaction');
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: note.id, noteId: note.id,
reaction: reaction, reaction: reaction,
@ -341,6 +348,8 @@ function heartReact(): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
sound.play('reaction');
if (props.mock) { if (props.mock) {
return; return;
} }

View File

@ -151,6 +151,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'sound_chatBg', 'sound_chatBg',
'sound_antenna', 'sound_antenna',
'sound_channel', 'sound_channel',
'sound_reaction',
]; ];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme', 'lightTheme',

View File

@ -7,9 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m"> <div class="_gaps_m">
<FormSection first> <FormSection first>
<template #label>{{ i18n.ts.sounds }}</template> <template #label>{{ i18n.ts.sounds }}</template>
<MkRange v-model="masterVolume" style="margin-bottom: 25px;" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <div class="_gaps_s">
<template #label>{{ i18n.ts.masterVolume }}</template> <MkSwitch v-model="notUseSound">
</MkRange> <template #label>{{ i18n.ts.notUseSound }}</template>
</MkSwitch>
<MkSwitch v-model="useSoundOnlyWhenActive">
<template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template>
</MkSwitch>
<MkRange v-model="masterVolume" style="margin-bottom: 25px;" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>{{ i18n.ts.masterVolume }}</template>
</MkRange>
</div>
<div class="_gaps_s"> <div class="_gaps_s">
<MkFolder v-for="type in soundsKeys" :key="type"> <MkFolder v-for="type in soundsKeys" :key="type">
@ -51,6 +59,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js'; import { unisonReload } from '@/scripts/unison-reload.js';
const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume')); const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
const soundsKeys = ['note', 'noteMy', 'noteEdited', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const; const soundsKeys = ['note', 'noteMy', 'noteEdited', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const;
@ -64,6 +74,7 @@ const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
chatBg: defaultStore.reactiveState.sound_chatBg, chatBg: defaultStore.reactiveState.sound_chatBg,
antenna: defaultStore.reactiveState.sound_antenna, antenna: defaultStore.reactiveState.sound_antenna,
channel: defaultStore.reactiveState.sound_channel, channel: defaultStore.reactiveState.sound_channel,
reaction: defaultStore.reactiveState.sound_reaction,
}); });
const vibrate = computed(defaultStore.makeGetterSetter('vibrate')); const vibrate = computed(defaultStore.makeGetterSetter('vibrate'));

View File

@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XPages v-else-if="tab === 'pages'" :user="user"/> <XPages v-else-if="tab === 'pages'" :user="user"/>
<XFlashs v-else-if="tab === 'flashs'" :user="user"/> <XFlashs v-else-if="tab === 'flashs'" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" :user="user"/> <XGallery v-else-if="tab === 'gallery'" :user="user"/>
<XRaw v-else-if="tab === 'raw'" :user="user"/>
</div> </div>
<MkError v-else-if="error" @retry="fetchUser()"/> <MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/> <MkLoading v-else/>
@ -56,6 +57,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
const XPages = defineAsyncComponent(() => import('./pages.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue'));
const XFlashs = defineAsyncComponent(() => import('./flashs.vue')); const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
acct: string; acct: string;
@ -124,6 +126,10 @@ const headerTabs = $computed(() => user ? [{
key: 'gallery', key: 'gallery',
title: i18n.ts.gallery, title: i18n.ts.gallery,
icon: 'ti ti-icons', icon: 'ti ti-icons',
}, {
key: 'raw',
title: 'Raw',
icon: 'ti ti-code',
}] : []); }] : []);
function menu(ev) { function menu(ev) {

View File

@ -0,0 +1,130 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
<div class="_gaps_m">
<div :class="$style.userMInfoRoot">
<MkAvatar :class="$style.userMInfoAvatar" :user="user" indicator link preview/>
<div :class="$style.userMInfoMetaRoot">
<span :class="$style.userMInfoMetaName"><MkUserName :class="$style.userMInfoMetaName" :user="user"/></span>
<span :class="$style.userMInfoMetaSub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
<span :class="$style.userMInfoMetaState">
<span v-if="suspended" :class="$style.suspended">Suspended</span>
<span v-if="silenced" :class="$style.silenced">Silenced</span>
<span v-if="moderator" :class="$style.moderator">Moderator</span>
</span>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="user.id" oneline>
<template #key>ID</template>
<template #value><span class="_monospace">{{ user.id }}</span></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
</MkKeyValue>
</div>
<FormSection>
<template #label>Raw</template>
<MkObjectView tall :value="user"></MkObjectView>
</FormSection>
</div>
</MkSpacer>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import * as Misskey from 'cherrypick-js';
import { acct } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSection from '@/components/form/section.vue';
import MkObjectView from '@/components/MkObjectView.vue';
const props = defineProps<{
user: Misskey.entities.User;
}>();
const moderator = computed(() => props.user.isModerator ?? false);
const silenced = computed(() => props.user.isSilenced ?? false);
const suspended = computed(() => props.user.isSuspended ?? false);
</script>
<style lang="scss" module>
.userMInfoRoot {
display: flex;
align-items: center;
}
.userMInfoAvatar {
display: block;
width: 64px;
height: 64px;
margin-right: 16px;
}
.userMInfoMetaRoot {
flex: 1;
overflow: hidden;
}
.userMInfoMetaName {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userMInfoMetaSub {
display: block;
width: 100%;
font-size: 85%;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userMInfoMetaState {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
&:empty {
display: none;
}
> .suspended,
> .silenced,
> .moderator {
display: inline-block;
border: solid 1px;
border-radius: 6px;
padding: 2px 6px;
font-size: 85%;
}
> .suspended {
color: var(--error);
border-color: var(--error);
}
> .silenced {
color: var(--warn);
border-color: var(--warn);
}
> .moderator {
color: var(--success);
border-color: var(--success);
}
}
</style>

View File

@ -0,0 +1,9 @@
export default async function hasAudio(media: HTMLMediaElement) {
const cloned = media.cloneNode() as HTMLMediaElement;
cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
cloned.play();
await new Promise((resolve) => cloned.addEventListener('playing', resolve));
const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount;
cloned.remove();
return result;
}

View File

@ -5,8 +5,9 @@
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const ctx = new AudioContext(); let ctx: AudioContext;
const cache = new Map<string, AudioBuffer>(); const cache = new Map<string, AudioBuffer>();
let canPlay = true;
export const soundsTypes = [ export const soundsTypes = [
null, null,
@ -38,6 +39,8 @@ export const soundsTypes = [
'syuilo/waon', 'syuilo/waon',
'syuilo/popo', 'syuilo/popo',
'syuilo/triple', 'syuilo/triple',
'syuilo/bubble1',
'syuilo/bubble2',
'syuilo/poi1', 'syuilo/poi1',
'syuilo/poi2', 'syuilo/poi2',
'syuilo/pirori', 'syuilo/pirori',
@ -62,6 +65,9 @@ export const soundsTypes = [
] as const; ] as const;
export async function loadAudio(file: string, useCache = true) { export async function loadAudio(file: string, useCache = true) {
if (ctx == null) {
ctx = new AudioContext();
}
if (useCache && cache.has(file)) { if (useCache && cache.has(file)) {
return cache.get(file)!; return cache.get(file)!;
} }
@ -77,11 +83,18 @@ export async function loadAudio(file: string, useCache = true) {
return audioBuffer; return audioBuffer;
} }
export function play(type: 'noteMy' | 'note' | 'noteEdited' | 'chat' | 'chatBg' | 'antenna' | 'channel' | 'notification') { export function play(type: 'noteMy' | 'note' | 'noteEdited' | 'chat' | 'chatBg' | 'antenna' | 'channel' | 'notification' | 'reaction') {
const sound = defaultStore.state[`sound_${type}`]; const sound = defaultStore.state[`sound_${type}`];
if (_DEV_) console.log('play', type, sound); if (_DEV_) console.log('play', type, sound);
if (sound.type == null) return; if (sound.type == null || !canPlay) return;
playFile(sound.type, sound.volume);
canPlay = false;
playFile(sound.type, sound.volume).then(() => {
// ごく短時間に音が重複しないように
setTimeout(() => {
canPlay = true;
}, 25);
});
} }
export async function playFile(file: string, volume: number) { export async function playFile(file: string, volume: number) {
@ -91,7 +104,7 @@ export async function playFile(file: string, volume: number) {
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
const masterVolume = defaultStore.state.sound_masterVolume; const masterVolume = defaultStore.state.sound_masterVolume;
if (masterVolume === 0 || volume === 0) { if (isMute() || masterVolume === 0 || volume === 0) {
return null; return null;
} }
@ -104,3 +117,18 @@ export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBuf
return soundSource; return soundSource;
} }
export function isMute(): boolean {
if (defaultStore.state.sound_notUseSound) {
// サウンドを出力しない
return true;
}
// noinspection RedundantIfStatementJS
if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') {
// ブラウザがアクティブな時のみサウンドを出力する
return true;
}
return false;
}

View File

@ -412,6 +412,14 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: 0.3, default: 0.3,
}, },
sound_notUseSound: {
where: 'device',
default: false,
},
sound_useSoundOnlyWhenActive: {
where: 'device',
default: false,
},
sound_note: { sound_note: {
where: 'device', where: 'device',
default: { type: 'syuilo/n-aec', volume: 1 }, default: { type: 'syuilo/n-aec', volume: 1 },
@ -444,6 +452,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: { type: 'syuilo/square-pico', volume: 1 }, default: { type: 'syuilo/square-pico', volume: 1 },
}, },
sound_reaction: {
where: 'device',
default: { type: 'syuilo/bubble2', volume: 1 },
},
// #region CherryPick // #region CherryPick
// - Settings/General // - Settings/General

View File

@ -58,6 +58,7 @@ import { useStream } from '@/stream.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
import { defaultStore } from '@/store.js';
const name = 'jobQueue'; const name = 'jobQueue';
@ -102,7 +103,9 @@ const prev = reactive({} as typeof current);
let jammedAudioBuffer: AudioBuffer | null = $ref(null); let jammedAudioBuffer: AudioBuffer | null = $ref(null);
let jammedSoundNodePlaying: boolean = $ref(false); let jammedSoundNodePlaying: boolean = $ref(false);
sound.loadAudio('syuilo/queue-jammed').then(buf => jammedAudioBuffer = buf); if (defaultStore.state.sound_masterVolume) {
sound.loadAudio('syuilo/queue-jammed').then(buf => jammedAudioBuffer = buf);
}
for (const domain of ['inbox', 'deliver']) { for (const domain of ['inbox', 'deliver']) {
prev[domain] = deepClone(current[domain]); prev[domain] = deepClone(current[domain]);

View File

@ -25,3 +25,21 @@ vi.mock('@/store.js', () => {
}, },
}; };
}); });
// Add mocks for Web Audio API
const AudioNodeMock = vi.fn(() => ({
connect: vi.fn(() => ({ connect: vi.fn() })),
start: vi.fn(),
}));
const GainNodeMock = vi.fn(() => ({
gain: vi.fn(),
}));
const AudioContextMock = vi.fn(() => ({
createBufferSource: vi.fn(() => new AudioNodeMock()),
createGain: vi.fn(() => new GainNodeMock()),
decodeAudioData: vi.fn(),
}));
vi.stubGlobal('AudioContext', AudioContextMock);