summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarie <github@yuugi.dev>2024-11-03 17:59:50 +0100
committerMarie <github@yuugi.dev>2024-12-09 05:32:51 +0100
commitfc9d777dc3161b40c5c62bb65cb03e2c7d8f4380 (patch)
treeaf42af2f205816cb3b833747d2df35935c5f9c92
parentchore: remove leftover log (diff)
downloadsharkey-fc9d777dc3161b40c5c62bb65cb03e2c7d8f4380.tar.gz
sharkey-fc9d777dc3161b40c5c62bb65cb03e2c7d8f4380.tar.bz2
sharkey-fc9d777dc3161b40c5c62bb65cb03e2c7d8f4380.zip
upd: add notification for failures, add reasons for failure, apply suggestions
-rw-r--r--locales/index.d.ts4
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts5
-rw-r--r--packages/backend/src/models/NoteSchedule.ts2
-rw-r--r--packages/backend/src/models/Notification.ts5
-rw-r--r--packages/backend/src/models/json-schema/notification.ts14
-rw-r--r--packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts59
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/create.ts5
-rw-r--r--packages/backend/src/types.ts1
-rw-r--r--packages/frontend-shared/js/const.ts1
-rw-r--r--packages/frontend/src/components/MkNotification.vue8
-rw-r--r--packages/frontend/src/components/MkPostForm.vue5
-rw-r--r--packages/frontend/src/components/MkSchedulePostListDialog.vue1
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md2
-rw-r--r--packages/misskey-js/src/autogen/types.ts16
-rw-r--r--packages/misskey-js/src/consts.ts2
-rw-r--r--packages/sw/src/scripts/create-notification.ts7
-rw-r--r--sharkey-locales/en-US.yml1
17 files changed, 110 insertions, 28 deletions
diff --git a/locales/index.d.ts b/locales/index.d.ts
index e181b13f33..4cfc220731 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9558,6 +9558,10 @@ export interface Locale extends ILocale {
* Note got edited
*/
"edited": string;
+ /**
+ * Posting scheduled note failed
+ */
+ "scheduledNoteFailed": string;
};
"_deck": {
/**
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index bbaf0cb7c8..27b8231854 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -20,7 +20,7 @@ import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
-const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited'] as (typeof groupedNotificationTypes[number])[]);
+const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited', 'scheduledNoteFailed'] as (typeof groupedNotificationTypes[number])[]);
@Injectable()
export class NotificationEntityService implements OnModuleInit {
@@ -169,6 +169,9 @@ export class NotificationEntityService implements OnModuleInit {
exportedEntity: notification.exportedEntity,
fileId: notification.fileId,
} : {}),
+ ...(notification.type === 'scheduledNoteFailed' ? {
+ reason: notification.reason,
+ } : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,
diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts
index 97ffe32ffa..dde0af6ad7 100644
--- a/packages/backend/src/models/NoteSchedule.ts
+++ b/packages/backend/src/models/NoteSchedule.ts
@@ -18,8 +18,6 @@ type MinimumUser = {
};
export type MiScheduleNoteType={
- /** Date.toISOString() */
- createdAt: string;
visibility: 'public' | 'home' | 'followers' | 'specified';
visibleUsers: MinimumUser[];
channel?: MiChannel['id'];
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index c4f046c565..5003e02d96 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -122,6 +122,11 @@ export type MiNotification = {
createdAt: string;
notifierId: MiUser['id'];
noteId: MiNote['id'];
+} | {
+ type: 'scheduledNoteFailed';
+ id: string;
+ createdAt: string;
+ reason: string;
};
export type MiGroupedNotification = MiNotification | {
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 990e8957cf..69bd9531ec 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -376,6 +376,20 @@ export const packedNotificationSchema = {
type: {
type: 'string',
optional: false, nullable: false,
+ enum: ['scheduledNoteFailed'],
+ },
+ reason: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
enum: ['reaction:grouped'],
},
note: {
diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts
index 62d527953d..59e23b865e 100644
--- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts
+++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts
@@ -9,6 +9,7 @@ import { bindThis } from '@/decorators.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { ScheduleNotePostJobData } from '../types.js';
@@ -32,6 +33,7 @@ export class ScheduleNotePostProcessorService {
private noteCreateService: NoteCreateService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post');
}
@@ -50,8 +52,9 @@ export class ScheduleNotePostProcessorService {
const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined;
const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined;
let files: MiDriveFile[] = [];
- const fileIds = note.files ?? null;
- if (fileIds != null && fileIds.length > 0 && me) {
+ const fileIds = note.files;
+
+ if (fileIds.length > 0 && me) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id,
@@ -61,22 +64,52 @@ export class ScheduleNotePostProcessorService {
.setParameters({ fileIds })
.getMany();
}
- if (
- !data.userId ||
- !me ||
- (note.reply && !reply) ||
- (note.renote && !renote) ||
- (note.channel && !channel) ||
- (note.files.length !== files.length)
- ) {
- //キューに積んだときは有った物が消滅してたら予約投稿をキャンセルする
- this.logger.warn('cancel schedule note');
+
+ if (!data.userId || !me) {
+ this.logger.warn('Schedule Note Failed Reason: User Not Found');
+ await this.noteScheduleRepository.remove(data);
+ return;
+ }
+
+ if (note.files.length !== files.length) {
+ this.logger.warn('Schedule Note Failed Reason: files are missing in the user\'s drive');
+ this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
+ reason: 'Some attached files on your scheduled note no longer exist',
+ });
+ await this.noteScheduleRepository.remove(data);
+ return;
+ }
+
+ if (note.reply && !reply) {
+ this.logger.warn('Schedule Note Failed Reason: parent note to reply does not exist');
+ this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
+ reason: 'Replied to note on your scheduled note no longer exists',
+ });
await this.noteScheduleRepository.remove(data);
return;
}
+
+ if (note.renote && !renote) {
+ this.logger.warn('Schedule Note Failed Reason: attached quote note no longer exists');
+ this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
+ reason: 'A quoted note from one of your scheduled notes no longer exists',
+ });
+ await this.noteScheduleRepository.remove(data);
+ return;
+ }
+
+ if (note.channel && !channel) {
+ this.logger.warn('Schedule Note Failed Reason: Channel does not exist');
+ this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
+ reason: 'An attached channel on your scheduled note no longer exists',
+ });
+ await this.noteScheduleRepository.remove(data);
+ return;
+ }
+
await this.noteCreateService.create(me, {
...note,
- createdAt: new Date(note.createdAt), //typeORMのjsonbで何故かstringにされるから戻す
+ createdAt: new Date(),
files,
poll: note.poll ? {
choices: note.poll.choices,
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
index c22c29ae31..b8ae3f44a3 100644
--- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
@@ -292,7 +292,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (reply.userId !== me.id) {
- const blockExist = await this.blockingsRepository.exist({
+ const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: me.id,
@@ -324,8 +324,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} else {
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule);
}
- const note:MiScheduleNoteType = {
- createdAt: new Date(ps.scheduleNote.scheduledAt!).toISOString(),
+ const note: MiScheduleNoteType = {
files: files.map(f => f.id),
poll: ps.poll ? {
choices: ps.poll.choices,
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 2aa4f279ea..7930129002 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -35,6 +35,7 @@ export const notificationTypes = [
'roleAssigned',
'achievementEarned',
'exportCompleted',
+ 'scheduledNoteFailed',
'app',
'test',
] as const;
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
index 882f19c7fd..5bc75db908 100644
--- a/packages/frontend-shared/js/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -131,6 +131,7 @@ export const notificationTypes = [
'test',
'app',
'edited',
+ 'scheduledNoteFailed',
] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 7bec9bdc65..3c4f56b537 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :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', 'scheduledNoteFailed'].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_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" 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>
@@ -29,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
[$style.t_pollEnded]: notification.type === 'edited',
+ [$style.t_roleAssigned]: notification.type === 'scheduledNoteFailed',
}]"
> <!-- we re-use t_pollEnded for "edited" instead of making an identical style -->
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
@@ -46,6 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else class="ti ti-badges"></i>
</template>
<i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i>
+ <i v-else-if="notification.type === 'scheduledNoteFailed'" class="ti ti-calendar-event"></i>
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
@@ -70,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'renote:grouped'">{{ 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 === 'edited'">{{ i18n.ts._notification.edited }}</span>
+ <span v-else-if="notification.type === 'scheduledNoteFailed'">{{ i18n.ts._notification.scheduledNoteFailed }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header>
<div>
@@ -109,6 +112,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
{{ i18n.ts.showFile }}
</MkA>
+ <div v-else-if="notification.type === 'scheduledNoteFailed'" :class="$style.text">
+ {{ notification.reason }}
+ </div>
<template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
</template>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 443e9e7ee9..bbde7c65f9 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -1046,8 +1046,9 @@ function openAccountMenu(ev: MouseEvent) {
}
function toggleScheduleNote() {
- if (scheduleNote.value) scheduleNote.value = null;
- else {
+ if (scheduleNote.value) {
+ scheduleNote.value = null;
+ } else {
scheduleNote.value = {
scheduledAt: null,
};
diff --git a/packages/frontend/src/components/MkSchedulePostListDialog.vue b/packages/frontend/src/components/MkSchedulePostListDialog.vue
index cfae94951b..d0716ead79 100644
--- a/packages/frontend/src/components/MkSchedulePostListDialog.vue
+++ b/packages/frontend/src/components/MkSchedulePostListDialog.vue
@@ -48,6 +48,7 @@ const cancel = () => {
emit('cancel');
dialogEl.value.close();
};
+
const paginationEl = ref();
const pagination: Paging = {
endpoint: 'notes/schedule/list',
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index ca7a374a67..880be518fa 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -2890,7 +2890,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented)
-export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited"];
+export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited", "scheduledNoteFailed"];
// @public (undocumented)
export function nyaize(text: string): string;
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index c8d7194405..6eb1819037 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4523,6 +4523,14 @@ export type components = {
/** Format: date-time */
createdAt: string;
/** @enum {string} */
+ type: 'scheduledNoteFailed';
+ reason: string;
+ } | {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** @enum {string} */
type: 'reaction:grouped';
note: components['schemas']['Note'];
reactions: {
@@ -19984,8 +19992,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
};
};
};
@@ -20052,8 +20060,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
};
};
};
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index d090a6b46f..34fc7c1a03 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -16,7 +16,7 @@ import type {
UserLite,
} from './autogen/models.js';
-export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited'] as const;
+export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited', 'scheduledNoteFailed'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
index 9c56e338c7..8442552e3b 100644
--- a/packages/sw/src/scripts/create-notification.ts
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -258,6 +258,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
data,
}];
+ case 'scheduledNoteFailed':
+ return [i18n.ts._notification.scheduledNoteFailed, {
+ body: data.body.reason,
+ badge: iconUrl('bell'),
+ data,
+ }];
+
default:
return null;
}
diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml
index 79c362cfd8..582f4eda0a 100644
--- a/sharkey-locales/en-US.yml
+++ b/sharkey-locales/en-US.yml
@@ -277,6 +277,7 @@ _notification:
youRenoted: "Boost from {name}"
renotedBySomeUsers: "Boosted by {n} users"
edited: "Note got edited"
+ scheduledNoteFailed: "Posting scheduled note failed"
_types:
renote: "Boosts"
edited: "Edits"