Merge remote-tracking branch 'upstream/develop' into feat-decoration-url-from-drive
This commit is contained in:
commit
5daec271e2
|
|
@ -8,7 +8,7 @@
|
|||
"version": "8.9.2"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "20.5.1"
|
||||
"version": "20.10.0"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.5.1]
|
||||
node-version: [20.10.0]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.5.1]
|
||||
node-version: [20.10.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
20.5.1
|
||||
20.10.0
|
||||
|
|
|
|||
|
|
@ -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: プラグインでノートの表示を書き換えられない問題を修正
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
||||
manual: "マニュアル"
|
||||
manualRoles: "マニュアルロール"
|
||||
conditional: "コンディショナル"
|
||||
conditionalRoles: "コンディショナルロール"
|
||||
condition: "条件"
|
||||
isConditionalRole: "これはコンディショナルロールです。"
|
||||
isPublic: "公開ロール"
|
||||
|
|
@ -1845,6 +1850,7 @@ _sfx:
|
|||
notification: "通知"
|
||||
antenna: "アンテナ受信"
|
||||
channel: "チャンネル通知"
|
||||
reaction: "リアクション選択時"
|
||||
|
||||
_ago:
|
||||
future: "未来"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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<void> {
|
||||
return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
|
||||
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<MiGalleryPost['id'][]> {
|
||||
return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
|
||||
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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 [];
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<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);
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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);
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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);
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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[] = [];
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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[] = [];
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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) {
|
||||
|
|
|
|||
|
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // eslint-
|
|||
]) : [new Set<string>()];
|
||||
|
||||
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([
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Mfm :text="report.comment"/>
|
||||
</div>
|
||||
<hr/>
|
||||
<div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div>
|
||||
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div>
|
||||
<div v-if="report.assignee">
|
||||
{{ i18n.ts.moderator }}:
|
||||
<MkAcct :user="report.assignee"/>
|
||||
|
|
|
|||
|
|
@ -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<string, EmojiScore>();
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HTMLVideoElement>();
|
|||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()"
|
||||
>
|
||||
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||
<span :class="$style.count">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -198,13 +198,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
||||
<div class="_gaps_s">
|
||||
<MkFoldableSection>
|
||||
<template #header>Manual roles</template>
|
||||
<template #header>{{ i18n.ts._role.manualRoles }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection>
|
||||
<template #header>Conditional roles</template>
|
||||
<template #header>{{ i18n.ts._role.conditionalRoles }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="medium">{{ i18n.ts.medium }}</option>
|
||||
<option value="large">{{ i18n.ts.large }}</option>
|
||||
</MkRadios>
|
||||
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
|
||||
</div>
|
||||
|
||||
<MkSelect v-model="instanceTicker">
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="notUseSound">
|
||||
<template #label>{{ i18n.ts.notUseSound }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="useSoundOnlyWhenActive">
|
||||
<template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
|
||||
<template #label>{{ i18n.ts.masterVolume }}</template>
|
||||
</MkRange>
|
||||
|
|
@ -35,10 +41,13 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
||||
const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
|
||||
const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
|
||||
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
|
||||
|
||||
const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const;
|
||||
const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel', 'reaction'] as const;
|
||||
|
||||
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
|
||||
note: defaultStore.reactiveState.sound_note,
|
||||
|
|
@ -46,6 +55,7 @@ const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
|
|||
notification: defaultStore.reactiveState.sound_notification,
|
||||
antenna: defaultStore.reactiveState.sound_antenna,
|
||||
channel: defaultStore.reactiveState.sound_channel,
|
||||
reaction: defaultStore.reactiveState.sound_reaction,
|
||||
});
|
||||
|
||||
async function updated(type: keyof typeof sounds.value, sound) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XPages v-else-if="tab === 'pages'" :user="user"/>
|
||||
<XFlashs v-else-if="tab === 'flashs'" :user="user"/>
|
||||
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
|
||||
<XRaw v-else-if="tab === 'raw'" :user="user"/>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchUser()"/>
|
||||
<MkLoading v-else/>
|
||||
|
|
@ -44,6 +45,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
|
|||
const XPages = defineAsyncComponent(() => import('./pages.vue'));
|
||||
const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
|
||||
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
||||
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
acct: string;
|
||||
|
|
@ -112,6 +114,10 @@ const headerTabs = $computed(() => user ? [{
|
|||
key: 'gallery',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'ti ti-icons',
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw',
|
||||
icon: 'ti ti-code',
|
||||
}] : []);
|
||||
|
||||
definePageMetadata(computed(() => user ? {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey 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 'misskey-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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -5,8 +5,9 @@
|
|||
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const ctx = new AudioContext();
|
||||
let ctx: AudioContext;
|
||||
const cache = new Map<string, AudioBuffer>();
|
||||
let canPlay = true;
|
||||
|
||||
export const soundsTypes = [
|
||||
null,
|
||||
|
|
@ -38,6 +39,8 @@ export const soundsTypes = [
|
|||
'syuilo/waon',
|
||||
'syuilo/popo',
|
||||
'syuilo/triple',
|
||||
'syuilo/bubble1',
|
||||
'syuilo/bubble2',
|
||||
'syuilo/poi1',
|
||||
'syuilo/poi2',
|
||||
'syuilo/pirori',
|
||||
|
|
@ -62,6 +65,9 @@ export const soundsTypes = [
|
|||
] as const;
|
||||
|
||||
export async function loadAudio(file: string, useCache = true) {
|
||||
if (ctx == null) {
|
||||
ctx = new AudioContext();
|
||||
}
|
||||
if (useCache && cache.has(file)) {
|
||||
return cache.get(file)!;
|
||||
}
|
||||
|
|
@ -77,11 +83,18 @@ export async function loadAudio(file: string, useCache = true) {
|
|||
return audioBuffer;
|
||||
}
|
||||
|
||||
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
|
||||
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification' | 'reaction') {
|
||||
const sound = defaultStore.state[`sound_${type}`];
|
||||
if (_DEV_) console.log('play', type, sound);
|
||||
if (sound.type == null) return;
|
||||
playFile(sound.type, sound.volume);
|
||||
if (sound.type == null || !canPlay) return;
|
||||
|
||||
canPlay = false;
|
||||
playFile(sound.type, sound.volume).then(() => {
|
||||
// ごく短時間に音が重複しないように
|
||||
setTimeout(() => {
|
||||
canPlay = true;
|
||||
}, 25);
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
if (masterVolume === 0 || volume === 0) {
|
||||
if (isMute() || masterVolume === 0 || volume === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -104,3 +117,18 @@ export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBuf
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,6 +330,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: 'medium' as 'small' | 'medium' | 'large',
|
||||
},
|
||||
limitWidthOfReaction: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
forceShowAds: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
|
|
@ -387,6 +391,14 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: 0.3,
|
||||
},
|
||||
sound_notUseSound: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
sound_useSoundOnlyWhenActive: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
sound_note: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-aec', volume: 1 },
|
||||
|
|
@ -407,6 +419,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: { type: 'syuilo/square-pico', volume: 1 },
|
||||
},
|
||||
sound_reaction: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/bubble2', volume: 1 },
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import { useStream } from '@/stream.js';
|
|||
import number from '@/filters/number.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const name = 'jobQueue';
|
||||
|
||||
|
|
@ -102,7 +103,9 @@ const prev = reactive({} as typeof current);
|
|||
let jammedAudioBuffer: AudioBuffer | null = $ref(null);
|
||||
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']) {
|
||||
prev[domain] = deepClone(current[domain]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue