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)
|
||||
|
||||
## 2024.8.0-yami-1.2.0
|
||||
### Feat
|
||||
* ノートの自動削除(cherry-pick)
|
||||
* フォローリクエスト自動拒否(cherry-pick)
|
||||
|
||||
## 2024.8.0-yami-1.1.0
|
||||
### Server
|
||||
* 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}"
|
||||
useGlobalSetting: "Use global settings"
|
||||
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"
|
||||
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."
|
||||
@ -1336,6 +1338,9 @@ _delivery:
|
||||
manuallySuspended: "Manually suspended"
|
||||
goneSuspended: "Server is suspended due to server deletion"
|
||||
autoSuspendedForNotResponding: "Server is suspended due to no responding"
|
||||
scheduledNoteDelete: "Time Bomb"
|
||||
noteDeletationAt: "This note will be deleted at {time}"
|
||||
|
||||
_bubbleGame:
|
||||
howToPlay: "How to play"
|
||||
hold: "Hold"
|
||||
@ -2819,3 +2824,6 @@ _contextMenu:
|
||||
app: "Application"
|
||||
appWithShift: "Application with shift key"
|
||||
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;
|
||||
/**
|
||||
* 独自機能
|
||||
*/
|
||||
"originalFeature": string;
|
||||
/**
|
||||
* 自動センシティブ判定
|
||||
*/
|
||||
@ -5266,6 +5270,14 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"gameRetry": string;
|
||||
/**
|
||||
* 時限爆弾
|
||||
*/
|
||||
"scheduledNoteDelete": string;
|
||||
/**
|
||||
* このノートは{time}に消去されます
|
||||
*/
|
||||
"noteDeletationAt": ParameterizedString<"time">;
|
||||
/**
|
||||
* 使用しない場合は空欄にしてください
|
||||
*/
|
||||
"notUsePleaseLeaveBlank": string;
|
||||
@ -5411,6 +5423,8 @@ export interface Locale extends ILocale {
|
||||
"section3": string;
|
||||
};
|
||||
};
|
||||
"autoRejectFollowRequest": string;
|
||||
"autoRejectFollowRequestDescription": string;
|
||||
"_announcement": {
|
||||
/**
|
||||
* 既存ユーザーのみ
|
||||
@ -10881,6 +10895,16 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"native": string;
|
||||
};
|
||||
"_reactionChecksMuting": {
|
||||
/**
|
||||
* リアクションでミュートを考慮する
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* リアクションがミュートを考慮しますが、キャッシュが効かず通信量が増えることがあります。
|
||||
*/
|
||||
"caption": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
@ -1024,6 +1024,7 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
|
||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
|
||||
beta: "ベータ"
|
||||
originalFeature: "独自機能"
|
||||
enableAutoSensitive: "自動センシティブ判定"
|
||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
||||
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
||||
@ -1337,6 +1338,8 @@ _delivery:
|
||||
manuallySuspended: "手動停止中"
|
||||
goneSuspended: "サーバー削除のため停止中"
|
||||
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
|
||||
scheduledNoteDelete: "時限爆弾"
|
||||
noteDeletationAt: "このノートは{time}に削除されます"
|
||||
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
@ -1353,6 +1356,8 @@ _bubbleGame:
|
||||
section1: "位置を調整してハコにモノを落とします。"
|
||||
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
|
||||
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
|
||||
autoRejectFollowRequest: "フォローリクエストを自動で拒否する"
|
||||
autoRejectFollowRequestDescription: "フォローリクエストを自動で拒否するようにします。「フォロー中ユーザーからのフォロリクを自動承認」がONになっている場合は、フォロー中ユーザーからのフォローリクエストは自動的に承認され、それ以外のユーザーからのフォローリクエストは自動的に拒否されるようになります。"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
@ -2894,3 +2899,7 @@ _contextMenu:
|
||||
app: "アプリケーション"
|
||||
appWithShift: "Shiftキーでアプリケーション"
|
||||
native: "ブラウザのUI"
|
||||
|
||||
_reactionChecksMuting:
|
||||
title: "リアクションでミュートを考慮する"
|
||||
caption: "リアクションがミュートを考慮しますが、キャッシュが効かず通信量が増えることがあります。"
|
||||
|
@ -693,6 +693,8 @@ notificationSetting: "通知設定"
|
||||
notificationSettingDesc: "出す通知の種類えらんでや。"
|
||||
useGlobalSetting: "グローバル設定を使ってや"
|
||||
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使われるで。オフにすると、別々に設定できるようになるで。"
|
||||
autoRejectFollowRequest: "フォローリクエストを自動で拒否するで"
|
||||
autoRejectFollowRequestDescription: "フォローリクエストを自動で拒否するようになるで。「フォロー中ユーザーからのフォロリクを自動承認」がONになってはると、フォロー中ユーザーからフォローリクエストが自動で承認されはり、それ以外のユーザーからのフォローリクエストは自動的に拒否されるで。"
|
||||
other: "その他"
|
||||
regenerateLoginToken: "ログイントークンを再生成"
|
||||
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 { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { Data } from 'ws';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@ -148,6 +149,7 @@ type Option = {
|
||||
uri?: string | null;
|
||||
url?: string | null;
|
||||
app?: MiApp | null;
|
||||
deleteAt?: Date | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@ -439,6 +441,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
name: data.name,
|
||||
text: data.text,
|
||||
hasPoll: data.poll != null,
|
||||
deleteAt: data.deleteAt,
|
||||
cw: data.cw ?? null,
|
||||
tags: tags.map(tag => normalizeForSearch(tag)),
|
||||
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 (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
|
||||
|
||||
|
@ -195,7 +195,9 @@ export class QueryService {
|
||||
qb
|
||||
.where('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}));
|
||||
})
|
||||
.andWhere('note.localOnly = FALSE') // 連合なしのノートは未ログイン者には見せない
|
||||
);
|
||||
} else {
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
|
@ -21,6 +21,7 @@ import type { Provider } from '@nestjs/common';
|
||||
|
||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
export type ScheduledNoteDeleteQueue = Bull.Queue<ScheduledNoteDeleteJobData>
|
||||
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
||||
export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||
export type DbQueue = Bull.Queue;
|
||||
@ -41,6 +42,12 @@ const $endedPollNotification: Provider = {
|
||||
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 = {
|
||||
provide: 'queue:deliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
||||
@ -89,6 +96,7 @@ const $systemWebhookDeliver: Provider = {
|
||||
providers: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$scheduledNoteDeleted,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
@ -100,6 +108,7 @@ const $systemWebhookDeliver: Provider = {
|
||||
exports: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$scheduledNoteDeleted,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
@ -113,6 +122,7 @@ export class QueueModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@ -129,6 +139,7 @@ export class QueueModule implements OnApplicationShutdown {
|
||||
await Promise.all([
|
||||
this.systemQueue.close(),
|
||||
this.endedPollNotificationQueue.close(),
|
||||
this.scheduledNoteDeleteQueue.close(),
|
||||
this.deliverQueue.close(),
|
||||
this.inboxQueue.close(),
|
||||
this.dbQueue.close(),
|
||||
|
@ -45,6 +45,7 @@ export class QueueService {
|
||||
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
|
@ -214,6 +214,20 @@ export class UserFollowingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@ -579,7 +593,9 @@ export class UserFollowingService implements OnModuleInit {
|
||||
});
|
||||
|
||||
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({
|
||||
|
@ -97,6 +97,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
// 連合なしで未ログインなら非表示
|
||||
if(packedNote.localOnly && !meId){
|
||||
hide = true;
|
||||
}
|
||||
|
||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||
if (packedNote.visibility === 'followers') {
|
||||
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 かつ自分が投稿者のフォロワーでなかったら非表示
|
||||
if (note.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
@ -395,6 +405,13 @@ export class NoteEntityService implements OnModuleInit {
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_hint_: options?._hint_,
|
||||
}) : 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,
|
||||
carefulBot: profile!.carefulBot,
|
||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
||||
autoRejectFollowRequest: profile!.autoRejectFollowRequest,
|
||||
noCrawle: profile!.noCrawle,
|
||||
preventAiLearning: profile!.preventAiLearning,
|
||||
isExplorable: user.isExplorable,
|
||||
|
@ -188,6 +188,11 @@ export class MiNote {
|
||||
})
|
||||
public hasPoll: boolean;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public deleteAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
@ -172,6 +172,11 @@ export class MiUserProfile {
|
||||
})
|
||||
public autoAcceptFollowed: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public autoRejectFollowRequest: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether reject index by crawler.',
|
||||
|
@ -152,6 +152,11 @@ export const packedNoteSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
deleteAt: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
emojis: {
|
||||
type: 'object',
|
||||
optional: true, nullable: false,
|
||||
|
@ -41,6 +41,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||
UserWebhookDeliverProcessorService,
|
||||
SystemWebhookDeliverProcessorService,
|
||||
EndedPollNotificationProcessorService,
|
||||
ScheduledNoteDeleteProcessorService,
|
||||
DeliverProcessorService,
|
||||
InboxProcessorService,
|
||||
AggregateRetentionProcessorService,
|
||||
|
@ -45,6 +45,7 @@ import { AggregateRetentionProcessorService } from './processors/AggregateRetent
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QUEUE, baseQueueOptions } from './const.js';
|
||||
import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js';
|
||||
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js';
|
||||
|
||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||
function httpRelatedBackoff(attemptsMade: number) {
|
||||
@ -84,6 +85,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private relationshipQueueWorker: Bull.Worker;
|
||||
private objectStorageQueueWorker: Bull.Worker;
|
||||
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||
private scheduledNoteDeleteQueueWorker: Bull.Worker;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@ -93,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
private scheduledNoteDeleteProcessorService: ScheduledNoteDeleteProcessorService,
|
||||
private deliverProcessorService: DeliverProcessorService,
|
||||
private inboxProcessorService: InboxProcessorService,
|
||||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||
@ -513,6 +516,12 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
});
|
||||
}
|
||||
//#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
|
||||
@ -527,6 +536,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
this.relationshipQueueWorker.run(),
|
||||
this.objectStorageQueueWorker.run(),
|
||||
this.endedPollNotificationQueueWorker.run(),
|
||||
this.scheduledNoteDeleteQueueWorker.run(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -542,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
this.relationshipQueueWorker.close(),
|
||||
this.objectStorageQueueWorker.close(),
|
||||
this.endedPollNotificationQueueWorker.close(),
|
||||
this.scheduledNoteDeleteQueueWorker.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ export const QUEUE = {
|
||||
INBOX: 'inbox',
|
||||
SYSTEM: 'system',
|
||||
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
||||
SCHEDULED_NOTE_DELETE: 'scheduledNoteDelete',
|
||||
DB: 'db',
|
||||
RELATIONSHIP: 'relationship',
|
||||
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'];
|
||||
};
|
||||
|
||||
export type ScheduledNoteDeleteJobData = {
|
||||
noteId: MiNote['id'];
|
||||
}
|
||||
|
||||
export type SystemWebhookDeliverJobData = {
|
||||
type: string;
|
||||
content: unknown;
|
||||
|
@ -183,6 +183,7 @@ export const paramDef = {
|
||||
publicReactions: { type: 'boolean' },
|
||||
carefulBot: { type: 'boolean' },
|
||||
autoAcceptFollowed: { type: 'boolean' },
|
||||
autoRejectFollowRequest: { type: 'boolean' },
|
||||
noCrawle: { type: 'boolean' },
|
||||
preventAiLearning: { 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.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
||||
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.isCat === 'boolean') updates.isCat = ps.isCat;
|
||||
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
|
||||
|
@ -104,6 +104,12 @@ export const meta = {
|
||||
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: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
@ -197,6 +203,14 @@ export const paramDef = {
|
||||
},
|
||||
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
|
||||
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;
|
||||
if (ps.channelId != null) {
|
||||
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,
|
||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||
deleteAt: ps.scheduledDelete?.deleteAt ? new Date(ps.scheduledDelete.deleteAt) : null,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -4,13 +4,12 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets, type FindOptionsWhere } from 'typeorm';
|
||||
import type { NoteReactionsRepository } from '@/models/_.js';
|
||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'reactions'],
|
||||
@ -59,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
private noteReactionEntityService: NoteReactionEntityService,
|
||||
private queryService: QueryService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
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.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) {
|
||||
// ローカルリアクションはホスト名が . とされているが
|
||||
// DB 上ではそうではないので、必要に応じて変換
|
||||
|
@ -30,7 +30,7 @@ import type {
|
||||
DeliverQueue,
|
||||
EndedPollNotificationQueue,
|
||||
InboxQueue,
|
||||
ObjectStorageQueue,
|
||||
ObjectStorageQueue, ScheduledNoteDeleteQueue,
|
||||
SystemQueue,
|
||||
UserWebhookDeliverQueue,
|
||||
SystemWebhookDeliverQueue,
|
||||
@ -116,6 +116,7 @@ export class ClientServerService {
|
||||
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@ -248,6 +249,7 @@ export class ClientServerService {
|
||||
queues: [
|
||||
this.systemQueue,
|
||||
this.endedPollNotificationQueue,
|
||||
this.scheduledNoteDeleteQueue,
|
||||
this.deliverQueue,
|
||||
this.inboxQueue,
|
||||
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)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
<span v-if="appearNote.deleteAt"><i class="ti ti-bomb"></i>{{ i18n.ts.scheduledNoteDelete }}: <MkTime :time="appearNote.deleteAt" mode="detail" colored/></span>
|
||||
</div>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
|
||||
<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.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.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>
|
||||
</header>
|
||||
</template>
|
||||
@ -43,6 +44,7 @@ import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
|
||||
import { popupMenu } from '@/os.js';
|
||||
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||
|
||||
const props = defineProps<{
|
||||
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">
|
||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||
<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"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
</div>
|
||||
@ -81,6 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<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.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.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>
|
||||
@ -110,6 +112,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||
import MkDeleteScheduleEditor, { type DeleteScheduleEditorModelValue } from '@/components/MkDeleteScheduleEditor.vue';
|
||||
import { host, url } from '@/config.js';
|
||||
import { erase, unique } from '@/scripts/array.js';
|
||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||
@ -182,6 +185,7 @@ const posted = ref(false);
|
||||
const text = ref(props.initialText ?? '');
|
||||
const files = ref(props.initialFiles ?? []);
|
||||
const poll = ref<PollEditorModelValue | null>(null);
|
||||
const scheduledNoteDelete = ref<DeleteScheduleEditorModelValue | null>(null);
|
||||
const useCw = ref<boolean>(!!props.initialCw);
|
||||
const showPreview = ref(defaultStore.state.showPreview);
|
||||
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
||||
@ -366,6 +370,7 @@ function watchForDraft() {
|
||||
watch(useCw, () => saveDraft());
|
||||
watch(cw, () => saveDraft());
|
||||
watch(poll, () => saveDraft());
|
||||
watch(scheduledNoteDelete, () => saveDraft());
|
||||
watch(files, () => saveDraft(), { deep: true });
|
||||
watch(visibility, () => 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) {
|
||||
insertTextAtCursor(textareaEl.value, ` #${tag} `);
|
||||
}
|
||||
@ -718,6 +734,7 @@ function saveDraft() {
|
||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
||||
quoteId: quoteId.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,
|
||||
channelId: props.channel ? props.channel.id : undefined,
|
||||
poll: poll.value,
|
||||
scheduledDelete: scheduledNoteDelete.value,
|
||||
cw: useCw.value ? cw.value ?? '' : null,
|
||||
localOnly: localOnly.value,
|
||||
visibility: visibility.value,
|
||||
@ -1029,6 +1047,9 @@ onMounted(() => {
|
||||
}
|
||||
quoteId.value = draft.data.quoteId;
|
||||
reactionAcceptance.value = draft.data.reactionAcceptance;
|
||||
if (draft.data.scheduledNoteDelete) {
|
||||
scheduledNoteDelete.value = draft.data.scheduledNoteDelete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1049,6 +1070,12 @@ onMounted(() => {
|
||||
expiredAfter: null,
|
||||
};
|
||||
}
|
||||
if (init.deleteAt) {
|
||||
scheduledNoteDelete.value = {
|
||||
deleteAt: init.deleteAt ? (new Date(init.deleteAt)).getTime() : null,
|
||||
deleteAfter: null,
|
||||
};
|
||||
}
|
||||
if (init.visibleUserIds) {
|
||||
misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => {
|
||||
users.forEach(u => pushVisibleUser(u));
|
||||
|
@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
<MkUserName :user="u" :nowrap="true"/>
|
||||
</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>
|
||||
</div>
|
||||
@ -26,6 +27,7 @@ import { } from 'vue';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
defineProps<{
|
||||
showing: boolean;
|
||||
|
@ -36,6 +36,8 @@ import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.j
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import { getUnicodeEmoji } from '@/scripts/emojilist.js';
|
||||
|
||||
const reactionChecksMuting = computed(defaultStore.makeGetterSetter('reactionChecksMuting'));
|
||||
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
count: number;
|
||||
@ -146,7 +148,9 @@ onMounted(() => {
|
||||
|
||||
if (!mock) {
|
||||
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,
|
||||
type: props.reaction,
|
||||
limit: 10,
|
||||
@ -154,12 +158,13 @@ if (!mock) {
|
||||
});
|
||||
|
||||
const users = reactions.map(x => x.user);
|
||||
const count = users.length;
|
||||
|
||||
const { dispose } = os.popup(XDetails, {
|
||||
showing,
|
||||
reaction: props.reaction,
|
||||
users,
|
||||
count: props.count,
|
||||
count,
|
||||
targetElement: buttonEl.value,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
|
@ -235,6 +235,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
|
||||
<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</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>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
@ -421,8 +426,6 @@ const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
|
||||
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 uncollapseCW = computed(defaultStore.makeGetterSetter('uncollapseCW'));
|
||||
@ -435,6 +438,8 @@ const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('u
|
||||
const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
|
||||
const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
|
||||
const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
|
||||
const searchEngine = computed(defaultStore.makeGetterSetter('searchEngine'));
|
||||
const reactionChecksMuting = computed(defaultStore.makeGetterSetter('reactionChecksMuting'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
|
@ -7,6 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_m">
|
||||
<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="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()">
|
||||
{{ i18n.ts.makeReactionsPublic }}
|
||||
@ -87,6 +91,7 @@ const $i = signinRequired();
|
||||
|
||||
const isLocked = ref($i.isLocked);
|
||||
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
|
||||
const autoRejectFollowRequest = ref($i.autoRejectFollowRequest);
|
||||
const noCrawle = ref($i.noCrawle);
|
||||
const noindex = ref($i.noindex);
|
||||
const isExplorable = ref($i.isExplorable);
|
||||
@ -104,6 +109,7 @@ function save() {
|
||||
misskeyApi('i/update', {
|
||||
isLocked: !!isLocked.value,
|
||||
autoAcceptFollowed: !!autoAcceptFollowed.value,
|
||||
autoRejectFollowRequest: !!autoRejectFollowRequest.value,
|
||||
noCrawle: !!noCrawle.value,
|
||||
noindex: !!noindex.value,
|
||||
isExplorable: !!isExplorable.value,
|
||||
|
@ -532,10 +532,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
contextMenu: {
|
||||
contextMenu: {
|
||||
where: 'device',
|
||||
default: 'app' as 'app' | 'appWithShift' | 'native',
|
||||
},
|
||||
},
|
||||
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
@ -565,6 +565,14 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
|
||||
},
|
||||
searchEngine: {
|
||||
where: 'device',
|
||||
default: 'https://google.com/search?q=',
|
||||
},
|
||||
reactionChecksMuting: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
@ -4145,7 +4145,7 @@ export type components = {
|
||||
/** @enum {string} */
|
||||
icon: 'info' | 'warning' | 'error' | 'success';
|
||||
/** @enum {string} */
|
||||
display: 'dialog' | 'normal' | 'banner';
|
||||
display: 'dialog' | 'normal' | 'banner' | 'emergency';
|
||||
needConfirmationToRead: boolean;
|
||||
silence: boolean;
|
||||
forYou: boolean;
|
||||
@ -4204,6 +4204,8 @@ export type components = {
|
||||
votes: number;
|
||||
}[];
|
||||
}) | null;
|
||||
/** Format: date-time */
|
||||
deleteAt?: string | null;
|
||||
emojis?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
@ -21897,6 +21899,10 @@ export type operations = {
|
||||
expiresAt?: number | null;
|
||||
expiredAfter?: number | null;
|
||||
}) | null;
|
||||
scheduledDelete?: ({
|
||||
deleteAt?: number | null;
|
||||
deleteAfter?: number | null;
|
||||
}) | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user