feat(note): 予約投稿 (MisskeyIO#890)
This commit is contained in:
parent
509f385402
commit
cbe80fdd26
56 changed files with 1633 additions and 166 deletions
|
@ -1319,6 +1319,12 @@ emailAddressLogin: "Login with email address"
|
||||||
usernameLogin: "Login with username"
|
usernameLogin: "Login with username"
|
||||||
autoloadDrafts: "Automatically load drafts when opening the posting form"
|
autoloadDrafts: "Automatically load drafts when opening the posting form"
|
||||||
drafts: "Drafts"
|
drafts: "Drafts"
|
||||||
|
unsent: "Unsent"
|
||||||
|
schedule: "Schedule"
|
||||||
|
scheduled: "Scheduled"
|
||||||
|
unschedule: "Unschedule"
|
||||||
|
setScheduledTime: "Set scheduled time"
|
||||||
|
willBePostedAt: "Note will be posted at {x}"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "How to play"
|
howToPlay: "How to play"
|
||||||
|
@ -1787,6 +1793,7 @@ _role:
|
||||||
gtlAvailable: "Can view the global timeline"
|
gtlAvailable: "Can view the global timeline"
|
||||||
ltlAvailable: "Can view the local timeline"
|
ltlAvailable: "Can view the local timeline"
|
||||||
canPublicNote: "Can send public notes"
|
canPublicNote: "Can send public notes"
|
||||||
|
canScheduleNote: "Can schedule notes"
|
||||||
canInitiateConversation: "Can mention, reply or quote"
|
canInitiateConversation: "Can mention, reply or quote"
|
||||||
canCreateContent: "Can create contents"
|
canCreateContent: "Can create contents"
|
||||||
canUpdateContent: "Can edit contents"
|
canUpdateContent: "Can edit contents"
|
||||||
|
@ -2474,6 +2481,9 @@ _notification:
|
||||||
roleAssigned: "Role given"
|
roleAssigned: "Role given"
|
||||||
emptyPushNotificationMessage: "Push notifications have been updated"
|
emptyPushNotificationMessage: "Push notifications have been updated"
|
||||||
achievementEarned: "Achievement unlocked"
|
achievementEarned: "Achievement unlocked"
|
||||||
|
noteScheduled: "Note has been scheduled"
|
||||||
|
scheduledNotePosted: "Scheduled note has been posted"
|
||||||
|
scheduledNoteError: "Scheduled note has problem with posting"
|
||||||
testNotification: "Test notification"
|
testNotification: "Test notification"
|
||||||
checkNotificationBehavior: "Check notification appearance"
|
checkNotificationBehavior: "Check notification appearance"
|
||||||
sendTestNotification: "Send test notification"
|
sendTestNotification: "Send test notification"
|
||||||
|
@ -2497,6 +2507,9 @@ _notification:
|
||||||
followRequestAccepted: "Accepted follow requests"
|
followRequestAccepted: "Accepted follow requests"
|
||||||
roleAssigned: "Role given"
|
roleAssigned: "Role given"
|
||||||
achievementEarned: "Achievement unlocked"
|
achievementEarned: "Achievement unlocked"
|
||||||
|
noteScheduled: "Note scheduled"
|
||||||
|
scheduledNotePosted: "Scheduled note posted"
|
||||||
|
scheduledNoteError: "Problem with scheduled note"
|
||||||
app: "Notifications from linked apps"
|
app: "Notifications from linked apps"
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "followed you back"
|
followBack: "followed you back"
|
||||||
|
|
52
locales/index.d.ts
vendored
52
locales/index.d.ts
vendored
|
@ -5338,6 +5338,30 @@ export interface Locale extends ILocale {
|
||||||
* 下書き
|
* 下書き
|
||||||
*/
|
*/
|
||||||
"drafts": string;
|
"drafts": string;
|
||||||
|
/**
|
||||||
|
* 未送信
|
||||||
|
*/
|
||||||
|
"unsent": string;
|
||||||
|
/**
|
||||||
|
* 予約
|
||||||
|
*/
|
||||||
|
"schedule": string;
|
||||||
|
/**
|
||||||
|
* 予約済み
|
||||||
|
*/
|
||||||
|
"scheduled": string;
|
||||||
|
/**
|
||||||
|
* 予約を解除
|
||||||
|
*/
|
||||||
|
"unschedule": string;
|
||||||
|
/**
|
||||||
|
* 予約日時を設定
|
||||||
|
*/
|
||||||
|
"setScheduledTime": string;
|
||||||
|
/**
|
||||||
|
* {x}に投稿されます
|
||||||
|
*/
|
||||||
|
"willBePostedAt": ParameterizedString<"x">;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -6991,6 +7015,10 @@ export interface Locale extends ILocale {
|
||||||
* パブリック投稿の許可
|
* パブリック投稿の許可
|
||||||
*/
|
*/
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
|
/**
|
||||||
|
* 予約投稿の許可
|
||||||
|
*/
|
||||||
|
"canScheduleNote": string;
|
||||||
/**
|
/**
|
||||||
* メンション、リプライ、引用の許可
|
* メンション、リプライ、引用の許可
|
||||||
*/
|
*/
|
||||||
|
@ -9649,6 +9677,18 @@ export interface Locale extends ILocale {
|
||||||
* 実績を獲得
|
* 実績を獲得
|
||||||
*/
|
*/
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
/**
|
||||||
|
* ノートが予約されました
|
||||||
|
*/
|
||||||
|
"noteScheduled": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートが投稿されました
|
||||||
|
*/
|
||||||
|
"scheduledNotePosted": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートを投稿できませんでした
|
||||||
|
*/
|
||||||
|
"scheduledNoteError": string;
|
||||||
/**
|
/**
|
||||||
* 通知テスト
|
* 通知テスト
|
||||||
*/
|
*/
|
||||||
|
@ -9738,6 +9778,18 @@ export interface Locale extends ILocale {
|
||||||
* 実績の獲得
|
* 実績の獲得
|
||||||
*/
|
*/
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
/**
|
||||||
|
* ノートが予約された
|
||||||
|
*/
|
||||||
|
"noteScheduled": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートが投稿された
|
||||||
|
*/
|
||||||
|
"scheduledNotePosted": string;
|
||||||
|
/**
|
||||||
|
* 予約済みのノートが投稿できなかった
|
||||||
|
*/
|
||||||
|
"scheduledNoteError": string;
|
||||||
/**
|
/**
|
||||||
* 連携アプリからの通知
|
* 連携アプリからの通知
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1328,6 +1328,12 @@ emailAddressLogin: "メールアドレスでログイン"
|
||||||
usernameLogin: "ユーザー名でログイン"
|
usernameLogin: "ユーザー名でログイン"
|
||||||
autoloadDrafts: "投稿フォームを開いたときに下書きを自動で読み込む"
|
autoloadDrafts: "投稿フォームを開いたときに下書きを自動で読み込む"
|
||||||
drafts: "下書き"
|
drafts: "下書き"
|
||||||
|
unsent: "未送信"
|
||||||
|
schedule: "予約"
|
||||||
|
scheduled: "予約済み"
|
||||||
|
unschedule: "予約を解除"
|
||||||
|
setScheduledTime: "予約日時を設定"
|
||||||
|
willBePostedAt: "{x}に投稿されます"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -1801,6 +1807,7 @@ _role:
|
||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
|
canScheduleNote: "予約投稿の許可"
|
||||||
canInitiateConversation: "メンション、リプライ、引用の許可"
|
canInitiateConversation: "メンション、リプライ、引用の許可"
|
||||||
canCreateContent: "コンテンツの作成"
|
canCreateContent: "コンテンツの作成"
|
||||||
canUpdateContent: "コンテンツの編集"
|
canUpdateContent: "コンテンツの編集"
|
||||||
|
@ -2538,6 +2545,9 @@ _notification:
|
||||||
roleAssigned: "ロールが付与されました"
|
roleAssigned: "ロールが付与されました"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
|
noteScheduled: "ノートが予約されました"
|
||||||
|
scheduledNotePosted: "予約済みのノートが投稿されました"
|
||||||
|
scheduledNoteError: "予約済みのノートを投稿できませんでした"
|
||||||
testNotification: "通知テスト"
|
testNotification: "通知テスト"
|
||||||
checkNotificationBehavior: "通知の表示を確かめる"
|
checkNotificationBehavior: "通知の表示を確かめる"
|
||||||
sendTestNotification: "テスト通知を送信する"
|
sendTestNotification: "テスト通知を送信する"
|
||||||
|
@ -2562,6 +2572,9 @@ _notification:
|
||||||
followRequestAccepted: "フォローが受理された"
|
followRequestAccepted: "フォローが受理された"
|
||||||
roleAssigned: "ロールが付与された"
|
roleAssigned: "ロールが付与された"
|
||||||
achievementEarned: "実績の獲得"
|
achievementEarned: "実績の獲得"
|
||||||
|
noteScheduled: "ノートが予約された"
|
||||||
|
scheduledNotePosted: "予約済みのノートが投稿された"
|
||||||
|
scheduledNoteError: "予約済みのノートが投稿できなかった"
|
||||||
app: "連携アプリからの通知"
|
app: "連携アプリからの通知"
|
||||||
|
|
||||||
_actions:
|
_actions:
|
||||||
|
|
|
@ -1316,6 +1316,12 @@ emailAddressLogin: "이메일 주소로 로그인"
|
||||||
usernameLogin: "사용자명으로 로그인"
|
usernameLogin: "사용자명으로 로그인"
|
||||||
autoloadDrafts: "글 작성 시 자동으로 임시 저장된 글 불러오기"
|
autoloadDrafts: "글 작성 시 자동으로 임시 저장된 글 불러오기"
|
||||||
drafts: "임시 저장"
|
drafts: "임시 저장"
|
||||||
|
unsent: "미전송"
|
||||||
|
schedule: "예약"
|
||||||
|
scheduled: "예약됨"
|
||||||
|
unschedule: "예약 취소"
|
||||||
|
setScheduledTime: "예약 시간 설정"
|
||||||
|
willBePostedAt: "{x}에 게시됩니다"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "설명"
|
howToPlay: "설명"
|
||||||
|
@ -1784,6 +1790,7 @@ _role:
|
||||||
gtlAvailable: "글로벌 타임라인 보이기"
|
gtlAvailable: "글로벌 타임라인 보이기"
|
||||||
ltlAvailable: "로컬 타임라인 보이기"
|
ltlAvailable: "로컬 타임라인 보이기"
|
||||||
canPublicNote: "공개 노트 허용"
|
canPublicNote: "공개 노트 허용"
|
||||||
|
canScheduleNote: "노트 예약 허용"
|
||||||
mentionMax: "노트에 넣을 수 있는 멘션 수"
|
mentionMax: "노트에 넣을 수 있는 멘션 수"
|
||||||
canCreateContent: "컨텐츠 생성 허용"
|
canCreateContent: "컨텐츠 생성 허용"
|
||||||
canUpdateContent: "컨텐츠 수정 허용"
|
canUpdateContent: "컨텐츠 수정 허용"
|
||||||
|
@ -2456,9 +2463,12 @@ _notification:
|
||||||
pollEnded: "투표 결과가 발표되었습니다"
|
pollEnded: "투표 결과가 발표되었습니다"
|
||||||
newNote: "새 게시물"
|
newNote: "새 게시물"
|
||||||
unreadAntennaNote: "안테나 {name}"
|
unreadAntennaNote: "안테나 {name}"
|
||||||
roleAssigned: "역할이 부여 되었습니다."
|
roleAssigned: "역할이 부여 되었습니다"
|
||||||
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
|
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
|
||||||
achievementEarned: "도전 과제를 달성했습니다"
|
achievementEarned: "도전 과제를 달성했습니다"
|
||||||
|
noteScheduled: "노트가 예약되었습니다"
|
||||||
|
scheduledNotePosted: "예약된 노트가 게시되었습니다"
|
||||||
|
scheduledNoteError: "예약된 노트를 게시하지 못했습니다"
|
||||||
testNotification: "알림 테스트"
|
testNotification: "알림 테스트"
|
||||||
checkNotificationBehavior: "알림 표시를 체크하기"
|
checkNotificationBehavior: "알림 표시를 체크하기"
|
||||||
sendTestNotification: "테스트 알림 보내기"
|
sendTestNotification: "테스트 알림 보내기"
|
||||||
|
@ -2482,6 +2492,9 @@ _notification:
|
||||||
followRequestAccepted: "팔로우 요청이 승인되었을 때"
|
followRequestAccepted: "팔로우 요청이 승인되었을 때"
|
||||||
roleAssigned: "역할이 부여 됨"
|
roleAssigned: "역할이 부여 됨"
|
||||||
achievementEarned: "도전 과제 획득"
|
achievementEarned: "도전 과제 획득"
|
||||||
|
noteScheduled: "노트가 예약됨"
|
||||||
|
scheduledNotePosted: "예약된 노트가 게시됨"
|
||||||
|
scheduledNoteError: "예약된 노트를 게시하지 못함"
|
||||||
app: "연동된 앱을 통한 알림"
|
app: "연동된 앱을 통한 알림"
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "팔로우"
|
followBack: "팔로우"
|
||||||
|
|
BIN
packages/backend/assets/tabler-badges/badges.png
Normal file
BIN
packages/backend/assets/tabler-badges/badges.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
packages/backend/assets/tabler-badges/calendar-check.png
Normal file
BIN
packages/backend/assets/tabler-badges/calendar-check.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/backend/assets/tabler-badges/calendar-exclamation.png
Normal file
BIN
packages/backend/assets/tabler-badges/calendar-exclamation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/backend/assets/tabler-badges/calendar-time.png
Normal file
BIN
packages/backend/assets/tabler-badges/calendar-time.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
21
packages/backend/migration/1736923279563-ScheduledNote.js
Normal file
21
packages/backend/migration/1736923279563-ScheduledNote.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export class ScheduledNote1736923279563 {
|
||||||
|
name = 'ScheduledNote1736923279563'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "note_scheduled" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "scheduledAt" TIMESTAMP WITH TIME ZONE, "reason" character varying(256), "userId" character varying(32) NOT NULL, "draft" jsonb NOT NULL, CONSTRAINT "PK_14ca8fa67f70dc68ebab8900f4b" PRIMARY KEY ("id")); COMMENT ON COLUMN "note_scheduled"."createdAt" IS 'The created date of the Note.'; COMMENT ON COLUMN "note_scheduled"."scheduledAt" IS 'The scheduled date of the Note.'; COMMENT ON COLUMN "note_scheduled"."userId" IS 'The ID of author.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_7ddf8710a9faee81081592ec35" ON "note_scheduled" ("createdAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_bbe52891059217fc31e73e84e2" ON "note_scheduled" ("scheduledAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_b148b24837cc7a2707ae1f0975" ON "note_scheduled" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dfeab22d6bbc4799193997553a" ON "note_scheduled" ("userId", "scheduledAt") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "note_scheduled" ADD CONSTRAINT "FK_b148b24837cc7a2707ae1f0975a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note_scheduled" DROP CONSTRAINT "FK_b148b24837cc7a2707ae1f0975a"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_dfeab22d6bbc4799193997553a"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_b148b24837cc7a2707ae1f0975"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_bbe52891059217fc31e73e84e2"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_7ddf8710a9faee81081592ec35"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "note_scheduled"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -104,6 +104,7 @@ import { UserAccountMoveLogEntityService } from './entities/UserAccountMoveLogEn
|
||||||
import { MutingEntityService } from './entities/MutingEntityService.js';
|
import { MutingEntityService } from './entities/MutingEntityService.js';
|
||||||
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
||||||
import { NoteEntityService } from './entities/NoteEntityService.js';
|
import { NoteEntityService } from './entities/NoteEntityService.js';
|
||||||
|
import { ScheduledNoteEntityService } from './entities/ScheduledNoteEntityService.js';
|
||||||
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
|
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
|
||||||
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js';
|
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js';
|
||||||
import { NotificationEntityService } from './entities/NotificationEntityService.js';
|
import { NotificationEntityService } from './entities/NotificationEntityService.js';
|
||||||
|
@ -245,6 +246,7 @@ const $UserAccountMoveLogEntityService: Provider = { provide: 'UserAccountMoveLo
|
||||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
||||||
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
||||||
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
||||||
|
const $ScheduledNoteEntityService: Provider = { provide: 'ScheduledNoteEntityService', useExisting: ScheduledNoteEntityService };
|
||||||
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
|
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
|
||||||
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
|
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
|
||||||
const $NotificationEntityService: Provider = { provide: 'NotificationEntityService', useExisting: NotificationEntityService };
|
const $NotificationEntityService: Provider = { provide: 'NotificationEntityService', useExisting: NotificationEntityService };
|
||||||
|
@ -385,6 +387,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
|
ScheduledNoteEntityService,
|
||||||
NoteFavoriteEntityService,
|
NoteFavoriteEntityService,
|
||||||
NoteReactionEntityService,
|
NoteReactionEntityService,
|
||||||
NotificationEntityService,
|
NotificationEntityService,
|
||||||
|
@ -521,6 +524,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
|
$ScheduledNoteEntityService,
|
||||||
$NoteFavoriteEntityService,
|
$NoteFavoriteEntityService,
|
||||||
$NoteReactionEntityService,
|
$NoteReactionEntityService,
|
||||||
$NotificationEntityService,
|
$NotificationEntityService,
|
||||||
|
@ -657,6 +661,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
|
ScheduledNoteEntityService,
|
||||||
NoteFavoriteEntityService,
|
NoteFavoriteEntityService,
|
||||||
NoteReactionEntityService,
|
NoteReactionEntityService,
|
||||||
NotificationEntityService,
|
NotificationEntityService,
|
||||||
|
@ -792,6 +797,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
|
$ScheduledNoteEntityService,
|
||||||
$NoteFavoriteEntityService,
|
$NoteFavoriteEntityService,
|
||||||
$NoteReactionEntityService,
|
$NoteReactionEntityService,
|
||||||
$NotificationEntityService,
|
$NotificationEntityService,
|
||||||
|
|
|
@ -13,16 +13,28 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
|
import type {
|
||||||
|
ChannelFollowingsRepository,
|
||||||
|
ChannelsRepository,
|
||||||
|
FollowingsRepository,
|
||||||
|
InstancesRepository,
|
||||||
|
MiFollowing,
|
||||||
|
NotesRepository,
|
||||||
|
NoteThreadMutingsRepository,
|
||||||
|
ScheduledNotesRepository,
|
||||||
|
UserListMembershipsRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
UsersRepository
|
||||||
|
} from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import type { IPoll } from '@/models/Poll.js';
|
import type { IPoll } from '@/models/Poll.js';
|
||||||
import { MiPoll } from '@/models/Poll.js';
|
import { MiPoll } from '@/models/Poll.js';
|
||||||
|
import type { NoteCreateOption, MinimumUser } from '@/types.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import type { MiChannel } from '@/models/Channel.js';
|
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
|
@ -118,35 +130,6 @@ class NotificationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MinimumUser = {
|
|
||||||
id: MiUser['id'];
|
|
||||||
host: MiUser['host'];
|
|
||||||
username: MiUser['username'];
|
|
||||||
uri: MiUser['uri'];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Option = {
|
|
||||||
createdAt?: Date | null;
|
|
||||||
name?: string | null;
|
|
||||||
text?: string | null;
|
|
||||||
reply?: MiNote | null;
|
|
||||||
renote?: MiNote | null;
|
|
||||||
files?: MiDriveFile[] | null;
|
|
||||||
poll?: IPoll | null;
|
|
||||||
localOnly?: boolean | null;
|
|
||||||
reactionAcceptance?: MiNote['reactionAcceptance'];
|
|
||||||
cw?: string | null;
|
|
||||||
visibility?: string;
|
|
||||||
visibleUsers?: MinimumUser[] | null;
|
|
||||||
channel?: MiChannel | null;
|
|
||||||
apMentions?: MinimumUser[] | null;
|
|
||||||
apHashtags?: string[] | null;
|
|
||||||
apEmojis?: string[] | null;
|
|
||||||
uri?: string | null;
|
|
||||||
url?: string | null;
|
|
||||||
app?: MiApp | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteCreateService implements OnApplicationShutdown {
|
export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
@ -169,6 +152,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
|
@ -229,7 +215,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
host: MiUser['host'];
|
host: MiUser['host'];
|
||||||
isBot: MiUser['isBot'];
|
isBot: MiUser['isBot'];
|
||||||
isCat: MiUser['isCat'];
|
isCat: MiUser['isCat'];
|
||||||
}, data: Option, silent = false): Promise<MiNote> {
|
}, data: NoteCreateOption, silent = false): Promise<MiNote | MiScheduledNote> {
|
||||||
// チャンネル外にリプライしたら対象のスコープに合わせる
|
// チャンネル外にリプライしたら対象のスコープに合わせる
|
||||||
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
||||||
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
|
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
|
||||||
|
@ -425,6 +411,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', `Notes including mentions are limited to ${policies.mentionLimit} users.`);
|
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', `Notes including mentions are limited to ${policies.mentionLimit} users.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.scheduledAt) {
|
||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
||||||
|
|
||||||
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||||
|
@ -433,10 +420,21 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
);
|
);
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
|
} else {
|
||||||
|
if (!policies.canScheduleNote) {
|
||||||
|
throw new IdentifiableError('7cc42034-f7ab-4f7c-87b4-e00854479080', 'User has no permission to schedule notes.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = await this.insertScheduledNote(user, data);
|
||||||
|
|
||||||
|
await this.queueService.createScheduledNoteJob(draft.id, draft.scheduledAt!);
|
||||||
|
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async insertNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) {
|
private async insertNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: NoteCreateOption, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) {
|
||||||
const insert = new MiNote({
|
const insert = new MiNote({
|
||||||
id: this.idService.gen(data.createdAt?.getTime()),
|
id: this.idService.gen(data.createdAt?.getTime()),
|
||||||
createdAt: data.createdAt!,
|
createdAt: data.createdAt!,
|
||||||
|
@ -534,13 +532,40 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async insertScheduledNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: NoteCreateOption) {
|
||||||
|
const insert = new MiScheduledNote({
|
||||||
|
id: this.idService.gen(data.createdAt?.getTime()),
|
||||||
|
createdAt: data.createdAt!,
|
||||||
|
scheduledAt: data.scheduledAt!,
|
||||||
|
userId: user.id,
|
||||||
|
draft: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 予約投稿を作成
|
||||||
|
try {
|
||||||
|
await this.scheduledNotesRepository.insert(insert);
|
||||||
|
|
||||||
|
return insert;
|
||||||
|
} catch (e) {
|
||||||
|
// duplicate key error
|
||||||
|
if (isDuplicateKeyValueError(e)) {
|
||||||
|
throw new IdentifiableError('5ea8e4f5-9d64-4e6c-92b8-9e2b5a4756bc', 'There is already a scheduled note with the same time.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Failed to create scheduled note: ${e}`, { error: e });
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async postNoteCreated(note: MiNote, user: {
|
private async postNoteCreated(note: MiNote, user: {
|
||||||
id: MiUser['id'];
|
id: MiUser['id'];
|
||||||
username: MiUser['username'];
|
username: MiUser['username'];
|
||||||
host: MiUser['host'];
|
host: MiUser['host'];
|
||||||
isBot: MiUser['isBot'];
|
isBot: MiUser['isBot'];
|
||||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
}, data: NoteCreateOption, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
this.notesChart.update(note, true);
|
this.notesChart.update(note, true);
|
||||||
|
@ -792,12 +817,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isRenote(note: Option): note is Option & { renote: MiNote } {
|
private isRenote(note: NoteCreateOption): note is NoteCreateOption & { renote: MiNote } {
|
||||||
return note.renote != null;
|
return note.renote != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isQuote(note: Option): note is Option & { renote: MiNote } & (
|
private isQuote(note: NoteCreateOption): note is NoteCreateOption & { renote: MiNote } & (
|
||||||
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
|
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
|
||||||
) {
|
) {
|
||||||
// NOTE: SYNC WITH misc/is-renote.ts
|
// NOTE: SYNC WITH misc/is-renote.ts
|
||||||
|
@ -873,7 +898,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
|
private async renderNoteOrRenoteActivity(data: NoteCreateOption, note: MiNote) {
|
||||||
if (data.localOnly) return null;
|
if (data.localOnly) return null;
|
||||||
|
|
||||||
const content = this.isRenote(data) && !this.isQuote(data)
|
const content = this.isRenote(data) && !this.isQuote(data)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
|
import type { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -34,6 +35,11 @@ export class QueueService {
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||||
) {
|
) {
|
||||||
|
this.ensureRepeatJobs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private ensureRepeatJobs() {
|
||||||
this.systemQueue.add('tickCharts', {
|
this.systemQueue.add('tickCharts', {
|
||||||
}, {
|
}, {
|
||||||
repeat: { pattern: '55 * * * *' },
|
repeat: { pattern: '55 * * * *' },
|
||||||
|
@ -69,6 +75,12 @@ export class QueueService {
|
||||||
repeat: { pattern: '*/5 * * * *' },
|
repeat: { pattern: '*/5 * * * *' },
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('checkMissingScheduledNote', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '*/5 * * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -382,6 +394,18 @@ export class QueueService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createScheduledNoteJob(draftId: MiScheduledNote['id'], scheduledAt: Date) {
|
||||||
|
return this.systemQueue.add('scheduledNote', {
|
||||||
|
draftId,
|
||||||
|
}, {
|
||||||
|
jobId: `scheduledNote:${draftId}`,
|
||||||
|
delay: Math.max(scheduledAt.getTime() - Date.now(), 0) + Math.floor(Math.random() * 500 + 250),
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
|
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
|
||||||
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
||||||
|
|
|
@ -36,6 +36,7 @@ export type RolePolicies = {
|
||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
|
canScheduleNote: boolean;
|
||||||
canInitiateConversation: boolean;
|
canInitiateConversation: boolean;
|
||||||
canCreateContent: boolean;
|
canCreateContent: boolean;
|
||||||
canUpdateContent: boolean;
|
canUpdateContent: boolean;
|
||||||
|
@ -77,6 +78,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
gtlAvailable: true,
|
gtlAvailable: true,
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
canPublicNote: true,
|
canPublicNote: true,
|
||||||
|
canScheduleNote: true,
|
||||||
canInitiateConversation: true,
|
canInitiateConversation: true,
|
||||||
canCreateContent: true,
|
canCreateContent: true,
|
||||||
canUpdateContent: true,
|
canUpdateContent: true,
|
||||||
|
@ -389,6 +391,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||||
|
canScheduleNote: calc('canScheduleNote', vs => vs.some(v => v === true)),
|
||||||
canInitiateConversation: calc('canInitiateConversation', vs => vs.some(v => v === true)),
|
canInitiateConversation: calc('canInitiateConversation', vs => vs.some(v => v === true)),
|
||||||
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
||||||
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -352,7 +352,7 @@ export class ApNoteService {
|
||||||
poll,
|
poll,
|
||||||
uri: note.id,
|
uri: note.id,
|
||||||
url: url,
|
url: url,
|
||||||
}, silent);
|
}, silent) as MiNote;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name !== 'duplicated') {
|
if (err.name !== 'duplicated') {
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -20,14 +20,16 @@ import { RoleEntityService } from './RoleEntityService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
|
import type { ScheduledNoteEntityService } from './ScheduledNoteEntityService.js';
|
||||||
|
|
||||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
|
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'scheduledNotePosted'] as (typeof groupedNotificationTypes[number])[]);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationEntityService implements OnModuleInit {
|
export class NotificationEntityService implements OnModuleInit {
|
||||||
private userEntityService: UserEntityService;
|
private userEntityService: UserEntityService;
|
||||||
private noteEntityService: NoteEntityService;
|
private noteEntityService: NoteEntityService;
|
||||||
private roleEntityService: RoleEntityService;
|
private roleEntityService: RoleEntityService;
|
||||||
|
private scheduledNoteEntityService: ScheduledNoteEntityService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
@ -52,6 +54,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||||
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
||||||
|
this.scheduledNoteEntityService = this.moduleRef.get('ScheduledNoteEntityService');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,6 +87,11 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
// if the note has been deleted, don't show this notification
|
// if the note has been deleted, don't show this notification
|
||||||
if (needsNote && !noteIfNeed) return null;
|
if (needsNote && !noteIfNeed) return null;
|
||||||
|
|
||||||
|
const needsDraft = 'draftId' in notification;
|
||||||
|
const draftIfNeed = needsDraft ? this.scheduledNoteEntityService.pack(notification.draftId, { id: meId }) : undefined;
|
||||||
|
// if the draft has been deleted, don't show this notification
|
||||||
|
if (needsDraft && !draftIfNeed) return null;
|
||||||
|
|
||||||
const needsUser = 'notifierId' in notification;
|
const needsUser = 'notifierId' in notification;
|
||||||
const userIfNeed = needsUser ? (
|
const userIfNeed = needsUser ? (
|
||||||
hint?.packedUsers != null
|
hint?.packedUsers != null
|
||||||
|
@ -116,6 +124,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
note: noteIfNeed,
|
note: noteIfNeed,
|
||||||
|
draft: draftIfNeed,
|
||||||
reactions,
|
reactions,
|
||||||
});
|
});
|
||||||
} else if (notification.type === 'renote:grouped') {
|
} else if (notification.type === 'renote:grouped') {
|
||||||
|
@ -139,6 +148,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
note: noteIfNeed,
|
note: noteIfNeed,
|
||||||
|
draft: draftIfNeed,
|
||||||
users,
|
users,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -158,6 +168,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
userId: 'notifierId' in notification ? notification.notifierId : undefined,
|
userId: 'notifierId' in notification ? notification.notifierId : undefined,
|
||||||
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
||||||
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
||||||
|
...(draftIfNeed != null ? { draft: draftIfNeed } : {}),
|
||||||
...(notification.type === 'reaction' ? {
|
...(notification.type === 'reaction' ? {
|
||||||
reaction: notification.reaction,
|
reaction: notification.reaction,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { ScheduledNotesRepository } from '@/models/_.js';
|
||||||
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import type { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ScheduledNoteEntityService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async pack(
|
||||||
|
src: MiScheduledNote['id'] | MiScheduledNote,
|
||||||
|
me: { id: MiUser['id'] },
|
||||||
|
) : Promise<Packed<'NoteDraft'>> {
|
||||||
|
const item = typeof src === 'object' ? src : await this.scheduledNotesRepository.findOneByOrFail({ id: src, userId: me.id });
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
updatedAt: item.createdAt.toISOString(),
|
||||||
|
scheduledAt: item.scheduledAt?.toISOString() ?? null,
|
||||||
|
reason: item.reason ?? undefined,
|
||||||
|
channel: item.draft.channel ? {
|
||||||
|
id: item.draft.channel.id,
|
||||||
|
name: item.draft.channel.name,
|
||||||
|
} : undefined,
|
||||||
|
renote: item.draft.renote ? {
|
||||||
|
id: item.draft.renote.id,
|
||||||
|
text: (item.draft.renote.cw ?? item.draft.renote.text)?.substring(0, 100) ?? null,
|
||||||
|
user: {
|
||||||
|
id: item.draft.renote.userId,
|
||||||
|
username: item.draft.renote.user!.username,
|
||||||
|
host: item.draft.renote.user!.host,
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
reply: item.draft.reply ? {
|
||||||
|
id: item.draft.reply.id,
|
||||||
|
text: (item.draft.reply.cw ?? item.draft.reply.text)?.substring(0, 100) ?? null,
|
||||||
|
user: {
|
||||||
|
id: item.draft.reply.userId,
|
||||||
|
username: item.draft.reply.user!.username,
|
||||||
|
host: item.draft.reply.user!.host,
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
data: {
|
||||||
|
text: item.draft.text ?? null,
|
||||||
|
useCw: !!item.draft.cw,
|
||||||
|
cw: item.draft.cw ?? null,
|
||||||
|
visibility: item.draft.visibility as 'public' | 'followers' | 'home' | 'specified',
|
||||||
|
localOnly: item.draft.localOnly ?? false,
|
||||||
|
files: item.draft.files ? await this.driveFileEntityService.packMany(item.draft.files, me) : [],
|
||||||
|
poll: item.draft.poll ? { ...item.draft.poll, expiresAt: item.draft.poll.expiresAt?.getTime() ?? null, expiredAfter: null } : null,
|
||||||
|
visibleUserIds: item.draft.visibility === 'specified' ? item.draft.visibleUsers?.map(x => x.id) : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packMany(
|
||||||
|
drafts: (MiScheduledNote['id'] | MiScheduledNote)[],
|
||||||
|
me: { id: MiUser['id'] },
|
||||||
|
) : Promise<Packed<'NoteDraft'>[]> {
|
||||||
|
return (await Promise.allSettled(drafts.map(x => this.pack(x, me))))
|
||||||
|
.filter(result => result.status === 'fulfilled')
|
||||||
|
.map(result => (result as PromiseFulfilledResult<Packed<'NoteDraft'>>).value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ export const DI = {
|
||||||
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
||||||
appsRepository: Symbol('appsRepository'),
|
appsRepository: Symbol('appsRepository'),
|
||||||
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
|
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
|
||||||
|
scheduledNotesRepository: Symbol('scheduledNotesRepository'),
|
||||||
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
||||||
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
||||||
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { packedMutingSchema } from '@/models/json-schema/muting.js';
|
||||||
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
||||||
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||||
import { packedNoteSchema } from '@/models/json-schema/note.js';
|
import { packedNoteSchema } from '@/models/json-schema/note.js';
|
||||||
|
import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
|
||||||
import { packedNotificationSchema } from '@/models/json-schema/notification.js';
|
import { packedNotificationSchema } from '@/models/json-schema/notification.js';
|
||||||
import { packedPageLikeSchema, packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
import { packedPageLikeSchema, packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
||||||
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
||||||
|
@ -77,6 +78,7 @@ export const refs = {
|
||||||
Announcement: packedAnnouncementSchema,
|
Announcement: packedAnnouncementSchema,
|
||||||
App: packedAppSchema,
|
App: packedAppSchema,
|
||||||
Note: packedNoteSchema,
|
Note: packedNoteSchema,
|
||||||
|
NoteDraft: packedNoteDraftSchema,
|
||||||
NoteReaction: packedNoteReactionSchema,
|
NoteReaction: packedNoteReactionSchema,
|
||||||
NoteFavorite: packedNoteFavoriteSchema,
|
NoteFavorite: packedNoteFavoriteSchema,
|
||||||
Notification: packedNotificationSchema,
|
Notification: packedNotificationSchema,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
import { MiNote } from './Note.js';
|
import { MiNote } from './Note.js';
|
||||||
|
import { MiScheduledNote } from './ScheduledNote.js';
|
||||||
import { MiAccessToken } from './AccessToken.js';
|
import { MiAccessToken } from './AccessToken.js';
|
||||||
import { MiRole } from './Role.js';
|
import { MiRole } from './Role.js';
|
||||||
|
|
||||||
|
@ -77,6 +78,21 @@ export type MiNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
achievement: string;
|
achievement: string;
|
||||||
|
} | {
|
||||||
|
type: 'noteScheduled';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
draftId: MiScheduledNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'scheduledNotePosted';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
noteId: MiNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'scheduledNoteError';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
draftId: MiScheduledNote['id'];
|
||||||
} | {
|
} | {
|
||||||
type: 'app';
|
type: 'app';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
MiModerationLog,
|
MiModerationLog,
|
||||||
MiMuting,
|
MiMuting,
|
||||||
MiNote,
|
MiNote,
|
||||||
|
MiScheduledNote,
|
||||||
MiNoteFavorite,
|
MiNoteFavorite,
|
||||||
MiNoteReaction,
|
MiNoteReaction,
|
||||||
MiNoteThreadMuting,
|
MiNoteThreadMuting,
|
||||||
|
@ -117,6 +118,12 @@ const $avatarDecorationsRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $scheduledNotesRepository: Provider = {
|
||||||
|
provide: DI.scheduledNotesRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(MiScheduledNote),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $noteFavoritesRepository: Provider = {
|
const $noteFavoritesRepository: Provider = {
|
||||||
provide: DI.noteFavoritesRepository,
|
provide: DI.noteFavoritesRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
|
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
|
||||||
|
@ -517,6 +524,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||||
$announcementReadsRepository,
|
$announcementReadsRepository,
|
||||||
$appsRepository,
|
$appsRepository,
|
||||||
$avatarDecorationsRepository,
|
$avatarDecorationsRepository,
|
||||||
|
$scheduledNotesRepository,
|
||||||
$noteFavoritesRepository,
|
$noteFavoritesRepository,
|
||||||
$noteThreadMutingsRepository,
|
$noteThreadMutingsRepository,
|
||||||
$noteReactionsRepository,
|
$noteReactionsRepository,
|
||||||
|
@ -590,6 +598,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||||
$announcementReadsRepository,
|
$announcementReadsRepository,
|
||||||
$appsRepository,
|
$appsRepository,
|
||||||
$avatarDecorationsRepository,
|
$avatarDecorationsRepository,
|
||||||
|
$scheduledNotesRepository,
|
||||||
$noteFavoritesRepository,
|
$noteFavoritesRepository,
|
||||||
$noteThreadMutingsRepository,
|
$noteThreadMutingsRepository,
|
||||||
$noteReactionsRepository,
|
$noteReactionsRepository,
|
||||||
|
|
54
packages/backend/src/models/ScheduledNote.ts
Normal file
54
packages/backend/src/models/ScheduledNote.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||||
|
import type { NoteCreateOption } from '@/types.js';
|
||||||
|
import { id } from './util/id.js';
|
||||||
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
|
@Entity('note_scheduled')
|
||||||
|
@Index(['userId', 'scheduledAt'], { unique: true })
|
||||||
|
export class MiScheduledNote {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The created date of the Note.',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The scheduled date of the Note.',
|
||||||
|
nullable: true
|
||||||
|
})
|
||||||
|
public scheduledAt: Date | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, nullable: true,
|
||||||
|
})
|
||||||
|
public reason: string | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'The ID of author.',
|
||||||
|
})
|
||||||
|
public userId: MiUser['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => MiUser, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: MiUser | null;
|
||||||
|
|
||||||
|
@Column('jsonb')
|
||||||
|
public draft: NoteCreateOption;
|
||||||
|
|
||||||
|
constructor(data: Partial<MiScheduledNote>) {
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(data)) {
|
||||||
|
(this as any)[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||||
import { MiMuting } from '@/models/Muting.js';
|
import { MiMuting } from '@/models/Muting.js';
|
||||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
import { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||||
|
@ -108,6 +109,7 @@ export {
|
||||||
MiMuting,
|
MiMuting,
|
||||||
MiRenoteMuting,
|
MiRenoteMuting,
|
||||||
MiNote,
|
MiNote,
|
||||||
|
MiScheduledNote,
|
||||||
MiNoteFavorite,
|
MiNoteFavorite,
|
||||||
MiNoteReaction,
|
MiNoteReaction,
|
||||||
MiNoteThreadMuting,
|
MiNoteThreadMuting,
|
||||||
|
@ -181,6 +183,7 @@ export type ModerationLogsRepository = Repository<MiModerationLog>;
|
||||||
export type MutingsRepository = Repository<MiMuting>;
|
export type MutingsRepository = Repository<MiMuting>;
|
||||||
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
|
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
|
||||||
export type NotesRepository = Repository<MiNote>;
|
export type NotesRepository = Repository<MiNote>;
|
||||||
|
export type ScheduledNotesRepository = Repository<MiScheduledNote>;
|
||||||
export type NoteFavoritesRepository = Repository<MiNoteFavorite>;
|
export type NoteFavoritesRepository = Repository<MiNoteFavorite>;
|
||||||
export type NoteReactionsRepository = Repository<MiNoteReaction>;
|
export type NoteReactionsRepository = Repository<MiNoteReaction>;
|
||||||
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>;
|
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>;
|
||||||
|
|
179
packages/backend/src/models/json-schema/note-draft.ts
Normal file
179
packages/backend/src/models/json-schema/note-draft.ts
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
export const packedNoteDraftSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id'
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'date-time'
|
||||||
|
},
|
||||||
|
scheduledAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: false
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id'
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renote: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reply: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id'
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
useCw: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
cw: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
visibility: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['public', 'home', 'followers', 'specified'],
|
||||||
|
},
|
||||||
|
localOnly: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'DriveFile',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
poll: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
properties: {
|
||||||
|
choices: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
expiredAfter: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
minimum: 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visibleUserIds: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
|
@ -296,6 +296,51 @@ export const packedNotificationSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['noteScheduled'],
|
||||||
|
},
|
||||||
|
draft: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'NoteDraft',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['scheduledNotePosted'],
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['scheduledNoteError'],
|
||||||
|
},
|
||||||
|
draft: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'NoteDraft',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|
|
@ -180,6 +180,10 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
canScheduleNote: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
canInitiateConversation: {
|
canInitiateConversation: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -44,6 +44,7 @@ import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||||
import { MiMuting } from '@/models/Muting.js';
|
import { MiMuting } from '@/models/Muting.js';
|
||||||
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
import { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||||
|
@ -162,6 +163,7 @@ export const entities = [
|
||||||
MiRenoteMuting,
|
MiRenoteMuting,
|
||||||
MiBlocking,
|
MiBlocking,
|
||||||
MiNote,
|
MiNote,
|
||||||
|
MiScheduledNote,
|
||||||
MiNoteFavorite,
|
MiNoteFavorite,
|
||||||
MiNoteReaction,
|
MiNoteReaction,
|
||||||
MiNoteThreadMuting,
|
MiNoteThreadMuting,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot
|
||||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
||||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||||
|
import { CheckMissingScheduledNoteProcessorService } from './processors/CheckMissingScheduledNoteProcessorService.js';
|
||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||||
|
@ -36,6 +37,7 @@ import { ImportUserListsProcessorService } from './processors/ImportUserListsPro
|
||||||
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
||||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
||||||
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
||||||
|
import { ScheduledNoteProcessorService } from './processors/ScheduledNoteProcessorService.js';
|
||||||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||||
|
@ -52,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
ResyncChartsProcessorService,
|
ResyncChartsProcessorService,
|
||||||
CleanChartsProcessorService,
|
CleanChartsProcessorService,
|
||||||
CheckExpiredMutingsProcessorService,
|
CheckExpiredMutingsProcessorService,
|
||||||
|
CheckMissingScheduledNoteProcessorService,
|
||||||
CleanProcessorService,
|
CleanProcessorService,
|
||||||
DeleteDriveFilesProcessorService,
|
DeleteDriveFilesProcessorService,
|
||||||
ExportCustomEmojisProcessorService,
|
ExportCustomEmojisProcessorService,
|
||||||
|
@ -75,6 +78,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
CleanRemoteFilesProcessorService,
|
CleanRemoteFilesProcessorService,
|
||||||
RelationshipProcessorService,
|
RelationshipProcessorService,
|
||||||
ReportAbuseProcessorService,
|
ReportAbuseProcessorService,
|
||||||
|
ScheduledNoteProcessorService,
|
||||||
WebhookDeliverProcessorService,
|
WebhookDeliverProcessorService,
|
||||||
EndedPollNotificationProcessorService,
|
EndedPollNotificationProcessorService,
|
||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
|
|
|
@ -35,10 +35,12 @@ import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesP
|
||||||
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
||||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||||
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
||||||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
|
||||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
||||||
|
import { ScheduledNoteProcessorService } from './processors/ScheduledNoteProcessorService.js';
|
||||||
|
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||||
|
import { CheckMissingScheduledNoteProcessorService } from './processors/CheckMissingScheduledNoteProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
|
@ -113,11 +115,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
||||||
private relationshipProcessorService: RelationshipProcessorService,
|
private relationshipProcessorService: RelationshipProcessorService,
|
||||||
private reportAbuseProcessorService: ReportAbuseProcessorService,
|
private reportAbuseProcessorService: ReportAbuseProcessorService,
|
||||||
private tickChartsProcessorService: TickChartsProcessorService,
|
|
||||||
private resyncChartsProcessorService: ResyncChartsProcessorService,
|
private resyncChartsProcessorService: ResyncChartsProcessorService,
|
||||||
|
private scheduledNoteProcessorService: ScheduledNoteProcessorService,
|
||||||
|
private tickChartsProcessorService: TickChartsProcessorService,
|
||||||
private cleanChartsProcessorService: CleanChartsProcessorService,
|
private cleanChartsProcessorService: CleanChartsProcessorService,
|
||||||
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
||||||
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
||||||
|
private checkMissingScheduledNoteProcessorService: CheckMissingScheduledNoteProcessorService,
|
||||||
private cleanProcessorService: CleanProcessorService,
|
private cleanProcessorService: CleanProcessorService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
this.logger = this.queueLoggerService.logger;
|
||||||
|
@ -141,11 +145,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
//#region system
|
//#region system
|
||||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
|
case 'scheduledNote': return this.scheduledNoteProcessorService.process(job);
|
||||||
case 'tickCharts': return this.tickChartsProcessorService.process();
|
case 'tickCharts': return this.tickChartsProcessorService.process();
|
||||||
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
||||||
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
||||||
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||||
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||||
|
case 'checkMissingScheduledNote': return this.checkMissingScheduledNoteProcessorService.process();
|
||||||
case 'clean': return this.cleanProcessorService.process();
|
case 'clean': return this.cleanProcessorService.process();
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { ScheduledNotesRepository } from '@/models/_.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { acquireDistributedLock } from '@/misc/distributed-lock.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CheckMissingScheduledNoteProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
|
private queueService: QueueService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('note:scheduled');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(): Promise<void> {
|
||||||
|
this.logger.info(`checking missing scheduled note tasks`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await acquireDistributedLock(this.redisForTimelines, `note:scheduled:check`, 3 * 60 * 1000, 1, 1000);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`check is already being processed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.scheduledNotesRepository.createQueryBuilder('draft')
|
||||||
|
.where('draft.scheduledAt < now() - interval \'5 minutes\'').orderBy('draft.createdAt', 'ASC');
|
||||||
|
|
||||||
|
let lastId = '0';
|
||||||
|
while (true) {
|
||||||
|
const drafts = await query.andWhere('draft.id > :lastId', { lastId }).limit(100).getMany();
|
||||||
|
|
||||||
|
if (drafts.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const draft of drafts.filter(draft => draft.scheduledAt !== null)) {
|
||||||
|
const jobState = await this.queueService.systemQueue.getJobState(`scheduledNote:${draft.id}`);
|
||||||
|
if (jobState !== 'unknown') continue;
|
||||||
|
|
||||||
|
this.logger.warn(`found missing scheduled note task: ${draft.id}`);
|
||||||
|
await this.queueService.createScheduledNoteJob(draft.id, draft.scheduledAt!);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastId = drafts[drafts.length - 1].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { MiNote, type ScheduledNotesRepository } from '@/models/_.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { acquireDistributedLock } from '@/misc/distributed-lock.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type { ScheduledNoteJobData } from '../types.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ScheduledNoteProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
|
private notificationService: NotificationService,
|
||||||
|
private noteCreateService: NoteCreateService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('note:scheduled');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(job: Bull.Job<ScheduledNoteJobData>): Promise<string> {
|
||||||
|
this.logger.info(`processing ${job.data.draftId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await acquireDistributedLock(this.redisForTimelines, `note:scheduled:${job.data.draftId}`, 30 * 1000, 1, 100);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`draft=${job.data.draftId} is already being processed`);
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = await this.scheduledNotesRepository.findOne({
|
||||||
|
where: { id: job.data.draftId, reason: IsNull() },
|
||||||
|
relations: ['user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (draft == null) {
|
||||||
|
this.logger.warn(`draft not found: ${job.data.draftId}`);
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!draft.user || draft.user.isSuspended) {
|
||||||
|
this.logger.warn(`user is suspended: ${draft.userId}`);
|
||||||
|
await this.scheduledNotesRepository.delete({ id: draft.id });
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const note = (await this.noteCreateService.create(draft.user, {
|
||||||
|
...draft.draft,
|
||||||
|
createdAt: new Date(),
|
||||||
|
scheduledAt: null,
|
||||||
|
})) as MiNote;
|
||||||
|
|
||||||
|
await this.scheduledNotesRepository.delete({ id: draft.id });
|
||||||
|
|
||||||
|
this.notificationService.createNotification(draft.userId, "scheduledNotePosted", {
|
||||||
|
noteId: note.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return 'ok';
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof IdentifiableError) {
|
||||||
|
if ([
|
||||||
|
'e11b3a16-f543-4885-8eb1-66cad131dbfd',
|
||||||
|
'689ee33f-f97c-479a-ac49-1b9f8140af99',
|
||||||
|
'9f466dab-c856-48cd-9e65-ff90ff750580',
|
||||||
|
'85ab9bd7-3a41-4530-959d-f07073900109',
|
||||||
|
'd450b8a9-48e4-4dab-ae36-f4db763fda7c',
|
||||||
|
].includes(e.id)) {
|
||||||
|
this.logger.warn(`creating note from draft=${draft.id} failed: ${e.message}`);
|
||||||
|
|
||||||
|
await this.scheduledNotesRepository.update({ id: draft.id }, {
|
||||||
|
scheduledAt: null,
|
||||||
|
reason: e.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.notificationService.createNotification(draft.userId, "scheduledNoteError", {
|
||||||
|
draftId: draft.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
import type { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
@ -116,6 +117,10 @@ export type EndedPollNotificationJobData = {
|
||||||
noteId: MiNote['id'];
|
noteId: MiNote['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScheduledNoteJobData = {
|
||||||
|
draftId: MiScheduledNote['id'];
|
||||||
|
};
|
||||||
|
|
||||||
export type WebhookDeliverJobData = {
|
export type WebhookDeliverJobData = {
|
||||||
type: string;
|
type: string;
|
||||||
content: unknown;
|
content: unknown;
|
||||||
|
|
|
@ -287,6 +287,8 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
|
||||||
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
||||||
import * as ep___notes_create from './endpoints/notes/create.js';
|
import * as ep___notes_create from './endpoints/notes/create.js';
|
||||||
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
||||||
|
import * as ep___notes_scheduled_cancel from './endpoints/notes/scheduled/cancel.js';
|
||||||
|
import * as ep___notes_scheduled_list from './endpoints/notes/scheduled/list.js';
|
||||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||||
|
@ -682,6 +684,8 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes
|
||||||
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
|
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
|
||||||
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
|
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
|
||||||
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
|
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
|
||||||
|
const $notes_scheduled_cancel: Provider = { provide: 'ep:notes/scheduled/cancel', useClass: ep___notes_scheduled_cancel.default };
|
||||||
|
const $notes_scheduled_list: Provider = { provide: 'ep:notes/scheduled/list', useClass: ep___notes_scheduled_list.default };
|
||||||
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
|
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
|
||||||
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
|
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
|
||||||
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
||||||
|
@ -1081,6 +1085,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
|
$notes_scheduled_cancel,
|
||||||
|
$notes_scheduled_list,
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
|
@ -1474,6 +1480,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
|
$notes_scheduled_cancel,
|
||||||
|
$notes_scheduled_list,
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
$notes_featured,
|
$notes_featured,
|
||||||
|
|
|
@ -287,6 +287,8 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
|
||||||
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
||||||
import * as ep___notes_create from './endpoints/notes/create.js';
|
import * as ep___notes_create from './endpoints/notes/create.js';
|
||||||
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
||||||
|
import * as ep___notes_scheduled_cancel from './endpoints/notes/scheduled/cancel.js';
|
||||||
|
import * as ep___notes_scheduled_list from './endpoints/notes/scheduled/list.js';
|
||||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
||||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||||
|
@ -680,6 +682,8 @@ const eps = [
|
||||||
['notes/conversation', ep___notes_conversation],
|
['notes/conversation', ep___notes_conversation],
|
||||||
['notes/create', ep___notes_create],
|
['notes/create', ep___notes_create],
|
||||||
['notes/delete', ep___notes_delete],
|
['notes/delete', ep___notes_delete],
|
||||||
|
['notes/scheduled/cancel', ep___notes_scheduled_cancel],
|
||||||
|
['notes/scheduled/list', ep___notes_scheduled_list],
|
||||||
['notes/favorites/create', ep___notes_favorites_create],
|
['notes/favorites/create', ep___notes_favorites_create],
|
||||||
['notes/favorites/delete', ep___notes_favorites_delete],
|
['notes/favorites/delete', ep___notes_favorites_delete],
|
||||||
['notes/featured', ep___notes_featured],
|
['notes/featured', ep___notes_featured],
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
@ -148,6 +149,31 @@ export const meta = {
|
||||||
id: '66819f28-9525-389d-4b0a-4974363fbbbf',
|
id: '66819f28-9525-389d-4b0a-4974363fbbbf',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cannotScheduleToPast: {
|
||||||
|
message: 'Cannot schedule to the past.',
|
||||||
|
code: 'CANNOT_SCHEDULE_TO_PAST',
|
||||||
|
id: 'e577d185-8179-4a17-b47f-6093985558e6',
|
||||||
|
},
|
||||||
|
|
||||||
|
cannotScheduleToFarFuture: {
|
||||||
|
message: 'Cannot schedule to the far future.',
|
||||||
|
code: 'CANNOT_SCHEDULE_TO_FAR_FUTURE',
|
||||||
|
id: 'ea102856-e8da-4ae9-a98a-0326821bd177',
|
||||||
|
},
|
||||||
|
|
||||||
|
cannotScheduleSameTime: {
|
||||||
|
message: 'Cannot schedule multiple notes at the same time.',
|
||||||
|
code: 'CANNOT_SCHEDULE_SAME_TIME',
|
||||||
|
id: '187a8fab-fd83-4ae6-a46c-0f6f07784634',
|
||||||
|
},
|
||||||
|
|
||||||
|
rolePermissionDenied: {
|
||||||
|
message: 'You are not assigned to a required role.',
|
||||||
|
code: 'ROLE_PERMISSION_DENIED',
|
||||||
|
kind: 'permission',
|
||||||
|
id: '12f1d5d2-f7ec-4d7c-b608-e873f4b20327',
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -207,6 +233,7 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
scheduledAt: { type: 'integer', nullable: true },
|
||||||
noCreatedNote: { type: 'boolean', default: false },
|
noCreatedNote: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
// (re)note with text, files and poll are optional
|
// (re)note with text, files and poll are optional
|
||||||
|
@ -263,6 +290,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
|
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
|
||||||
const logger = this.loggerService.getLogger('api:notes:create');
|
const logger = this.loggerService.getLogger('api:notes:create');
|
||||||
|
@ -318,7 +346,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
let renote: MiNote | null = null;
|
let renote: MiNote | null = null;
|
||||||
if (ps.renoteId != null) {
|
if (ps.renoteId != null) {
|
||||||
// Fetch renote to note
|
// Fetch renote to note
|
||||||
renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
|
renote = await this.notesRepository.findOne({ where: { id: ps.renoteId }, relations: ['user'] });
|
||||||
|
|
||||||
if (renote == null) {
|
if (renote == null) {
|
||||||
logger.error('No such renote target.', { renoteId: ps.renoteId });
|
logger.error('No such renote target.', { renoteId: ps.renoteId });
|
||||||
|
@ -371,7 +399,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
let reply: MiNote | null = null;
|
let reply: MiNote | null = null;
|
||||||
if (ps.replyId != null) {
|
if (ps.replyId != null) {
|
||||||
// Fetch reply
|
// Fetch reply
|
||||||
reply = await this.notesRepository.findOneBy({ id: ps.replyId });
|
reply = await this.notesRepository.findOne({ where: { id: ps.replyId }, relations: ['user'] });
|
||||||
|
|
||||||
if (reply == null) {
|
if (reply == null) {
|
||||||
logger.error('No such reply target.', { replyId: ps.replyId });
|
logger.error('No such reply target.', { replyId: ps.replyId });
|
||||||
|
@ -384,12 +412,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||||
} else if ( me.isBot ) {
|
} else if (me.isBot && reply.user!.isBot) {
|
||||||
const replayuser = await this.usersRepository.findOneBy({ id: reply.userId });
|
|
||||||
if (replayuser?.isBot) {
|
|
||||||
throw new ApiError(meta.errors.replyingToAnotherBot);
|
throw new ApiError(meta.errors.replyingToAnotherBot);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (reply.userId !== me.id) {
|
if (reply.userId !== me.id) {
|
||||||
|
@ -427,10 +452,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scheduledAt: Date | null = null;
|
||||||
|
if (ps.scheduledAt) {
|
||||||
|
const now = new Date();
|
||||||
|
scheduledAt = new Date(ps.scheduledAt);
|
||||||
|
scheduledAt.setMilliseconds(0);
|
||||||
|
|
||||||
|
if (scheduledAt < now) {
|
||||||
|
logger.error('Cannot schedule to the past.', { scheduledAt });
|
||||||
|
throw new ApiError(meta.errors.cannotScheduleToPast);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledAt.getTime() - now.getTime() > ms('1year')) {
|
||||||
|
logger.error('Cannot schedule to the far future.', { scheduledAt });
|
||||||
|
throw new ApiError(meta.errors.cannotScheduleToFarFuture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 投稿を作成
|
// 投稿を作成
|
||||||
try {
|
try {
|
||||||
const note = await this.noteCreateService.create(me, {
|
const note = await this.noteCreateService.create(me, {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
scheduledAt: ps.scheduledAt ? scheduledAt : null,
|
||||||
files: files,
|
files: files,
|
||||||
poll: ps.poll ? {
|
poll: ps.poll ? {
|
||||||
choices: ps.poll.choices,
|
choices: ps.poll.choices,
|
||||||
|
@ -454,10 +497,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// 1分間、リクエストの処理結果を記録
|
// 1分間、リクエストの処理結果を記録
|
||||||
await this.redisForTimelines.set(`note:idempotent:${me.id}:${hash}`, note.id, 'EX', 60);
|
await this.redisForTimelines.set(`note:idempotent:${me.id}:${hash}`, note.id, 'EX', 60);
|
||||||
|
|
||||||
|
if (!scheduledAt) {
|
||||||
logger.info('Successfully created a note.', { noteId: note.id });
|
logger.info('Successfully created a note.', { noteId: note.id });
|
||||||
if (ps.noCreatedNote) return;
|
} else {
|
||||||
|
this.notificationService.createNotification(me.id, "noteScheduled", {
|
||||||
|
draftId: note.id,
|
||||||
|
});
|
||||||
|
logger.info('Successfully scheduled a note.', { draftId: note.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.noCreatedNote || scheduledAt) return;
|
||||||
else return {
|
else return {
|
||||||
createdNote: await this.noteEntityService.pack(note, me),
|
createdNote: await this.noteEntityService.pack(note as MiNote, me),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除
|
// エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除
|
||||||
|
@ -468,6 +519,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (err instanceof IdentifiableError) {
|
if (err instanceof IdentifiableError) {
|
||||||
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
|
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
|
||||||
if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') throw new ApiError(meta.errors.containsTooManyMentions);
|
if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') throw new ApiError(meta.errors.containsTooManyMentions);
|
||||||
|
if (err.id === '7cc42034-f7ab-4f7c-87b4-e00854479080') throw new ApiError(meta.errors.rolePermissionDenied);
|
||||||
|
if (err.id === '5ea8e4f5-9d64-4e6c-92b8-9e2b5a4756bc') throw new ApiError(meta.errors.cannotScheduleSameTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { ScheduledNotesRepository } from '@/models/_.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import ms from 'ms';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['notes'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireRolePolicy: 'canCreateContent',
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: ms('1hour'),
|
||||||
|
max: 300,
|
||||||
|
},
|
||||||
|
|
||||||
|
kind: 'write:notes',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchDraft: {
|
||||||
|
message: 'No such draft',
|
||||||
|
code: 'NO_SUCH_DRAFT',
|
||||||
|
id: '91c2ad21-fb45-4f2a-ba4c-ea749b262947',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
draftId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['draftId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
|
private queueService: QueueService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const draft = await this.scheduledNotesRepository.findOneBy({ id: ps.draftId, userId: me.id });
|
||||||
|
if (!draft) throw new ApiError(meta.errors.noSuchDraft);
|
||||||
|
|
||||||
|
await this.queueService.systemQueue.remove(`scheduledNote:${draft.id}`);
|
||||||
|
await this.scheduledNotesRepository.delete({ id: draft.id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { ScheduledNotesRepository } from '@/models/_.js';
|
||||||
|
import { ScheduledNoteEntityService } from '@/core/entities/ScheduledNoteEntityService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['notes'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireRolePolicy: 'canScheduleNote',
|
||||||
|
|
||||||
|
kind: 'write:notes',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'NoteDraft',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
offset: { type: 'integer', default: 0 },
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
|
private scheduledNoteEntityService: ScheduledNoteEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const query = this.scheduledNotesRepository.createQueryBuilder('draft').where('draft.userId = :userId', { userId: me.id });
|
||||||
|
const drafts = await query.orderBy('draft.scheduledAt', 'ASC', 'NULLS FIRST').offset(ps.offset).limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
return await this.scheduledNoteEntityService.packMany(drafts, me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,13 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
|
import type { IPoll } from '@/models/Poll.js';
|
||||||
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
|
import type { MiApp } from '@/models/App.js';
|
||||||
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* note - 通知オンにしているユーザーが投稿した
|
* note - 通知オンにしているユーザーが投稿した
|
||||||
* follow - フォローされた
|
* follow - フォローされた
|
||||||
|
@ -16,6 +23,9 @@
|
||||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||||
* roleAssigned - ロールが付与された
|
* roleAssigned - ロールが付与された
|
||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
|
* noteScheduled - 予約投稿が予約された
|
||||||
|
* scheduledNotePosted - 予約投稿が投稿された
|
||||||
|
* scheduledNoteError - 予約投稿がエラーになった
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
* test - テスト通知(サーバー側)
|
* test - テスト通知(サーバー側)
|
||||||
*/
|
*/
|
||||||
|
@ -32,6 +42,9 @@ export const notificationTypes = [
|
||||||
'followRequestAccepted',
|
'followRequestAccepted',
|
||||||
'roleAssigned',
|
'roleAssigned',
|
||||||
'achievementEarned',
|
'achievementEarned',
|
||||||
|
'noteScheduled',
|
||||||
|
'scheduledNotePosted',
|
||||||
|
'scheduledNoteError',
|
||||||
'app',
|
'app',
|
||||||
'test',
|
'test',
|
||||||
] as const;
|
] as const;
|
||||||
|
@ -338,6 +351,36 @@ export type ModerationLogPayloads = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MinimumUser = {
|
||||||
|
id: MiUser['id'];
|
||||||
|
host: MiUser['host'];
|
||||||
|
username: MiUser['username'];
|
||||||
|
uri: MiUser['uri'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NoteCreateOption = {
|
||||||
|
createdAt?: Date | null;
|
||||||
|
scheduledAt?: Date | null;
|
||||||
|
name?: string | null;
|
||||||
|
text?: string | null;
|
||||||
|
reply?: MiNote | null;
|
||||||
|
renote?: MiNote | null;
|
||||||
|
files?: MiDriveFile[] | null;
|
||||||
|
poll?: IPoll | null;
|
||||||
|
localOnly?: boolean | null;
|
||||||
|
reactionAcceptance?: MiNote['reactionAcceptance'];
|
||||||
|
cw?: string | null;
|
||||||
|
visibility?: string;
|
||||||
|
visibleUsers?: MinimumUser[] | null;
|
||||||
|
channel?: MiChannel | null;
|
||||||
|
apMentions?: MinimumUser[] | null;
|
||||||
|
apHashtags?: string[] | null;
|
||||||
|
apEmojis?: string[] | null;
|
||||||
|
uri?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
app?: MiApp | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type Serialized<T> = {
|
export type Serialized<T> = {
|
||||||
[K in keyof T]:
|
[K in keyof T]:
|
||||||
T[K] extends Date
|
T[K] extends Date
|
||||||
|
|
|
@ -10,14 +10,18 @@
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ i18n.ts.drafts }}
|
{{ i18n.ts.drafts }}
|
||||||
</template>
|
</template>
|
||||||
<div style="display: flex; flex-direction: column">
|
<MkTab v-if="$i!.policies.canScheduleNote" v-model="tab" style="margin-bottom: var(--margin);">
|
||||||
|
<option value="unsent">{{ i18n.ts.unsent }}</option>
|
||||||
|
<option value="scheduled">{{ i18n.ts.scheduled }}</option>
|
||||||
|
</MkTab>
|
||||||
|
<div v-if="tab === 'unsent'" style="display: flex; flex-direction: column">
|
||||||
<div v-if="drafts.length === 0" class="empty">
|
<div v-if="drafts.length === 0" class="empty">
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img :src="infoImageUrl" class="_ghost"/>
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
<div>{{ i18n.ts.nothing }}</div>
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="draft in drafts" :key="draft.id" :class="$style.draftItem">
|
<div v-for="draft in drafts" :key="draft.id" :class="[$style.draftItem, $style.draftItemHover]">
|
||||||
<div :class="$style.draftNote" @click="selectDraft(draft.id)">
|
<div :class="$style.draftNote" @click="selectDraft(draft.id)">
|
||||||
<div :class="$style.draftNoteHeader">
|
<div :class="$style.draftNoteHeader">
|
||||||
<div :class="$style.draftNoteDestination">
|
<div :class="$style.draftNoteDestination">
|
||||||
|
@ -35,7 +39,13 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.draftNoteInfo">
|
<div :class="$style.draftNoteInfo">
|
||||||
|
<div style="display: flex; gap: 4px">
|
||||||
|
<div v-if="draft.scheduledAt" style="display: flex; opacity: 0.6">
|
||||||
|
<span><i class="ti ti-calendar-clock" style="margin-right: 4px;"/></span>
|
||||||
|
<MkTime :time="draft.scheduledAt"/>
|
||||||
|
</div>
|
||||||
<MkTime :time="draft.createdAt" colored />
|
<MkTime :time="draft.createdAt" colored />
|
||||||
|
</div>
|
||||||
<span v-if="draft.visibility !== 'public'" :title="i18n.ts._visibility[draft.visibility]" style="margin-left: 0.5em">
|
<span v-if="draft.visibility !== 'public'" :title="i18n.ts._visibility[draft.visibility]" style="margin-left: 0.5em">
|
||||||
<i v-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
<i v-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
@ -56,24 +66,94 @@
|
||||||
<MkSubNoteContent :class="$style.draftNoteText" :note="draft" />
|
<MkSubNoteContent :class="$style.draftNoteText" :note="draft" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button :class="$style.delete" class="_button" @click="removeDraft(draft.id)">
|
<button v-tooltip="i18n.ts.delete" :class="$style.button" class="_button" @click="removeDraft(draft.id)">
|
||||||
<i class="ti ti-trash"></i>
|
<i class="ti ti-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<MkPagination v-if="tab === 'scheduled'" ref="scheduledPaginationEl" :pagination="scheduledPagination">
|
||||||
|
<template #empty>
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ items }">
|
||||||
|
<div v-for="draft in items.map(x => convertNoteDraftToNoteCompat(x))" :key="draft.id" :class="$style.draftItem">
|
||||||
|
<div :class="$style.draftNote">
|
||||||
|
<div :class="$style.draftNoteHeader">
|
||||||
|
<div :class="$style.draftNoteDestination">
|
||||||
|
<span v-if="draft.channel" style="opacity: 0.7; padding-right: 0.5em">
|
||||||
|
<i class="ti ti-device-tv"></i> {{ draft.channel.name }}
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.renote">
|
||||||
|
<i class="ti ti-quote"></i> <MkAcct :user="draft.renote.user" /> <span>{{ draft.renote.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="draft.reply">
|
||||||
|
<i class="ti ti-arrow-back-up"></i> <MkAcct :user="draft.reply.user" /> <span>{{ draft.reply.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.draftNoteInfo">
|
||||||
|
<div style="display: flex; gap: 4px">
|
||||||
|
<div v-if="draft.scheduledAt" style="display: flex; opacity: 0.6">
|
||||||
|
<span><i class="ti ti-calendar-clock" style="margin-right: 4px;"/></span>
|
||||||
|
<MkTime :time="draft.scheduledAt"/>
|
||||||
|
</div>
|
||||||
|
<div v-else style="display: flex; opacity: 0.6">
|
||||||
|
<span><i class="ti ti-exclamation-circle"/></span>
|
||||||
|
</div>
|
||||||
|
<MkTime :time="draft.createdAt" colored />
|
||||||
|
</div>
|
||||||
|
<span v-if="draft.visibility !== 'public'" :title="i18n.ts._visibility[draft.visibility]" style="margin-left: 0.5em">
|
||||||
|
<i v-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']" style="margin-left: 0.5em">
|
||||||
|
<i class="ti ti-rocket-off"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.channel" :title="draft.channel.name" style="margin-left: 0.5em">
|
||||||
|
<i class="ti ti-device-tv"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p v-if="!!draft.cw" :class="$style.draftNoteCw">
|
||||||
|
<Mfm :text="draft.cw" />
|
||||||
|
</p>
|
||||||
|
<MkSubNoteContent :class="$style.draftNoteText" :note="draft" />
|
||||||
|
<div v-if="draft.reason" style="opacity: 0.6; margin-top: 4px">
|
||||||
|
{{ i18n.ts.error }}: {{ draft.reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button v-tooltip="i18n.ts.unschedule" :class="$style.button" class="_button" @click="unschedule(draft.id)">
|
||||||
|
<i class="ti ti-calendar-x"></i>
|
||||||
|
</button>
|
||||||
|
<button v-tooltip="i18n.ts.delete" :class="$style.button" class="_button" @click="cancelScheduled(draft.id)">
|
||||||
|
<i class="ti ti-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onActivated, onMounted, ref, shallowRef } from 'vue';
|
import { onActivated, onMounted, ref, shallowRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import type { NoteDraftItem } from '@/types/note-draft-item.js';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import MkTab from '@/components/MkTab.vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { canceled: true } | { canceled: false; selected: string | undefined }): void;
|
(ev: 'done', v: { canceled: true } | { canceled: false; selected: string | undefined }): void;
|
||||||
|
@ -81,23 +161,30 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
const tab = ref('unsent');
|
||||||
|
|
||||||
const drafts = ref<(Misskey.entities.Note & { useCw: boolean })[]>([]);
|
const drafts = ref<(Misskey.entities.Note & { useCw: boolean, scheduledAt: string })[]>([]);
|
||||||
|
|
||||||
onMounted(loadDrafts);
|
onMounted(loadDrafts);
|
||||||
onActivated(loadDrafts);
|
onActivated(loadDrafts);
|
||||||
|
|
||||||
function loadDrafts() {
|
function convertNoteDraftToNoteCompat(draft: Misskey.entities.NoteDraft, key?: string) {
|
||||||
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
return {
|
||||||
drafts.value = Object.keys(stored).map((key) => ({
|
...(draft.data as Misskey.entities.Note & { useCw: boolean }),
|
||||||
...(stored[key].data as Misskey.entities.Note & { useCw: boolean }),
|
id: key ?? draft.id,
|
||||||
id: key,
|
createdAt: draft.updatedAt,
|
||||||
createdAt: stored[key].updatedAt,
|
scheduledAt: draft.scheduledAt,
|
||||||
channel: stored[key].channel as Misskey.entities.Channel,
|
reason: draft.reason,
|
||||||
renote: stored[key].renote as Misskey.entities.Note,
|
channel: draft.channel as Misskey.entities.Channel,
|
||||||
reply: stored[key].reply as Misskey.entities.Note,
|
renote: draft.renote as Misskey.entities.Note,
|
||||||
|
reply: draft.reply as Misskey.entities.Note,
|
||||||
user: $i as Misskey.entities.User,
|
user: $i as Misskey.entities.User,
|
||||||
}));
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDrafts() {
|
||||||
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
drafts.value = Object.keys(stored).map((key) => convertNoteDraftToNoteCompat(stored[key], key));
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDraft(draft: string) {
|
function selectDraft(draft: string) {
|
||||||
|
@ -105,7 +192,7 @@ function selectDraft(draft: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeDraft(draft: string) {
|
function removeDraft(draft: string) {
|
||||||
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
delete stored[draft];
|
delete stored[draft];
|
||||||
miLocalStorage.setItem('drafts', JSON.stringify(stored));
|
miLocalStorage.setItem('drafts', JSON.stringify(stored));
|
||||||
|
@ -113,12 +200,53 @@ function removeDraft(draft: string) {
|
||||||
loadDrafts();
|
loadDrafts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unschedule(draft: string) {
|
||||||
|
const item = scheduledPaginationEl.value!.items.find(x => x.id === draft);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
let key = item.channel ? `channel:${item.channel.id}` : '';
|
||||||
|
if (item.renote) {
|
||||||
|
key += `renote:${item.renote.id}`;
|
||||||
|
} else if (item.reply) {
|
||||||
|
key += `reply:${item.reply.id}`;
|
||||||
|
} else {
|
||||||
|
key += `note:${item.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
|
stored[key] = item as unknown as Misskey.entities.NoteDraft;
|
||||||
|
miLocalStorage.setItem('drafts', JSON.stringify(stored));
|
||||||
|
|
||||||
|
cancelScheduled(item.id);
|
||||||
|
loadDrafts();
|
||||||
|
tab.value = 'unsent';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelScheduled(draft: string) {
|
||||||
|
os.apiWithDialog('notes/scheduled/cancel', {
|
||||||
|
draftId: draft,
|
||||||
|
}).then(() => {
|
||||||
|
scheduledPaginationEl.value?.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function done(canceled: boolean, selected?: string): void {
|
function done(canceled: boolean, selected?: string): void {
|
||||||
emit('done', { canceled, selected } as
|
emit('done', { canceled, selected } as
|
||||||
| { canceled: true }
|
| { canceled: true }
|
||||||
| { canceled: false; selected: string | undefined });
|
| { canceled: false; selected: string | undefined });
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scheduledPaginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
|
const scheduledPagination = {
|
||||||
|
endpoint: 'notes/scheduled/list' as const,
|
||||||
|
offsetMode: true,
|
||||||
|
limit: 10,
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -126,10 +254,12 @@ function done(canceled: boolean, selected?: string): void {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 8px 0 8px 0;
|
padding: 8px 0 8px 0;
|
||||||
border-bottom: 1px solid var(--divider);
|
border-bottom: 1px solid var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftItemHover {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background-color: var(--accentedBg);
|
background: var(--accentedBg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,15 +299,19 @@ function done(canceled: boolean, selected?: string): void {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete {
|
.button {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--buttonBg);
|
background: var(--buttonBg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--buttonHoverBg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
|
@ -25,6 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.t_quote]: notification.type === 'quote',
|
[$style.t_quote]: notification.type === 'quote',
|
||||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||||
|
[$style.t_noteScheduled]: notification.type === 'noteScheduled',
|
||||||
|
[$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted',
|
||||||
|
[$style.t_scheduledNoteError]: notification.type === 'scheduledNoteError',
|
||||||
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||||
}]"
|
}]"
|
||||||
>
|
>
|
||||||
|
@ -37,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
|
<i v-else-if="notification.type === 'noteScheduled'" class="ti ti-calendar-clock"></i>
|
||||||
|
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-calendar-check"></i>
|
||||||
|
<i v-else-if="notification.type === 'scheduledNoteError'" class="ti ti-calendar-exclamation"></i>
|
||||||
<template v-else-if="notification.type === 'roleAssigned'">
|
<template v-else-if="notification.type === 'roleAssigned'">
|
||||||
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||||
<i v-else class="ti ti-badges"></i>
|
<i v-else class="ti ti-badges"></i>
|
||||||
|
@ -52,16 +58,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.tail">
|
<div :class="$style.tail">
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
<span v-if="notification.type === 'pollEnded'" :class="$style.headerName">{{ i18n.ts._notification.pollEnded }}</span>
|
||||||
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
<span v-else-if="notification.type === 'note'" :class="$style.headerName">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
||||||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
<span v-else-if="notification.type === 'roleAssigned'" :class="$style.headerName">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'" :class="$style.headerName">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'noteScheduled'" :class="$style.headerName">{{ i18n.ts._notification.noteScheduled }}</span>
|
||||||
|
<span v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.headerName">{{ i18n.ts._notification.scheduledNotePosted }}</span>
|
||||||
|
<span v-else-if="notification.type === 'scheduledNoteError'" :class="$style.headerName">{{ i18n.ts._notification.scheduledNoteError }}</span>
|
||||||
|
<span v-else-if="notification.type === 'test'" :class="$style.headerName">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="$style.headerName">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
<span v-else-if="notification.type === 'reaction:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
<span v-else-if="notification.type === 'renote:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||||
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
|
<span v-else-if="notification.type === 'app'" :class="$style.headerName">{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
|
@ -98,6 +107,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||||
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<div v-else-if="notification.type === 'noteScheduled'">
|
||||||
|
<Mfm :class="$style.text" :text="getNoteSummary(notification.draft.data as unknown as Misskey.entities.Note)" :plain="true" :nowrap="true"/>
|
||||||
|
<div v-if="notification.draft.scheduledAt" :class="$style.text" style="opacity: 0.6;">
|
||||||
|
<span><i class="ti ti-calendar-clock" style="margin-right: 4px;"/></span>
|
||||||
|
<MkTime :time="notification.draft.scheduledAt"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
|
</MkA>
|
||||||
|
<div v-else-if="notification.type === 'scheduledNoteError'">
|
||||||
|
<Mfm :class="$style.text" :text="getNoteSummary(notification.draft.data as unknown as Misskey.entities.Note)" :plain="true" :nowrap="true"/>
|
||||||
|
<div v-if="notification.draft.reason" :class="$style.text" style="opacity: 0.6;">
|
||||||
|
<span><i class="ti ti-exclamation-circle" style="margin-right: 4px;"/></span>
|
||||||
|
{{ notification.draft.reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template v-else-if="notification.type === 'follow'">
|
<template v-else-if="notification.type === 'follow'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||||
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
|
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
|
||||||
|
@ -300,6 +326,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.t_noteScheduled, .t_scheduledNotePosted, .t_scheduledNoteError {
|
||||||
|
padding: 3px;
|
||||||
|
background: var(--eventOther);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.t_roleAssigned {
|
.t_roleAssigned {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: var(--eventOther);
|
background: var(--eventOther);
|
||||||
|
|
|
@ -81,6 +81,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||||
</div>
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="mk-input-text" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="mk-input-text" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
|
<div v-if="scheduledTime" :class="$style.scheduledTime">
|
||||||
|
<div><i class="ti ti-calendar-clock"></i></div>
|
||||||
|
<span>{{ i18n.tsx.willBePostedAt({ x: dateTimeFormat.format(scheduledTime) }) }}</span>
|
||||||
|
<button class="_button" style="margin-left: auto" @click="scheduledTime = null"><i class="ti ti-x"></i></button>
|
||||||
|
</div>
|
||||||
<MkInfo v-if="files.length > 0" warn :class="$style.guidelineInfo" :rounded="false"><Mfm :text="i18n.tsx._postForm.guidelineInfo({ tosUrl: instance.tosUrl, nsfwGuideUrl })"/></MkInfo>
|
<MkInfo v-if="files.length > 0" warn :class="$style.guidelineInfo" :rounded="false"><Mfm :text="i18n.tsx._postForm.guidelineInfo({ tosUrl: instance.tosUrl, nsfwGuideUrl })"/></MkInfo>
|
||||||
<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"/>
|
||||||
|
@ -94,6 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<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>
|
||||||
|
<button v-if="$i.policies.canScheduleNote" v-tooltip="i18n.ts.setScheduledTime" class="_button" :class="$style.footerButton" @click="setScheduledTime"><i class="ti ti-calendar-clock"></i></button>
|
||||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||||
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
||||||
|
@ -115,7 +121,6 @@ import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import { toASCII } from 'punycode.js';
|
import { toASCII } from 'punycode.js';
|
||||||
import type { NoteDraftItem } from '@/types/note-draft-item.js';
|
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
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';
|
||||||
|
@ -138,8 +143,8 @@ import { uploadFile } from '@/scripts/upload.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
|
||||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
@ -211,6 +216,7 @@ if (props.initialVisibleUsers) {
|
||||||
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
||||||
}
|
}
|
||||||
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
|
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
|
||||||
|
const scheduledTime = ref<Date | null>(null);
|
||||||
const autocompleteTextareaInput = ref<Autocomplete | null>(null);
|
const autocompleteTextareaInput = ref<Autocomplete | null>(null);
|
||||||
const autocompleteCwInput = ref<Autocomplete | null>(null);
|
const autocompleteCwInput = ref<Autocomplete | null>(null);
|
||||||
const autocompleteHashtagsInput = ref<Autocomplete | null>(null);
|
const autocompleteHashtagsInput = ref<Autocomplete | null>(null);
|
||||||
|
@ -259,11 +265,15 @@ const placeholder = computed((): string => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitText = computed((): string => {
|
const submitText = computed((): string => {
|
||||||
return renote.value
|
if (scheduledTime.value) {
|
||||||
? i18n.ts.quote
|
return i18n.ts.schedule;
|
||||||
: reply.value
|
} else if (renote.value) {
|
||||||
? i18n.ts.reply
|
return i18n.ts.quote;
|
||||||
: i18n.ts.note;
|
} else if (reply.value) {
|
||||||
|
return i18n.ts.reply;
|
||||||
|
} else {
|
||||||
|
return i18n.ts.note;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const textLength = computed((): number => {
|
const textLength = computed((): number => {
|
||||||
|
@ -389,6 +399,7 @@ function watchForDraft() {
|
||||||
watch(files, () => saveDraft(), { deep: true });
|
watch(files, () => saveDraft(), { deep: true });
|
||||||
watch(visibility, () => saveDraft());
|
watch(visibility, () => saveDraft());
|
||||||
watch(localOnly, () => saveDraft());
|
watch(localOnly, () => saveDraft());
|
||||||
|
watch(scheduledTime, () => saveDraft());
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkMissingMention() {
|
function checkMissingMention() {
|
||||||
|
@ -583,10 +594,25 @@ function removeVisibleUser(user) {
|
||||||
visibleUsers.value = erase(user, visibleUsers.value);
|
visibleUsers.value = erase(user, visibleUsers.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setScheduledTime() {
|
||||||
|
const { canceled, result: date } = await os.inputDateTime({
|
||||||
|
title: i18n.ts.setScheduledTime,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
scheduledTime.value = date;
|
||||||
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
text.value = '';
|
text.value = '';
|
||||||
|
useCw.value = false;
|
||||||
|
cw.value = null;
|
||||||
|
visibility.value = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
|
||||||
|
localOnly.value = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
|
||||||
files.value = [];
|
files.value = [];
|
||||||
poll.value = null;
|
poll.value = null;
|
||||||
|
visibleUsers.value = [];
|
||||||
|
scheduledTime.value = null;
|
||||||
quoteId.value = null;
|
quoteId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -694,10 +720,16 @@ function onDrop(ev: DragEvent): void {
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
if (props.instant || props.mock) return;
|
if (props.instant || props.mock) return;
|
||||||
|
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
let scheduledAt = scheduledTime.value ?? null;
|
||||||
|
if (scheduledAt && (isNaN(scheduledAt.getTime()) || scheduledAt.getTime() < Date.now())) {
|
||||||
|
scheduledAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
draftData[draftKey.value] = {
|
draftData[draftKey.value] = {
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
scheduledAt: scheduledAt?.toISOString() ?? null,
|
||||||
channel: channel.value ? {
|
channel: channel.value ? {
|
||||||
id: channel.value.id,
|
id: channel.value.id,
|
||||||
name: channel.value.name,
|
name: channel.value.name,
|
||||||
|
@ -737,7 +769,7 @@ function saveDraft() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteDraft() {
|
function deleteDraft() {
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
|
|
||||||
delete draftData[draftKey.value];
|
delete draftData[draftKey.value];
|
||||||
|
|
||||||
|
@ -777,7 +809,7 @@ async function openDrafts() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDraft(exactMatch = false) {
|
function loadDraft(exactMatch = false) {
|
||||||
const drafts = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
const drafts = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, Misskey.entities.NoteDraft>;
|
||||||
const scope = exactMatch ? draftKey.value : draftKey.value.replace(`note:${draftId.value}`, 'note:');
|
const scope = exactMatch ? draftKey.value : draftKey.value.replace(`note:${draftId.value}`, 'note:');
|
||||||
const draft = Object.entries(drafts).filter(([k]) => k.startsWith(scope))
|
const draft = Object.entries(drafts).filter(([k]) => k.startsWith(scope))
|
||||||
.map(r => ({ key: r[0], value: { ...r[1], updatedAt: new Date(r[1].updatedAt).getTime() } }))
|
.map(r => ({ key: r[0], value: { ...r[1], updatedAt: new Date(r[1].updatedAt).getTime() } }))
|
||||||
|
@ -788,7 +820,11 @@ function loadDraft(exactMatch = false) {
|
||||||
draftId.value = draft.key.replace(scope, '');
|
draftId.value = draft.key.replace(scope, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
text.value = draft.value.data.text;
|
scheduledTime.value = draft.value.scheduledAt ? new Date(draft.value.scheduledAt) : null;
|
||||||
|
if (scheduledTime.value && (isNaN(scheduledTime.value.getTime()) || scheduledTime.value.getTime() < Date.now())) {
|
||||||
|
scheduledTime.value = null;
|
||||||
|
}
|
||||||
|
text.value = draft.value.data.text ?? '';
|
||||||
useCw.value = draft.value.data.useCw;
|
useCw.value = draft.value.data.useCw;
|
||||||
cw.value = draft.value.data.cw;
|
cw.value = draft.value.data.cw;
|
||||||
visibility.value = draft.value.data.visibility;
|
visibility.value = draft.value.data.visibility;
|
||||||
|
@ -872,6 +908,7 @@ async function post(ev?: MouseEvent) {
|
||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
|
scheduledAt: scheduledTime.value?.getTime() ?? undefined,
|
||||||
noCreatedNote: true,
|
noCreatedNote: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1079,6 +1116,7 @@ onMounted(() => {
|
||||||
visibility.value = init.visibility;
|
visibility.value = init.visibility;
|
||||||
localOnly.value = init.localOnly ?? false;
|
localOnly.value = init.localOnly ?? false;
|
||||||
quoteId.value = init.renote ? init.renote.id : null;
|
quoteId.value = init.renote ? init.renote.id : null;
|
||||||
|
scheduledTime.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => watchForDraft());
|
nextTick(() => watchForDraft());
|
||||||
|
@ -1352,6 +1390,13 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scheduledTime {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 24px;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--infoBg);
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 16px 16px 16px;
|
padding: 0 16px 16px 16px;
|
||||||
|
|
|
@ -67,6 +67,9 @@ export const notificationTypes = [
|
||||||
'followRequestAccepted',
|
'followRequestAccepted',
|
||||||
'roleAssigned',
|
'roleAssigned',
|
||||||
'achievementEarned',
|
'achievementEarned',
|
||||||
|
'noteScheduled',
|
||||||
|
'scheduledNotePosted',
|
||||||
|
'scheduledNoteError',
|
||||||
'app',
|
'app',
|
||||||
] as const;
|
] as const;
|
||||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
|
@ -75,6 +78,7 @@ export const ROLE_POLICIES = [
|
||||||
'gtlAvailable',
|
'gtlAvailable',
|
||||||
'ltlAvailable',
|
'ltlAvailable',
|
||||||
'canPublicNote',
|
'canPublicNote',
|
||||||
|
'canScheduleNote',
|
||||||
'canInitiateConversation',
|
'canInitiateConversation',
|
||||||
'canCreateContent',
|
'canCreateContent',
|
||||||
'canUpdateContent',
|
'canUpdateContent',
|
||||||
|
|
|
@ -427,28 +427,36 @@ export function inputNumber(props: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inputDate(props: {
|
export function inputDateTime(props: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
placeholder?: string | null;
|
placeholder?: string | null;
|
||||||
default?: string | null;
|
default?: Date | null;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
canceled: true; result: undefined;
|
canceled: true; result: undefined;
|
||||||
} | {
|
} | {
|
||||||
canceled: false; result: Date;
|
canceled: false; result: Date;
|
||||||
}> {
|
}> {
|
||||||
|
const defaultValue = props.default ?? new Date();
|
||||||
|
defaultValue.setMinutes(defaultValue.getMinutes() - defaultValue.getTimezoneOffset());
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, {
|
popup(MkDialog, {
|
||||||
title: props.title ?? undefined,
|
title: props.title ?? undefined,
|
||||||
text: props.text ?? undefined,
|
text: props.text ?? undefined,
|
||||||
input: {
|
input: {
|
||||||
type: 'date',
|
type: 'datetime-local',
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
default: props.default ?? null,
|
default: defaultValue.toISOString().slice(0, -5),
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
|
const date = result ? new Date(result.result) : undefined;
|
||||||
|
if (date && !isNaN(date.getTime())) {
|
||||||
|
resolve({ result: date, canceled: false });
|
||||||
|
} else {
|
||||||
|
resolve({ result: undefined, canceled: true });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
});
|
});
|
||||||
|
|
|
@ -165,6 +165,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canScheduleNote, 'canScheduleNote'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canScheduleNote }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.canScheduleNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.canScheduleNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canScheduleNote)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.canScheduleNote.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.canScheduleNote.value" :disabled="role.policies.canScheduleNote.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.canScheduleNote.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -48,6 +48,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canScheduleNote, 'canScheduleNote'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canScheduleNote }}</template>
|
||||||
|
<template #suffix>{{ policies.canScheduleNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
<MkSwitch v-model="policies.canScheduleNote">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
||||||
<template #suffix>{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
|
|
@ -57,7 +57,7 @@ function top() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function timetravel() {
|
async function timetravel() {
|
||||||
const { canceled, result: date } = await os.inputDate({
|
const { canceled, result: date } = await os.inputDateTime({
|
||||||
title: i18n.ts.date,
|
title: i18n.ts.date,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
|
@ -221,7 +221,7 @@ function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue:
|
||||||
}
|
}
|
||||||
|
|
||||||
async function timetravel(): Promise<void> {
|
async function timetravel(): Promise<void> {
|
||||||
const { canceled, result: date } = await os.inputDate({
|
const { canceled, result: date } = await os.inputDateTime({
|
||||||
title: i18n.ts.date,
|
title: i18n.ts.date,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
|
||||||
|
|
||||||
export type NoteDraftItem = {
|
|
||||||
updatedAt: string;
|
|
||||||
channel?: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
renote?: {
|
|
||||||
id: string;
|
|
||||||
text: string | null;
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
host: string | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
reply?: {
|
|
||||||
id: string;
|
|
||||||
text: string | null;
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
host: string | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
data: {
|
|
||||||
text: string;
|
|
||||||
useCw: boolean;
|
|
||||||
cw: string | null;
|
|
||||||
visibility: 'public' | 'followers' | 'home' | 'specified';
|
|
||||||
localOnly: boolean;
|
|
||||||
files: Misskey.entities.DriveFile[];
|
|
||||||
poll: PollEditorModelValue | null;
|
|
||||||
visibleUserIds?: string[];
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1640,6 +1640,9 @@ declare namespace entities {
|
||||||
NotesCreateRequest,
|
NotesCreateRequest,
|
||||||
NotesCreateResponse,
|
NotesCreateResponse,
|
||||||
NotesDeleteRequest,
|
NotesDeleteRequest,
|
||||||
|
NotesScheduledCancelRequest,
|
||||||
|
NotesScheduledListRequest,
|
||||||
|
NotesScheduledListResponse,
|
||||||
NotesFavoritesCreateRequest,
|
NotesFavoritesCreateRequest,
|
||||||
NotesFavoritesDeleteRequest,
|
NotesFavoritesDeleteRequest,
|
||||||
NotesFeaturedRequest,
|
NotesFeaturedRequest,
|
||||||
|
@ -1825,6 +1828,7 @@ declare namespace entities {
|
||||||
Announcement,
|
Announcement,
|
||||||
App,
|
App,
|
||||||
Note,
|
Note,
|
||||||
|
NoteDraft,
|
||||||
NoteReaction,
|
NoteReaction,
|
||||||
NoteFavorite,
|
NoteFavorite,
|
||||||
Notification_2 as Notification,
|
Notification_2 as Notification,
|
||||||
|
@ -2573,6 +2577,9 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Note = components['schemas']['Note'];
|
type Note = components['schemas']['Note'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type NoteDraft = components['schemas']['NoteDraft'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type NoteFavorite = components['schemas']['NoteFavorite'];
|
type NoteFavorite = components['schemas']['NoteFavorite'];
|
||||||
|
|
||||||
|
@ -2681,6 +2688,15 @@ type NotesRequest = operations['notes']['requestBody']['content']['application/j
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type NotesResponse = operations['notes']['responses']['200']['content']['application/json'];
|
type NotesResponse = operations['notes']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type NotesScheduledCancelRequest = operations['notes___scheduled___cancel']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type NotesScheduledListRequest = operations['notes___scheduled___list']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type NotesScheduledListResponse = operations['notes___scheduled___list']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json'];
|
type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -2742,7 +2758,7 @@ type Notification_2 = components['schemas']['Notification'];
|
||||||
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
|
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
|
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "noteScheduled", "scheduledNotePosted", "scheduledNoteError"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Page = components['schemas']['Page'];
|
type Page = components['schemas']['Page'];
|
||||||
|
|
|
@ -3129,6 +3129,28 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:notes*
|
||||||
|
*/
|
||||||
|
request<E extends 'notes/scheduled/cancel', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:notes*
|
||||||
|
*/
|
||||||
|
request<E extends 'notes/scheduled/list', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
@ -418,6 +418,9 @@ import type {
|
||||||
NotesCreateRequest,
|
NotesCreateRequest,
|
||||||
NotesCreateResponse,
|
NotesCreateResponse,
|
||||||
NotesDeleteRequest,
|
NotesDeleteRequest,
|
||||||
|
NotesScheduledCancelRequest,
|
||||||
|
NotesScheduledListRequest,
|
||||||
|
NotesScheduledListResponse,
|
||||||
NotesFavoritesCreateRequest,
|
NotesFavoritesCreateRequest,
|
||||||
NotesFavoritesDeleteRequest,
|
NotesFavoritesDeleteRequest,
|
||||||
NotesFeaturedRequest,
|
NotesFeaturedRequest,
|
||||||
|
@ -872,6 +875,8 @@ export type Endpoints = {
|
||||||
'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse };
|
'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse };
|
||||||
'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse };
|
'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse };
|
||||||
'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse };
|
'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse };
|
||||||
|
'notes/scheduled/cancel': { req: NotesScheduledCancelRequest; res: EmptyResponse };
|
||||||
|
'notes/scheduled/list': { req: NotesScheduledListRequest; res: NotesScheduledListResponse };
|
||||||
'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse };
|
'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse };
|
||||||
'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse };
|
'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse };
|
||||||
'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse };
|
'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse };
|
||||||
|
|
|
@ -421,6 +421,9 @@ export type NotesConversationResponse = operations['notes___conversation']['resp
|
||||||
export type NotesCreateRequest = operations['notes___create']['requestBody']['content']['application/json'];
|
export type NotesCreateRequest = operations['notes___create']['requestBody']['content']['application/json'];
|
||||||
export type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json'];
|
export type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json'];
|
||||||
export type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json'];
|
export type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json'];
|
||||||
|
export type NotesScheduledCancelRequest = operations['notes___scheduled___cancel']['requestBody']['content']['application/json'];
|
||||||
|
export type NotesScheduledListRequest = operations['notes___scheduled___list']['requestBody']['content']['application/json'];
|
||||||
|
export type NotesScheduledListResponse = operations['notes___scheduled___list']['responses']['200']['content']['application/json'];
|
||||||
export type NotesFavoritesCreateRequest = operations['notes___favorites___create']['requestBody']['content']['application/json'];
|
export type NotesFavoritesCreateRequest = operations['notes___favorites___create']['requestBody']['content']['application/json'];
|
||||||
export type NotesFavoritesDeleteRequest = operations['notes___favorites___delete']['requestBody']['content']['application/json'];
|
export type NotesFavoritesDeleteRequest = operations['notes___favorites___delete']['requestBody']['content']['application/json'];
|
||||||
export type NotesFeaturedRequest = operations['notes___featured']['requestBody']['content']['application/json'];
|
export type NotesFeaturedRequest = operations['notes___featured']['requestBody']['content']['application/json'];
|
||||||
|
|
|
@ -14,6 +14,7 @@ export type Ad = components['schemas']['Ad'];
|
||||||
export type Announcement = components['schemas']['Announcement'];
|
export type Announcement = components['schemas']['Announcement'];
|
||||||
export type App = components['schemas']['App'];
|
export type App = components['schemas']['App'];
|
||||||
export type Note = components['schemas']['Note'];
|
export type Note = components['schemas']['Note'];
|
||||||
|
export type NoteDraft = components['schemas']['NoteDraft'];
|
||||||
export type NoteReaction = components['schemas']['NoteReaction'];
|
export type NoteReaction = components['schemas']['NoteReaction'];
|
||||||
export type NoteFavorite = components['schemas']['NoteFavorite'];
|
export type NoteFavorite = components['schemas']['NoteFavorite'];
|
||||||
export type Notification = components['schemas']['Notification'];
|
export type Notification = components['schemas']['Notification'];
|
||||||
|
|
|
@ -2709,6 +2709,24 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['notes___delete'];
|
post: operations['notes___delete'];
|
||||||
};
|
};
|
||||||
|
'/notes/scheduled/cancel': {
|
||||||
|
/**
|
||||||
|
* notes/scheduled/cancel
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:notes*
|
||||||
|
*/
|
||||||
|
post: operations['notes___scheduled___cancel'];
|
||||||
|
};
|
||||||
|
'/notes/scheduled/list': {
|
||||||
|
/**
|
||||||
|
* notes/scheduled/list
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:notes*
|
||||||
|
*/
|
||||||
|
post: operations['notes___scheduled___list'];
|
||||||
|
};
|
||||||
'/notes/favorites/create': {
|
'/notes/favorites/create': {
|
||||||
/**
|
/**
|
||||||
* notes/favorites/create
|
* notes/favorites/create
|
||||||
|
@ -4272,6 +4290,58 @@ export type components = {
|
||||||
clippedCount?: number;
|
clippedCount?: number;
|
||||||
myReaction?: string | null;
|
myReaction?: string | null;
|
||||||
};
|
};
|
||||||
|
NoteDraft: {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
updatedAt: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
scheduledAt: string | null;
|
||||||
|
reason?: string;
|
||||||
|
channel?: {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
renote?: ({
|
||||||
|
/** Format: misskey:id */
|
||||||
|
id: string;
|
||||||
|
text: string | null;
|
||||||
|
user: {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
host: string | null;
|
||||||
|
};
|
||||||
|
}) | null;
|
||||||
|
reply?: ({
|
||||||
|
/** Format: misskey:id */
|
||||||
|
id: string;
|
||||||
|
text: string | null;
|
||||||
|
user: {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
host: string | null;
|
||||||
|
};
|
||||||
|
}) | null;
|
||||||
|
data: {
|
||||||
|
text: string | null;
|
||||||
|
useCw: boolean;
|
||||||
|
cw: string | null;
|
||||||
|
/** @enum {string} */
|
||||||
|
visibility: 'public' | 'home' | 'followers' | 'specified';
|
||||||
|
localOnly: boolean;
|
||||||
|
files: components['schemas']['DriveFile'][];
|
||||||
|
poll: ({
|
||||||
|
choices: string[];
|
||||||
|
multiple: boolean;
|
||||||
|
expiresAt: number | null;
|
||||||
|
expiredAfter: number | null;
|
||||||
|
}) | null;
|
||||||
|
visibleUserIds?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
NoteReaction: {
|
NoteReaction: {
|
||||||
/**
|
/**
|
||||||
* Format: id
|
* Format: id
|
||||||
|
@ -4419,6 +4489,30 @@ export type components = {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'achievementEarned';
|
type: 'achievementEarned';
|
||||||
achievement: string;
|
achievement: string;
|
||||||
|
} | {
|
||||||
|
/** Format: id */
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'noteScheduled';
|
||||||
|
draft: components['schemas']['NoteDraft'];
|
||||||
|
} | {
|
||||||
|
/** Format: id */
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'scheduledNotePosted';
|
||||||
|
note: components['schemas']['Note'];
|
||||||
|
} | {
|
||||||
|
/** Format: id */
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'scheduledNoteError';
|
||||||
|
draft: components['schemas']['NoteDraft'];
|
||||||
} | {
|
} | {
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -4983,6 +5077,7 @@ export type components = {
|
||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
|
canScheduleNote: boolean;
|
||||||
canInitiateConversation: boolean;
|
canInitiateConversation: boolean;
|
||||||
canCreateContent: boolean;
|
canCreateContent: boolean;
|
||||||
canUpdateContent: boolean;
|
canUpdateContent: boolean;
|
||||||
|
@ -20366,8 +20461,8 @@ export type operations = {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
/** @default true */
|
/** @default true */
|
||||||
markAsRead?: boolean;
|
markAsRead?: boolean;
|
||||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -20434,8 +20529,8 @@ export type operations = {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
/** @default true */
|
/** @default true */
|
||||||
markAsRead?: boolean;
|
markAsRead?: boolean;
|
||||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -23333,6 +23428,7 @@ export type operations = {
|
||||||
expiresAt?: number | null;
|
expiresAt?: number | null;
|
||||||
expiredAfter?: number | null;
|
expiredAfter?: number | null;
|
||||||
}) | null;
|
}) | null;
|
||||||
|
scheduledAt?: number | null;
|
||||||
/** @default false */
|
/** @default false */
|
||||||
noCreatedNote?: boolean;
|
noCreatedNote?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -23447,6 +23543,120 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* notes/scheduled/cancel
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:notes*
|
||||||
|
*/
|
||||||
|
notes___scheduled___cancel: {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
draftId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (without any results) */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description To many requests */
|
||||||
|
429: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* notes/scheduled/list
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:notes*
|
||||||
|
*/
|
||||||
|
notes___scheduled___list: {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** @default 10 */
|
||||||
|
limit?: number;
|
||||||
|
/** @default 0 */
|
||||||
|
offset?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['NoteDraft'][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* notes/favorites/create
|
* notes/favorites/create
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
|
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError'] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ export async function createNotification<K extends keyof PushNotificationDataMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function composeNotification(data: PushNotificationDataMap[keyof PushNotificationDataMap]): Promise<[string, NotificationOptions] | null> {
|
async function composeNotification(data: PushNotificationDataMap[keyof PushNotificationDataMap]): Promise<[string, NotificationOptions & { actions?: Record<string, string>[], renotify?: boolean }] | null> {
|
||||||
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
||||||
const { t } = i18n;
|
const { t } = i18n;
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
@ -60,7 +60,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
|
const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
|
||||||
return [t('_notification.youWereFollowed'), {
|
return [t('_notification.youWereFollowed'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('user-plus'),
|
badge: iconUrl('user-plus'),
|
||||||
data,
|
data,
|
||||||
actions: userDetail.isFollowing ? [] : [
|
actions: userDetail.isFollowing ? [] : [
|
||||||
|
@ -75,7 +75,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'mention':
|
case 'mention':
|
||||||
return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text ?? '',
|
body: data.body.note.text ?? '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('at'),
|
badge: iconUrl('at'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -89,7 +89,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'reply':
|
case 'reply':
|
||||||
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text ?? '',
|
body: data.body.note.text ?? '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('arrow-back-up'),
|
badge: iconUrl('arrow-back-up'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -103,7 +103,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'renote':
|
case 'renote':
|
||||||
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text ?? '',
|
body: data.body.note.text ?? '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('repeat'),
|
badge: iconUrl('repeat'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -117,7 +117,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'quote':
|
case 'quote':
|
||||||
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text ?? '',
|
body: data.body.note.text ?? '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('quote'),
|
badge: iconUrl('quote'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -137,7 +137,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'note':
|
case 'note':
|
||||||
return [t('_notification.newNote') + ': ' + getUserName(data.body.user), {
|
return [t('_notification.newNote') + ': ' + getUserName(data.body.user), {
|
||||||
body: data.body.note.text ?? '',
|
body: data.body.note.text ?? '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
const tag = `reaction:${data.body.note.id}`;
|
const tag = `reaction:${data.body.note.id}`;
|
||||||
return [`${reaction} ${getUserName(data.body.user)}`, {
|
return [`${reaction} ${getUserName(data.body.user)}`, {
|
||||||
body: data.body.note.text ?? '',
|
body: data.body.note.text ?? '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
tag,
|
tag,
|
||||||
badge,
|
badge,
|
||||||
data,
|
data,
|
||||||
|
@ -180,7 +180,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'receiveFollowRequest':
|
case 'receiveFollowRequest':
|
||||||
return [t('_notification.youReceivedFollowRequest'), {
|
return [t('_notification.youReceivedFollowRequest'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('user-plus'),
|
badge: iconUrl('user-plus'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -198,11 +198,18 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'followRequestAccepted':
|
case 'followRequestAccepted':
|
||||||
return [t('_notification.yourFollowRequestAccepted'), {
|
return [t('_notification.yourFollowRequestAccepted'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('circle-check'),
|
badge: iconUrl('circle-check'),
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
case 'pollEnded':
|
||||||
|
return [t('_notification.pollEnded'), {
|
||||||
|
body: data.body.note.text ?? '',
|
||||||
|
badge: iconUrl('chart-arrows'),
|
||||||
|
data,
|
||||||
|
}];
|
||||||
|
|
||||||
case 'achievementEarned':
|
case 'achievementEarned':
|
||||||
return [t('_notification.achievementEarned'), {
|
return [t('_notification.achievementEarned'), {
|
||||||
body: t(`_achievements._types._${data.body.achievement}.title`),
|
body: t(`_achievements._types._${data.body.achievement}.title`),
|
||||||
|
@ -211,10 +218,32 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
tag: `achievement:${data.body.achievement}`,
|
tag: `achievement:${data.body.achievement}`,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'pollEnded':
|
case 'noteScheduled':
|
||||||
return [t('_notification.pollEnded'), {
|
return [t('_notification.noteScheduled'), {
|
||||||
|
body: data.body.draft.data.text ?? '',
|
||||||
|
badge: iconUrl('calendar-clock'),
|
||||||
|
data,
|
||||||
|
}];
|
||||||
|
|
||||||
|
case 'scheduledNotePosted':
|
||||||
|
return [t('_notification.scheduledNotePosted'), {
|
||||||
body: data.body.note.text ?? '',
|
body: data.body.note.text ?? '',
|
||||||
badge: iconUrl('chart-arrows'),
|
badge: iconUrl('calendar-check'),
|
||||||
|
data,
|
||||||
|
}];
|
||||||
|
|
||||||
|
case 'scheduledNoteError':
|
||||||
|
return [t('_notification.scheduledNoteError'), {
|
||||||
|
body: data.body.draft.reason ?? '',
|
||||||
|
badge: iconUrl('calendar-exclamation'),
|
||||||
|
data,
|
||||||
|
}];
|
||||||
|
|
||||||
|
case 'roleAssigned':
|
||||||
|
return [t('_notification.roleAssigned'), {
|
||||||
|
body: data.body.role.name,
|
||||||
|
icon: data.body.role.iconUrl ?? undefined,
|
||||||
|
badge: iconUrl('badges'),
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
@ -238,7 +267,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
case 'unreadAntennaNote':
|
case 'unreadAntennaNote':
|
||||||
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
|
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
|
||||||
body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`,
|
body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`,
|
||||||
icon: data.body.note.user.avatarUrl,
|
icon: data.body.note.user.avatarUrl ?? undefined,
|
||||||
badge: iconUrl('antenna'),
|
badge: iconUrl('antenna'),
|
||||||
tag: `antenna:${data.body.antenna.id}`,
|
tag: `antenna:${data.body.antenna.id}`,
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -50,4 +50,9 @@ export type BadgeNames =
|
||||||
| 'quote'
|
| 'quote'
|
||||||
| 'repeat'
|
| 'repeat'
|
||||||
| 'user-plus'
|
| 'user-plus'
|
||||||
| 'users';
|
| 'users'
|
||||||
|
| 'badges'
|
||||||
|
| 'calendar-clock'
|
||||||
|
| 'calendar-check'
|
||||||
|
| 'calendar-exclamation'
|
||||||
|
;
|
||||||
|
|
Loading…
Reference in a new issue