Compare commits
10 Commits
7022c2f912
...
deaf864470
Author | SHA1 | Date | |
---|---|---|---|
|
deaf864470 | ||
|
6d231c4bc9 | ||
|
a313fc6366 | ||
|
759109e7a1 | ||
|
da27431d14 | ||
|
6072b8744f | ||
|
927111fc0d | ||
|
455cce801d | ||
|
36657836f7 | ||
|
10454e688c |
@ -55,6 +55,11 @@
|
|||||||
* ノートの自動削除(cherry-pick)
|
* ノートの自動削除(cherry-pick)
|
||||||
* フォローリクエスト自動拒否(cherry-pick)
|
* フォローリクエスト自動拒否(cherry-pick)
|
||||||
|
|
||||||
|
## 2024.8.0-yami-1.2.0
|
||||||
|
### Feat
|
||||||
|
* ノートの自動削除(cherry-pick)
|
||||||
|
* フォローリクエスト自動拒否(cherry-pick)
|
||||||
|
|
||||||
## 2024.8.0-yami-1.1.0
|
## 2024.8.0-yami-1.1.0
|
||||||
### Server
|
### Server
|
||||||
* Cherry-Pick リバーシの連合に対応(yojo-art/cherrypick)
|
* Cherry-Pick リバーシの連合に対応(yojo-art/cherrypick)
|
||||||
|
@ -714,6 +714,8 @@ notificationDotWorking: "The notification dot is functioning properly on this in
|
|||||||
notificationDotNotWorkingAdvice: "If the notification dot doesn't work, ask an admin to check our documentation {link}"
|
notificationDotNotWorkingAdvice: "If the notification dot doesn't work, ask an admin to check our documentation {link}"
|
||||||
useGlobalSetting: "Use global settings"
|
useGlobalSetting: "Use global settings"
|
||||||
useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made."
|
useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made."
|
||||||
|
autoRejectFollowRequest: "Automatic rejection of follow requests"
|
||||||
|
autoRejectFollowRequestDescription: "Enables automatic rejection of follow requests. If \"Automatically approve follow requests from users you are following\" is turned on, follow requests from users you are following will be automatically approved, and follow requests from other users will be automatically rejected."
|
||||||
other: "Other"
|
other: "Other"
|
||||||
regenerateLoginToken: "Regenerate login token"
|
regenerateLoginToken: "Regenerate login token"
|
||||||
regenerateLoginTokenDescription: "Regenerates the token used internally during login. Normally this action is not necessary. If regenerated, all devices will be logged out."
|
regenerateLoginTokenDescription: "Regenerates the token used internally during login. Normally this action is not necessary. If regenerated, all devices will be logged out."
|
||||||
@ -1336,6 +1338,9 @@ _delivery:
|
|||||||
manuallySuspended: "Manually suspended"
|
manuallySuspended: "Manually suspended"
|
||||||
goneSuspended: "Server is suspended due to server deletion"
|
goneSuspended: "Server is suspended due to server deletion"
|
||||||
autoSuspendedForNotResponding: "Server is suspended due to no responding"
|
autoSuspendedForNotResponding: "Server is suspended due to no responding"
|
||||||
|
scheduledNoteDelete: "Time Bomb"
|
||||||
|
noteDeletationAt: "This note will be deleted at {time}"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "How to play"
|
howToPlay: "How to play"
|
||||||
hold: "Hold"
|
hold: "Hold"
|
||||||
@ -2819,3 +2824,6 @@ _contextMenu:
|
|||||||
app: "Application"
|
app: "Application"
|
||||||
appWithShift: "Application with shift key"
|
appWithShift: "Application with shift key"
|
||||||
native: "Native"
|
native: "Native"
|
||||||
|
_reactionChecksMuting:
|
||||||
|
title: "Check mutings when get reactions"
|
||||||
|
caption: "Check mutings when get reactions, but cache does not work and may increase traffic"
|
||||||
|
24
locales/index.d.ts
vendored
24
locales/index.d.ts
vendored
@ -4113,6 +4113,10 @@ export interface Locale extends ILocale {
|
|||||||
* ベータ
|
* ベータ
|
||||||
*/
|
*/
|
||||||
"beta": string;
|
"beta": string;
|
||||||
|
/**
|
||||||
|
* 独自機能
|
||||||
|
*/
|
||||||
|
"originalFeature": string;
|
||||||
/**
|
/**
|
||||||
* 自動センシティブ判定
|
* 自動センシティブ判定
|
||||||
*/
|
*/
|
||||||
@ -5266,6 +5270,14 @@ export interface Locale extends ILocale {
|
|||||||
*/
|
*/
|
||||||
"gameRetry": string;
|
"gameRetry": string;
|
||||||
/**
|
/**
|
||||||
|
* 時限爆弾
|
||||||
|
*/
|
||||||
|
"scheduledNoteDelete": string;
|
||||||
|
/**
|
||||||
|
* このノートは{time}に消去されます
|
||||||
|
*/
|
||||||
|
"noteDeletationAt": ParameterizedString<"time">;
|
||||||
|
/**
|
||||||
* 使用しない場合は空欄にしてください
|
* 使用しない場合は空欄にしてください
|
||||||
*/
|
*/
|
||||||
"notUsePleaseLeaveBlank": string;
|
"notUsePleaseLeaveBlank": string;
|
||||||
@ -5411,6 +5423,8 @@ export interface Locale extends ILocale {
|
|||||||
"section3": string;
|
"section3": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
"autoRejectFollowRequest": string;
|
||||||
|
"autoRejectFollowRequestDescription": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
/**
|
/**
|
||||||
* 既存ユーザーのみ
|
* 既存ユーザーのみ
|
||||||
@ -10881,6 +10895,16 @@ export interface Locale extends ILocale {
|
|||||||
*/
|
*/
|
||||||
"native": string;
|
"native": string;
|
||||||
};
|
};
|
||||||
|
"_reactionChecksMuting": {
|
||||||
|
/**
|
||||||
|
* リアクションでミュートを考慮する
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* リアクションがミュートを考慮しますが、キャッシュが効かず通信量が増えることがあります。
|
||||||
|
*/
|
||||||
|
"caption": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
@ -1024,6 +1024,7 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
|
|||||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
||||||
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
|
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
|
||||||
beta: "ベータ"
|
beta: "ベータ"
|
||||||
|
originalFeature: "独自機能"
|
||||||
enableAutoSensitive: "自動センシティブ判定"
|
enableAutoSensitive: "自動センシティブ判定"
|
||||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
||||||
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
||||||
@ -1337,6 +1338,8 @@ _delivery:
|
|||||||
manuallySuspended: "手動停止中"
|
manuallySuspended: "手動停止中"
|
||||||
goneSuspended: "サーバー削除のため停止中"
|
goneSuspended: "サーバー削除のため停止中"
|
||||||
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
|
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
|
||||||
|
scheduledNoteDelete: "時限爆弾"
|
||||||
|
noteDeletationAt: "このノートは{time}に削除されます"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
@ -1353,6 +1356,8 @@ _bubbleGame:
|
|||||||
section1: "位置を調整してハコにモノを落とします。"
|
section1: "位置を調整してハコにモノを落とします。"
|
||||||
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
|
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
|
||||||
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
|
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
|
||||||
|
autoRejectFollowRequest: "フォローリクエストを自動で拒否する"
|
||||||
|
autoRejectFollowRequestDescription: "フォローリクエストを自動で拒否するようにします。「フォロー中ユーザーからのフォロリクを自動承認」がONになっている場合は、フォロー中ユーザーからのフォローリクエストは自動的に承認され、それ以外のユーザーからのフォローリクエストは自動的に拒否されるようになります。"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
@ -2894,3 +2899,7 @@ _contextMenu:
|
|||||||
app: "アプリケーション"
|
app: "アプリケーション"
|
||||||
appWithShift: "Shiftキーでアプリケーション"
|
appWithShift: "Shiftキーでアプリケーション"
|
||||||
native: "ブラウザのUI"
|
native: "ブラウザのUI"
|
||||||
|
|
||||||
|
_reactionChecksMuting:
|
||||||
|
title: "リアクションでミュートを考慮する"
|
||||||
|
caption: "リアクションがミュートを考慮しますが、キャッシュが効かず通信量が増えることがあります。"
|
||||||
|
@ -693,6 +693,8 @@ notificationSetting: "通知設定"
|
|||||||
notificationSettingDesc: "出す通知の種類えらんでや。"
|
notificationSettingDesc: "出す通知の種類えらんでや。"
|
||||||
useGlobalSetting: "グローバル設定を使ってや"
|
useGlobalSetting: "グローバル設定を使ってや"
|
||||||
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使われるで。オフにすると、別々に設定できるようになるで。"
|
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使われるで。オフにすると、別々に設定できるようになるで。"
|
||||||
|
autoRejectFollowRequest: "フォローリクエストを自動で拒否するで"
|
||||||
|
autoRejectFollowRequestDescription: "フォローリクエストを自動で拒否するようになるで。「フォロー中ユーザーからのフォロリクを自動承認」がONになってはると、フォロー中ユーザーからフォローリクエストが自動で承認されはり、それ以外のユーザーからのフォローリクエストは自動的に拒否されるで。"
|
||||||
other: "その他"
|
other: "その他"
|
||||||
regenerateLoginToken: "ログイントークンを再生成"
|
regenerateLoginToken: "ログイントークンを再生成"
|
||||||
regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。"
|
regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。"
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
export class FeatAutoRejectFollowRequest1697683129062 {
|
||||||
|
name = 'FeatAutoRejectFollowRequest1697683129062'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoRejectFollowRequest" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoRejectFollowRequest"`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
export class ScheduledNoteDelete1709187210308 {
|
||||||
|
name = 'ScheduledNoteDelete1709187210308'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" ADD "deleteAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "deleteAt"`);
|
||||||
|
}
|
||||||
|
}
|
@ -62,6 +62,7 @@ import { isReply } from '@/misc/is-reply.js';
|
|||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { Data } from 'ws';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@ -148,6 +149,7 @@ type Option = {
|
|||||||
uri?: string | null;
|
uri?: string | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
app?: MiApp | null;
|
app?: MiApp | null;
|
||||||
|
deleteAt?: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -439,6 +441,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
text: data.text,
|
text: data.text,
|
||||||
hasPoll: data.poll != null,
|
hasPoll: data.poll != null,
|
||||||
|
deleteAt: data.deleteAt,
|
||||||
cw: data.cw ?? null,
|
cw: data.cw ?? null,
|
||||||
tags: tags.map(tag => normalizeForSearch(tag)),
|
tags: tags.map(tag => normalizeForSearch(tag)),
|
||||||
emojis,
|
emojis,
|
||||||
@ -614,6 +617,16 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.deleteAt) {
|
||||||
|
const delay = data.deleteAt.getTime() - Date.now();
|
||||||
|
this.queueService.scheduledNoteDeleteQueue.add(note.id, {
|
||||||
|
noteId: note.id
|
||||||
|
}, {
|
||||||
|
delay,
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
|
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
|
||||||
|
|
||||||
|
@ -195,7 +195,9 @@ export class QueryService {
|
|||||||
qb
|
qb
|
||||||
.where('note.visibility = \'public\'')
|
.where('note.visibility = \'public\'')
|
||||||
.orWhere('note.visibility = \'home\'');
|
.orWhere('note.visibility = \'home\'');
|
||||||
}));
|
})
|
||||||
|
.andWhere('note.localOnly = FALSE') // 連合なしのノートは未ログイン者には見せない
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||||
.select('following.followeeId')
|
.select('following.followeeId')
|
||||||
|
@ -21,6 +21,7 @@ import type { Provider } from '@nestjs/common';
|
|||||||
|
|
||||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||||
|
export type ScheduledNoteDeleteQueue = Bull.Queue<ScheduledNoteDeleteJobData>
|
||||||
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
||||||
export type InboxQueue = Bull.Queue<InboxJobData>;
|
export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||||
export type DbQueue = Bull.Queue;
|
export type DbQueue = Bull.Queue;
|
||||||
@ -41,6 +42,12 @@ const $endedPollNotification: Provider = {
|
|||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $scheduledNoteDeleted: Provider = {
|
||||||
|
provide: 'queue:scheduledNoteDelete',
|
||||||
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULED_NOTE_DELETE, baseQueueOptions(config, QUEUE.SCHEDULED_NOTE_DELETE)),
|
||||||
|
inject: [DI.config],
|
||||||
|
};
|
||||||
|
|
||||||
const $deliver: Provider = {
|
const $deliver: Provider = {
|
||||||
provide: 'queue:deliver',
|
provide: 'queue:deliver',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
||||||
@ -89,6 +96,7 @@ const $systemWebhookDeliver: Provider = {
|
|||||||
providers: [
|
providers: [
|
||||||
$system,
|
$system,
|
||||||
$endedPollNotification,
|
$endedPollNotification,
|
||||||
|
$scheduledNoteDeleted,
|
||||||
$deliver,
|
$deliver,
|
||||||
$inbox,
|
$inbox,
|
||||||
$db,
|
$db,
|
||||||
@ -100,6 +108,7 @@ const $systemWebhookDeliver: Provider = {
|
|||||||
exports: [
|
exports: [
|
||||||
$system,
|
$system,
|
||||||
$endedPollNotification,
|
$endedPollNotification,
|
||||||
|
$scheduledNoteDeleted,
|
||||||
$deliver,
|
$deliver,
|
||||||
$inbox,
|
$inbox,
|
||||||
$db,
|
$db,
|
||||||
@ -113,6 +122,7 @@ export class QueueModule implements OnApplicationShutdown {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||||
|
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
|
||||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@ -129,6 +139,7 @@ export class QueueModule implements OnApplicationShutdown {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.systemQueue.close(),
|
this.systemQueue.close(),
|
||||||
this.endedPollNotificationQueue.close(),
|
this.endedPollNotificationQueue.close(),
|
||||||
|
this.scheduledNoteDeleteQueue.close(),
|
||||||
this.deliverQueue.close(),
|
this.deliverQueue.close(),
|
||||||
this.inboxQueue.close(),
|
this.inboxQueue.close(),
|
||||||
this.dbQueue.close(),
|
this.dbQueue.close(),
|
||||||
|
@ -45,6 +45,7 @@ export class QueueService {
|
|||||||
|
|
||||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||||
|
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
|
||||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
|
@ -214,6 +214,20 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!autoAccept) {
|
if (!autoAccept) {
|
||||||
|
// autoAcceptが無効かつautoRejectが有効な場合はフォローリクエストを拒否する
|
||||||
|
if (this.userEntityService.isLocalUser(followee) && followeeProfile.autoRejectFollowRequest) {
|
||||||
|
if (this.userEntityService.isRemoteUser(follower)) {
|
||||||
|
// リモートからならRejectを返す
|
||||||
|
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||||
|
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ローカルユーザーに対しては敢えてpublishUnfollowせずにお茶を濁す
|
||||||
|
// フォローできない不具合と勘違いされたりフォローリクエストを連打される可能性があるため
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.createFollowRequest(follower, followee, requestId, withReplies);
|
await this.createFollowRequest(follower, followee, requestId, withReplies);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -579,7 +593,9 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!requestExist) {
|
if (!requestExist) {
|
||||||
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
// throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||||||
|
// 本来ならエラーを返すが、フォローリクエストの自動拒否機能の関係上、エラーを返さずに無視する
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.followRequestsRepository.delete({
|
await this.followRequestsRepository.delete({
|
||||||
|
@ -97,6 +97,11 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 連合なしで未ログインなら非表示
|
||||||
|
if(packedNote.localOnly && !meId){
|
||||||
|
hide = true;
|
||||||
|
}
|
||||||
|
|
||||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||||
if (packedNote.visibility === 'followers') {
|
if (packedNote.visibility === 'followers') {
|
||||||
if (meId == null) {
|
if (meId == null) {
|
||||||
@ -241,6 +246,11 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 連合なし、かつ visibility が home で未ログインなら非表示
|
||||||
|
if(note.localOnly && note.visibility === 'home' && !meId){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||||
if (note.visibility === 'followers') {
|
if (note.visibility === 'followers') {
|
||||||
if (meId == null) {
|
if (meId == null) {
|
||||||
@ -395,6 +405,13 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||||
_hint_: options?._hint_,
|
_hint_: options?._hint_,
|
||||||
}) : undefined,
|
}) : undefined,
|
||||||
|
|
||||||
|
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||||
|
deleteAt: note.deleteAt?.toISOString() ?? undefined,
|
||||||
|
|
||||||
|
...(meId && Object.keys(note.reactions).length > 0 ? {
|
||||||
|
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||||
|
} : {}),
|
||||||
} : {}),
|
} : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -620,6 +620,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
autoSensitive: profile!.autoSensitive,
|
autoSensitive: profile!.autoSensitive,
|
||||||
carefulBot: profile!.carefulBot,
|
carefulBot: profile!.carefulBot,
|
||||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
||||||
|
autoRejectFollowRequest: profile!.autoRejectFollowRequest,
|
||||||
noCrawle: profile!.noCrawle,
|
noCrawle: profile!.noCrawle,
|
||||||
preventAiLearning: profile!.preventAiLearning,
|
preventAiLearning: profile!.preventAiLearning,
|
||||||
isExplorable: user.isExplorable,
|
isExplorable: user.isExplorable,
|
||||||
|
@ -188,6 +188,11 @@ export class MiNote {
|
|||||||
})
|
})
|
||||||
public hasPoll: boolean;
|
public hasPoll: boolean;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public deleteAt: Date | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
|
@ -172,6 +172,11 @@ export class MiUserProfile {
|
|||||||
})
|
})
|
||||||
public autoAcceptFollowed: boolean;
|
public autoAcceptFollowed: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public autoRejectFollowRequest: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
comment: 'Whether reject index by crawler.',
|
comment: 'Whether reject index by crawler.',
|
||||||
|
@ -152,6 +152,11 @@ export const packedNoteSchema = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deleteAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
emojis: {
|
emojis: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
@ -41,6 +41,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
|
|||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||||
|
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
|||||||
UserWebhookDeliverProcessorService,
|
UserWebhookDeliverProcessorService,
|
||||||
SystemWebhookDeliverProcessorService,
|
SystemWebhookDeliverProcessorService,
|
||||||
EndedPollNotificationProcessorService,
|
EndedPollNotificationProcessorService,
|
||||||
|
ScheduledNoteDeleteProcessorService,
|
||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
InboxProcessorService,
|
InboxProcessorService,
|
||||||
AggregateRetentionProcessorService,
|
AggregateRetentionProcessorService,
|
||||||
|
@ -45,6 +45,7 @@ import { AggregateRetentionProcessorService } from './processors/AggregateRetent
|
|||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
import { QUEUE, baseQueueOptions } from './const.js';
|
import { QUEUE, baseQueueOptions } from './const.js';
|
||||||
import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js';
|
import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js';
|
||||||
|
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js';
|
||||||
|
|
||||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||||
function httpRelatedBackoff(attemptsMade: number) {
|
function httpRelatedBackoff(attemptsMade: number) {
|
||||||
@ -84,6 +85,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
private relationshipQueueWorker: Bull.Worker;
|
private relationshipQueueWorker: Bull.Worker;
|
||||||
private objectStorageQueueWorker: Bull.Worker;
|
private objectStorageQueueWorker: Bull.Worker;
|
||||||
private endedPollNotificationQueueWorker: Bull.Worker;
|
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||||
|
private scheduledNoteDeleteQueueWorker: Bull.Worker;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
@ -93,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||||
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||||
|
private scheduledNoteDeleteProcessorService: ScheduledNoteDeleteProcessorService,
|
||||||
private deliverProcessorService: DeliverProcessorService,
|
private deliverProcessorService: DeliverProcessorService,
|
||||||
private inboxProcessorService: InboxProcessorService,
|
private inboxProcessorService: InboxProcessorService,
|
||||||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||||
@ -513,6 +516,12 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region scheduled note delete
|
||||||
|
this.scheduledNoteDeleteQueueWorker = new Bull.Worker(QUEUE.SCHEDULED_NOTE_DELETE, (job) => this.scheduledNoteDeleteProcessorService.process(job), {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.SCHEDULED_NOTE_DELETE),
|
||||||
|
autorun: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@ -527,6 +536,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
this.relationshipQueueWorker.run(),
|
this.relationshipQueueWorker.run(),
|
||||||
this.objectStorageQueueWorker.run(),
|
this.objectStorageQueueWorker.run(),
|
||||||
this.endedPollNotificationQueueWorker.run(),
|
this.endedPollNotificationQueueWorker.run(),
|
||||||
|
this.scheduledNoteDeleteQueueWorker.run(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
this.relationshipQueueWorker.close(),
|
this.relationshipQueueWorker.close(),
|
||||||
this.objectStorageQueueWorker.close(),
|
this.objectStorageQueueWorker.close(),
|
||||||
this.endedPollNotificationQueueWorker.close(),
|
this.endedPollNotificationQueueWorker.close(),
|
||||||
|
this.scheduledNoteDeleteQueueWorker.close(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ export const QUEUE = {
|
|||||||
INBOX: 'inbox',
|
INBOX: 'inbox',
|
||||||
SYSTEM: 'system',
|
SYSTEM: 'system',
|
||||||
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
||||||
|
SCHEDULED_NOTE_DELETE: 'scheduledNoteDelete',
|
||||||
DB: 'db',
|
DB: 'db',
|
||||||
RELATIONSHIP: 'relationship',
|
RELATIONSHIP: 'relationship',
|
||||||
OBJECT_STORAGE: 'objectStorage',
|
OBJECT_STORAGE: 'objectStorage',
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { NotesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { ScheduledNoteDeleteJobData } from '../types.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ScheduledNoteDeleteProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
private noteDeleteService: NoteDeleteService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('scheduled-note-delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(job: Bull.Job<ScheduledNoteDeleteJobData>): Promise<void> {
|
||||||
|
const note = await this.notesRepository.findOneBy({ id: job.data.noteId });
|
||||||
|
if (note == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.noteDeleteService.delete(user, note);
|
||||||
|
this.logger.info(`Deleted note ${note.id}`);
|
||||||
|
}
|
||||||
|
}
|
@ -131,6 +131,10 @@ export type EndedPollNotificationJobData = {
|
|||||||
noteId: MiNote['id'];
|
noteId: MiNote['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScheduledNoteDeleteJobData = {
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
}
|
||||||
|
|
||||||
export type SystemWebhookDeliverJobData = {
|
export type SystemWebhookDeliverJobData = {
|
||||||
type: string;
|
type: string;
|
||||||
content: unknown;
|
content: unknown;
|
||||||
|
@ -183,6 +183,7 @@ export const paramDef = {
|
|||||||
publicReactions: { type: 'boolean' },
|
publicReactions: { type: 'boolean' },
|
||||||
carefulBot: { type: 'boolean' },
|
carefulBot: { type: 'boolean' },
|
||||||
autoAcceptFollowed: { type: 'boolean' },
|
autoAcceptFollowed: { type: 'boolean' },
|
||||||
|
autoRejectFollowRequest: { type: 'boolean' },
|
||||||
noCrawle: { type: 'boolean' },
|
noCrawle: { type: 'boolean' },
|
||||||
preventAiLearning: { type: 'boolean' },
|
preventAiLearning: { type: 'boolean' },
|
||||||
noindex: { type: 'boolean' },
|
noindex: { type: 'boolean' },
|
||||||
@ -337,6 +338,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
|
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
|
||||||
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
||||||
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
||||||
|
if (typeof ps.autoRejectFollowRequest === 'boolean') profileUpdates.autoRejectFollowRequest = ps.autoRejectFollowRequest;
|
||||||
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
||||||
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
||||||
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
|
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
|
||||||
|
@ -104,6 +104,12 @@ export const meta = {
|
|||||||
id: '04da457d-b083-4055-9082-955525eda5a5',
|
id: '04da457d-b083-4055-9082-955525eda5a5',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cannotScheduleDeleteEarlierThanNow: {
|
||||||
|
message: 'Scheduled delete time is earlier than now.',
|
||||||
|
code: 'CANNOT_SCHEDULE_DELETE_EARLIER_THAN_NOW',
|
||||||
|
id: '9576c3c8-d8f3-11ee-ac15-00155d19d35d',
|
||||||
|
},
|
||||||
|
|
||||||
noSuchChannel: {
|
noSuchChannel: {
|
||||||
message: 'No such channel.',
|
message: 'No such channel.',
|
||||||
code: 'NO_SUCH_CHANNEL',
|
code: 'NO_SUCH_CHANNEL',
|
||||||
@ -197,6 +203,14 @@ export const paramDef = {
|
|||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
scheduledDelete: {
|
||||||
|
type: 'object',
|
||||||
|
nullable: true,
|
||||||
|
properties: {
|
||||||
|
deleteAt: { type: 'integer', nullable: true },
|
||||||
|
deleteAfter: { type: 'integer', nullable: true, minimum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// (re)note with text, files and poll are optional
|
// (re)note with text, files and poll are optional
|
||||||
if: {
|
if: {
|
||||||
@ -365,6 +379,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.scheduledDelete) {
|
||||||
|
if (typeof ps.scheduledDelete.deleteAt === 'number') {
|
||||||
|
if (ps.scheduledDelete.deleteAt < Date.now()) {
|
||||||
|
throw new ApiError(meta.errors.cannotScheduleDeleteEarlierThanNow);
|
||||||
|
}
|
||||||
|
} else if (typeof ps.scheduledDelete.deleteAfter === 'number') {
|
||||||
|
ps.scheduledDelete.deleteAt = Date.now() + ps.scheduledDelete.deleteAfter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let channel: MiChannel | null = null;
|
let channel: MiChannel | null = null;
|
||||||
if (ps.channelId != null) {
|
if (ps.channelId != null) {
|
||||||
channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
|
channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
|
||||||
@ -396,6 +420,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||||
|
deleteAt: ps.scheduledDelete?.deleteAt ? new Date(ps.scheduledDelete.deleteAt) : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -4,13 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Brackets, type FindOptionsWhere } from 'typeorm';
|
|
||||||
import type { NoteReactionsRepository } from '@/models/_.js';
|
import type { NoteReactionsRepository } from '@/models/_.js';
|
||||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
|
import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes', 'reactions'],
|
tags: ['notes', 'reactions'],
|
||||||
@ -59,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
private noteReactionEntityService: NoteReactionEntityService,
|
private noteReactionEntityService: NoteReactionEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId)
|
||||||
@ -66,6 +66,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
.leftJoinAndSelect('reaction.user', 'user')
|
.leftJoinAndSelect('reaction.user', 'user')
|
||||||
.leftJoinAndSelect('reaction.note', 'note');
|
.leftJoinAndSelect('reaction.note', 'note');
|
||||||
|
|
||||||
|
if (me != null) {
|
||||||
|
const [userIdsWhoMeMuting, userIdsWhoBlockingMe] = await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.get(me.id),
|
||||||
|
this.cacheService.userBlockedCache.get(me.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
query.andWhere('reaction.userId NOT IN (:...userIds)', { userIds: Array.from(userIdsWhoMeMuting ?? []).concat(Array.from(userIdsWhoBlockingMe ?? [])) });
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.type) {
|
if (ps.type) {
|
||||||
// ローカルリアクションはホスト名が . とされているが
|
// ローカルリアクションはホスト名が . とされているが
|
||||||
// DB 上ではそうではないので、必要に応じて変換
|
// DB 上ではそうではないので、必要に応じて変換
|
||||||
|
@ -30,7 +30,7 @@ import type {
|
|||||||
DeliverQueue,
|
DeliverQueue,
|
||||||
EndedPollNotificationQueue,
|
EndedPollNotificationQueue,
|
||||||
InboxQueue,
|
InboxQueue,
|
||||||
ObjectStorageQueue,
|
ObjectStorageQueue, ScheduledNoteDeleteQueue,
|
||||||
SystemQueue,
|
SystemQueue,
|
||||||
UserWebhookDeliverQueue,
|
UserWebhookDeliverQueue,
|
||||||
SystemWebhookDeliverQueue,
|
SystemWebhookDeliverQueue,
|
||||||
@ -116,6 +116,7 @@ export class ClientServerService {
|
|||||||
|
|
||||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||||
|
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
|
||||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@ -248,6 +249,7 @@ export class ClientServerService {
|
|||||||
queues: [
|
queues: [
|
||||||
this.systemQueue,
|
this.systemQueue,
|
||||||
this.endedPollNotificationQueue,
|
this.endedPollNotificationQueue,
|
||||||
|
this.scheduledNoteDeleteQueue,
|
||||||
this.deliverQueue,
|
this.deliverQueue,
|
||||||
this.inboxQueue,
|
this.inboxQueue,
|
||||||
this.dbQueue,
|
this.dbQueue,
|
||||||
|
165
packages/frontend/src/components/MkDeleteScheduleEditor.vue
Normal file
165
packages/frontend/src/components/MkDeleteScheduleEditor.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="zmdxowus">
|
||||||
|
<span>{{ i18n.ts.scheduledNoteDelete }}</span>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<MkSelect v-model="expiration" small>
|
||||||
|
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||||
|
<option value="at">{{ i18n.ts._poll.at }}</option>
|
||||||
|
<option value="after">{{ i18n.ts._poll.after }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<section v-if="expiration === 'at'">
|
||||||
|
<MkInput v-model="atDate" small type="date" class="input">
|
||||||
|
<template #label>{{ i18n.ts._poll.deadlineDate }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="atTime" small type="time" class="input">
|
||||||
|
<template #label>{{ i18n.ts._poll.deadlineTime }}</template>
|
||||||
|
</MkInput>
|
||||||
|
</section>
|
||||||
|
<section v-else-if="expiration === 'after'">
|
||||||
|
<MkInput v-model="after" small type="number" class="input">
|
||||||
|
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkSelect v-model="unit" small>
|
||||||
|
<option value="second">{{ i18n.ts._time.second }}</option>
|
||||||
|
<option value="minute">{{ i18n.ts._time.minute }}</option>
|
||||||
|
<option value="hour">{{ i18n.ts._time.hour }}</option>
|
||||||
|
<option value="day">{{ i18n.ts._time.day }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import MkInput from './MkInput.vue';
|
||||||
|
import MkSelect from './MkSelect.vue';
|
||||||
|
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||||
|
import { addTime } from '@/scripts/time.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
export type DeleteScheduleEditorModelValue = {
|
||||||
|
deleteAt: number | null;
|
||||||
|
deleteAfter: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: DeleteScheduleEditorModelValue;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'update:modelValue', v: DeleteScheduleEditorModelValue): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const expiration = ref('at');
|
||||||
|
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||||
|
const atTime = ref('00:00');
|
||||||
|
const after = ref(0);
|
||||||
|
const unit = ref('second');
|
||||||
|
|
||||||
|
if (props.modelValue.deleteAt) {
|
||||||
|
expiration.value = 'at';
|
||||||
|
const deleteAt = new Date(props.modelValue.deleteAt);
|
||||||
|
atDate.value = formatDateTimeString(deleteAt, 'yyyy-MM-dd');
|
||||||
|
atTime.value = formatDateTimeString(deleteAt, 'HH:mm');
|
||||||
|
} else if (typeof props.modelValue.deleteAfter === 'number') {
|
||||||
|
expiration.value = 'after';
|
||||||
|
after.value = props.modelValue.deleteAfter / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(): DeleteScheduleEditorModelValue {
|
||||||
|
const calcAt = () => {
|
||||||
|
return new Date(`${atDate.value} ${atTime.value}`).getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcAfter = () => {
|
||||||
|
let base = parseInt(after.value.toString());
|
||||||
|
switch (unit.value) {
|
||||||
|
// @ts-expect-error fallthrough
|
||||||
|
case 'day': base *= 24;
|
||||||
|
// @ts-expect-error fallthrough
|
||||||
|
case 'hour': base *= 60;
|
||||||
|
// @ts-expect-error fallthrough
|
||||||
|
case 'minute': base *= 60;
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
case 'second': return base *= 1000;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteAt: expiration.value === 'at' ? calcAt() : null,
|
||||||
|
deleteAfter: expiration.value === 'after' ? calcAfter() : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
|
||||||
|
deep: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.zmdxowus {
|
||||||
|
padding: 8px 16px;
|
||||||
|
|
||||||
|
>span {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
>ul {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
>li {
|
||||||
|
display: flex;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
>.input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
>button {
|
||||||
|
width: 32px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>section {
|
||||||
|
margin: 16px 0 0 0;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
margin: 0 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
>section {
|
||||||
|
// MAGIC: Prevent div above from growing unless wrapped to its own line
|
||||||
|
flex-grow: 9999;
|
||||||
|
align-items: end;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
>.input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -116,6 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkA :to="notePage(appearNote)">
|
<MkA :to="notePage(appearNote)">
|
||||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<span v-if="appearNote.deleteAt"><i class="ti ti-bomb"></i>{{ i18n.ts.scheduledNoteDelete }}: <MkTime :time="appearNote.deleteAt" mode="detail" colored/></span>
|
||||||
</div>
|
</div>
|
||||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
|
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
|
||||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||||
|
@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
|
<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
|
||||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||||
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||||
|
<span v-if="note.deleteAt" style="margin-left: 0.5em;" :title="i18n.tsx.noteDeletationAt({ time: dateTimeFormat.format(new Date(note.deleteAt)) })"><i class="ti ti-bomb"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
@ -43,6 +44,7 @@ import { notePage } from '@/filters/note.js';
|
|||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
|
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
|
||||||
import { popupMenu } from '@/os.js';
|
import { popupMenu } from '@/os.js';
|
||||||
|
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -74,6 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||||
|
<MkDeleteScheduleEditor v-if="scheduledNoteDelete" v-model="scheduledNoteDelete" @destroyed="scheduledNoteDelete = null"/>
|
||||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||||
</div>
|
</div>
|
||||||
@ -81,6 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div :class="$style.footerLeft">
|
<div :class="$style.footerLeft">
|
||||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
||||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||||
|
<button v-tooltip="i18n.ts.scheduledNoteDelete" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: scheduledNoteDelete }]" @click="toggleScheduledNoteDelete"><i class="ti ti-bomb"></i></button>
|
||||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||||
@ -110,6 +112,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
|||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
|
import MkDeleteScheduleEditor, { type DeleteScheduleEditorModelValue } from '@/components/MkDeleteScheduleEditor.vue';
|
||||||
import { host, url } from '@/config.js';
|
import { host, url } from '@/config.js';
|
||||||
import { erase, unique } from '@/scripts/array.js';
|
import { erase, unique } from '@/scripts/array.js';
|
||||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||||
@ -182,6 +185,7 @@ const posted = ref(false);
|
|||||||
const text = ref(props.initialText ?? '');
|
const text = ref(props.initialText ?? '');
|
||||||
const files = ref(props.initialFiles ?? []);
|
const files = ref(props.initialFiles ?? []);
|
||||||
const poll = ref<PollEditorModelValue | null>(null);
|
const poll = ref<PollEditorModelValue | null>(null);
|
||||||
|
const scheduledNoteDelete = ref<DeleteScheduleEditorModelValue | null>(null);
|
||||||
const useCw = ref<boolean>(!!props.initialCw);
|
const useCw = ref<boolean>(!!props.initialCw);
|
||||||
const showPreview = ref(defaultStore.state.showPreview);
|
const showPreview = ref(defaultStore.state.showPreview);
|
||||||
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
||||||
@ -366,6 +370,7 @@ function watchForDraft() {
|
|||||||
watch(useCw, () => saveDraft());
|
watch(useCw, () => saveDraft());
|
||||||
watch(cw, () => saveDraft());
|
watch(cw, () => saveDraft());
|
||||||
watch(poll, () => saveDraft());
|
watch(poll, () => saveDraft());
|
||||||
|
watch(scheduledNoteDelete, () => saveDraft());
|
||||||
watch(files, () => saveDraft(), { deep: true });
|
watch(files, () => saveDraft(), { deep: true });
|
||||||
watch(visibility, () => saveDraft());
|
watch(visibility, () => saveDraft());
|
||||||
watch(localOnly, () => saveDraft());
|
watch(localOnly, () => saveDraft());
|
||||||
@ -421,6 +426,17 @@ function togglePoll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleScheduledNoteDelete() {
|
||||||
|
if (scheduledNoteDelete.value) {
|
||||||
|
scheduledNoteDelete.value = null;
|
||||||
|
} else {
|
||||||
|
scheduledNoteDelete.value = {
|
||||||
|
deleteAt: null,
|
||||||
|
deleteAfter: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addTag(tag: string) {
|
function addTag(tag: string) {
|
||||||
insertTextAtCursor(textareaEl.value, ` #${tag} `);
|
insertTextAtCursor(textareaEl.value, ` #${tag} `);
|
||||||
}
|
}
|
||||||
@ -718,6 +734,7 @@ function saveDraft() {
|
|||||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
||||||
quoteId: quoteId.value,
|
quoteId: quoteId.value,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
|
scheduledNoteDelete: scheduledNoteDelete.value,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -794,6 +811,7 @@ async function post(ev?: MouseEvent) {
|
|||||||
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
|
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
|
||||||
channelId: props.channel ? props.channel.id : undefined,
|
channelId: props.channel ? props.channel.id : undefined,
|
||||||
poll: poll.value,
|
poll: poll.value,
|
||||||
|
scheduledDelete: scheduledNoteDelete.value,
|
||||||
cw: useCw.value ? cw.value ?? '' : null,
|
cw: useCw.value ? cw.value ?? '' : null,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
@ -1029,6 +1047,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
quoteId.value = draft.data.quoteId;
|
quoteId.value = draft.data.quoteId;
|
||||||
reactionAcceptance.value = draft.data.reactionAcceptance;
|
reactionAcceptance.value = draft.data.reactionAcceptance;
|
||||||
|
if (draft.data.scheduledNoteDelete) {
|
||||||
|
scheduledNoteDelete.value = draft.data.scheduledNoteDelete;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1049,6 +1070,12 @@ onMounted(() => {
|
|||||||
expiredAfter: null,
|
expiredAfter: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (init.deleteAt) {
|
||||||
|
scheduledNoteDelete.value = {
|
||||||
|
deleteAt: init.deleteAt ? (new Date(init.deleteAt)).getTime() : null,
|
||||||
|
deleteAfter: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (init.visibleUserIds) {
|
if (init.visibleUserIds) {
|
||||||
misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => {
|
misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => {
|
||||||
users.forEach(u => pushVisibleUser(u));
|
users.forEach(u => pushVisibleUser(u));
|
||||||
|
@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||||
<MkUserName :user="u" :nowrap="true"/>
|
<MkUserName :user="u" :nowrap="true"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="count <= 0" :class="$style.user"> {{ i18n.ts.noUsers }} </div>
|
||||||
<div v-if="count > 10" :class="$style.more">+{{ count - 10 }}</div>
|
<div v-if="count > 10" :class="$style.more">+{{ count - 10 }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -26,6 +27,7 @@ import { } from 'vue';
|
|||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from './MkTooltip.vue';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
import { getEmojiName } from '@/scripts/emojilist.js';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
|
@ -36,6 +36,8 @@ import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.j
|
|||||||
import { customEmojisMap } from '@/custom-emojis.js';
|
import { customEmojisMap } from '@/custom-emojis.js';
|
||||||
import { getUnicodeEmoji } from '@/scripts/emojilist.js';
|
import { getUnicodeEmoji } from '@/scripts/emojilist.js';
|
||||||
|
|
||||||
|
const reactionChecksMuting = computed(defaultStore.makeGetterSetter('reactionChecksMuting'));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
count: number;
|
count: number;
|
||||||
@ -146,7 +148,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
if (!mock) {
|
if (!mock) {
|
||||||
useTooltip(buttonEl, async (showing) => {
|
useTooltip(buttonEl, async (showing) => {
|
||||||
const reactions = await misskeyApiGet('notes/reactions', {
|
const useGet = !reactionChecksMuting.value;
|
||||||
|
const apiCall = useGet ? misskeyApiGet : misskeyApi;
|
||||||
|
const reactions = await apiCall('notes/reactions', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
type: props.reaction,
|
type: props.reaction,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@ -154,12 +158,13 @@ if (!mock) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const users = reactions.map(x => x.user);
|
const users = reactions.map(x => x.user);
|
||||||
|
const count = users.length;
|
||||||
|
|
||||||
const { dispose } = os.popup(XDetails, {
|
const { dispose } = os.popup(XDetails, {
|
||||||
showing,
|
showing,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
users,
|
users,
|
||||||
count: props.count,
|
count,
|
||||||
targetElement: buttonEl.value,
|
targetElement: buttonEl.value,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -235,6 +235,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
|
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
|
||||||
<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
|
<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
|
||||||
<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
|
<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
|
||||||
|
|
||||||
|
<MkSwitch v-model="reactionChecksMuting">
|
||||||
|
{{ i18n.ts._reactionChecksMuting.title }}<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||||
|
<template #caption>{{ i18n.ts._reactionChecksMuting.caption }}</template>
|
||||||
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
<MkSelect v-model="serverDisconnectedBehavior">
|
<MkSelect v-model="serverDisconnectedBehavior">
|
||||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||||
@ -421,8 +426,6 @@ const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
|||||||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||||
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
|
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
|
||||||
const showTickerOnReplies = computed(defaultStore.makeGetterSetter('showTickerOnReplies'));
|
const showTickerOnReplies = computed(defaultStore.makeGetterSetter('showTickerOnReplies'));
|
||||||
//const searchEngine = computed(defaultStore.makeGetterSetter('searchEngine'));
|
|
||||||
const searchEngine = computed(defaultStore.makeGetterSetter('searchEngine'));
|
|
||||||
|
|
||||||
const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign'));
|
const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign'));
|
||||||
const uncollapseCW = computed(defaultStore.makeGetterSetter('uncollapseCW'));
|
const uncollapseCW = computed(defaultStore.makeGetterSetter('uncollapseCW'));
|
||||||
@ -435,6 +438,8 @@ const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('u
|
|||||||
const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
|
const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
|
||||||
const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
|
const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
|
||||||
const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
|
const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
|
||||||
|
const searchEngine = computed(defaultStore.makeGetterSetter('searchEngine'));
|
||||||
|
const reactionChecksMuting = computed(defaultStore.makeGetterSetter('reactionChecksMuting'));
|
||||||
|
|
||||||
watch(lang, () => {
|
watch(lang, () => {
|
||||||
miLocalStorage.setItem('lang', lang.value as string);
|
miLocalStorage.setItem('lang', lang.value as string);
|
||||||
|
@ -7,6 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkSwitch v-model="isLocked" @update:modelValue="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
|
<MkSwitch v-model="isLocked" @update:modelValue="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
|
||||||
<MkSwitch v-if="isLocked" v-model="autoAcceptFollowed" @update:modelValue="save()">{{ i18n.ts.autoAcceptFollowed }}</MkSwitch>
|
<MkSwitch v-if="isLocked" v-model="autoAcceptFollowed" @update:modelValue="save()">{{ i18n.ts.autoAcceptFollowed }}</MkSwitch>
|
||||||
|
<MkSwitch v-if="isLocked" v-model="autoRejectFollowRequest" @update:modelValue="save()">
|
||||||
|
{{ i18n.ts.autoRejectFollowRequest }}<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||||
|
<template #caption>{{ i18n.ts.autoRejectFollowRequestDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
<MkSwitch v-model="publicReactions" @update:modelValue="save()">
|
<MkSwitch v-model="publicReactions" @update:modelValue="save()">
|
||||||
{{ i18n.ts.makeReactionsPublic }}
|
{{ i18n.ts.makeReactionsPublic }}
|
||||||
@ -87,6 +91,7 @@ const $i = signinRequired();
|
|||||||
|
|
||||||
const isLocked = ref($i.isLocked);
|
const isLocked = ref($i.isLocked);
|
||||||
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
|
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
|
||||||
|
const autoRejectFollowRequest = ref($i.autoRejectFollowRequest);
|
||||||
const noCrawle = ref($i.noCrawle);
|
const noCrawle = ref($i.noCrawle);
|
||||||
const noindex = ref($i.noindex);
|
const noindex = ref($i.noindex);
|
||||||
const isExplorable = ref($i.isExplorable);
|
const isExplorable = ref($i.isExplorable);
|
||||||
@ -104,6 +109,7 @@ function save() {
|
|||||||
misskeyApi('i/update', {
|
misskeyApi('i/update', {
|
||||||
isLocked: !!isLocked.value,
|
isLocked: !!isLocked.value,
|
||||||
autoAcceptFollowed: !!autoAcceptFollowed.value,
|
autoAcceptFollowed: !!autoAcceptFollowed.value,
|
||||||
|
autoRejectFollowRequest: !!autoRejectFollowRequest.value,
|
||||||
noCrawle: !!noCrawle.value,
|
noCrawle: !!noCrawle.value,
|
||||||
noindex: !!noindex.value,
|
noindex: !!noindex.value,
|
||||||
isExplorable: !!isExplorable.value,
|
isExplorable: !!isExplorable.value,
|
||||||
|
@ -532,10 +532,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 'app' as 'app' | 'appWithShift' | 'native',
|
default: 'app' as 'app' | 'appWithShift' | 'native',
|
||||||
},
|
},
|
||||||
|
|
||||||
sound_masterVolume: {
|
sound_masterVolume: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
@ -565,6 +565,14 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
where: 'device',
|
where: 'device',
|
||||||
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
|
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
|
||||||
},
|
},
|
||||||
|
searchEngine: {
|
||||||
|
where: 'device',
|
||||||
|
default: 'https://google.com/search?q=',
|
||||||
|
},
|
||||||
|
reactionChecksMuting: {
|
||||||
|
where: 'device',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
@ -4145,7 +4145,7 @@ export type components = {
|
|||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
icon: 'info' | 'warning' | 'error' | 'success';
|
icon: 'info' | 'warning' | 'error' | 'success';
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
display: 'dialog' | 'normal' | 'banner';
|
display: 'dialog' | 'normal' | 'banner' | 'emergency';
|
||||||
needConfirmationToRead: boolean;
|
needConfirmationToRead: boolean;
|
||||||
silence: boolean;
|
silence: boolean;
|
||||||
forYou: boolean;
|
forYou: boolean;
|
||||||
@ -4204,6 +4204,8 @@ export type components = {
|
|||||||
votes: number;
|
votes: number;
|
||||||
}[];
|
}[];
|
||||||
}) | null;
|
}) | null;
|
||||||
|
/** Format: date-time */
|
||||||
|
deleteAt?: string | null;
|
||||||
emojis?: {
|
emojis?: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
@ -21897,6 +21899,10 @@ export type operations = {
|
|||||||
expiresAt?: number | null;
|
expiresAt?: number | null;
|
||||||
expiredAfter?: number | null;
|
expiredAfter?: number | null;
|
||||||
}) | null;
|
}) | null;
|
||||||
|
scheduledDelete?: ({
|
||||||
|
deleteAt?: number | null;
|
||||||
|
deleteAfter?: number | null;
|
||||||
|
}) | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user