From a00a3c684136688fa265a537c1543c4c1d27908f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Mon, 14 Jul 2025 18:52:13 +0200 Subject: [PATCH] Add importCompleted notification. Send importCompleted when antenna/customEmoji/muting/userList is imported The only userImportableEntities that don't notify are blocking and following because they fork off a batch of single Closes #891 --- locales/index.d.ts | 8 +++++++ .../entities/NotificationEntityService.ts | 4 ++++ packages/backend/src/models/Notification.ts | 8 ++++++- .../src/models/json-schema/notification.ts | 22 ++++++++++++++++++- .../ImportAntennasProcessorService.ts | 6 +++++ .../ImportCustomEmojisProcessorService.ts | 9 +++++++- .../ImportMutingProcessorService.ts | 7 ++++++ .../ImportUserListsProcessorService.ts | 7 ++++++ packages/backend/src/types.ts | 1 + packages/frontend-shared/js/const.ts | 1 + .../src/components/MkNotification.vue | 19 +++++++++++++--- .../src/pages/settings/notifications.vue | 2 +- .../sw/src/scripts/create-notification.ts | 16 ++++++++++++++ sharkey-locales/en-US.yml | 1 + 14 files changed, 104 insertions(+), 7 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index d126174ebb..387f92f456 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10378,6 +10378,10 @@ export interface Locale extends ILocale { * Scheduled Note was posted */ "scheduledNotePosted": string; + /** + * Import of {x} has been completed + */ + "importOfXCompleted": ParameterizedString<"x">; }; "_deck": { /** @@ -13346,6 +13350,10 @@ export interface Locale extends ILocale { * Don't delete files used as avatars&c */ "keepFilesInUse": string; + /** + * this option requires more complicated database queries, you may need to increase the value of db.extra.statement_timeout in the configuration file + */ + "keepFilesInUseDescription": string; }; } declare const locales: { diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index cc8edfc666..bb956b1097 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -187,6 +187,10 @@ export class NotificationEntityService implements OnModuleInit { exportedEntity: notification.exportedEntity, fileId: notification.fileId, } : {}), + ...(notification.type === 'importCompleted' ? { + importedEntity: notification.importedEntity, + fileId: notification.fileId, + } : {}), ...(notification.type === 'scheduledNoteFailed' ? { reason: notification.reason, } : {}), diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index cff95cbf6e..1f87c1068d 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { userExportableEntities } from '@/types.js'; +import { userExportableEntities, userImportableEntities } from '@/types.js'; import { MiUser } from './User.js'; import { MiNote } from './Note.js'; import { MiAccessToken } from './AccessToken.js'; @@ -92,6 +92,12 @@ export type MiNotification = { createdAt: string; exportedEntity: typeof userExportableEntities[number]; fileId: MiDriveFile['id']; +} | { + type: 'importCompleted'; + id: string; + createdAt: string; + importedEntity: typeof userImportableEntities[number]; + fileId?: MiDriveFile['id']; } | { type: 'login'; id: string; diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 79c41e7e46..2663b2f5f8 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { notificationTypes, userExportableEntities } from '@/types.js'; +import { notificationTypes, userExportableEntities, userImportableEntities } from '@/types.js'; const baseSchema = { type: 'object', @@ -334,6 +334,26 @@ export const packedNotificationSchema = { format: 'id', }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['importCompleted'], + }, + importedEntity: { + type: 'string', + optional: false, nullable: false, + enum: userImportableEntities, + }, + fileId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index f29a19ce66..35e31b9533 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -12,6 +12,7 @@ import type { AntennasRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { DBAntennaImportJobData } from '../types.js'; import type * as Bull from 'bullmq'; @@ -65,6 +66,7 @@ export class ImportAntennasProcessorService { private queueLoggerService: QueueLoggerService, private idService: IdService, private globalEventService: GlobalEventService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('import-antennas'); } @@ -106,6 +108,10 @@ export class ImportAntennasProcessorService { this.logger.debug('Antenna created: ' + result.id); this.globalEventService.publishInternalEvent('antennaCreated', result); } + + this.notificationService.createNotification(job.data.user.id, 'importCompleted', { + importedEntity: 'antenna', + }); } catch (err: any) { this.logger.error('Error importing antennas:', err); } diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 4b909328cd..3be5b8401b 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js'; import type { Config } from '@/config.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @@ -40,6 +41,7 @@ export class ImportCustomEmojisProcessorService { private driveService: DriveService, private downloadService: DownloadService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('import-custom-emojis'); } @@ -127,7 +129,12 @@ export class ImportCustomEmojisProcessorService { cleanup(); - this.logger.debug('Imported'); + this.notificationService.createNotification(job.data.user.id, 'importCompleted', { + importedEntity: 'customEmoji', + fileId: file.id, + }); + + this.logger.debug('Imported', file.name); } catch (e) { this.logger.error('Error importing custom emojis:', e as Error); cleanup(); diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index d3827b12fd..492f3785e8 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -16,6 +16,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @@ -35,6 +36,7 @@ export class ImportMutingProcessorService { private remoteUserResolveService: RemoteUserResolveService, private downloadService: DownloadService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('import-muting'); } @@ -99,6 +101,11 @@ export class ImportMutingProcessorService { } } + this.notificationService.createNotification(job.data.user.id, 'importCompleted', { + importedEntity: 'muting', + fileId: file.id, + }); + this.logger.debug('Imported'); } } diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index 482054e52f..059a2a9773 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -17,6 +17,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @@ -43,6 +44,7 @@ export class ImportUserListsProcessorService { private remoteUserResolveService: RemoteUserResolveService, private downloadService: DownloadService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('import-user-lists'); } @@ -109,6 +111,11 @@ export class ImportUserListsProcessorService { } } + this.notificationService.createNotification(job.data.user.id, 'importCompleted', { + importedEntity: 'userList', + fileId: file.id, + }); + this.logger.debug('Imported'); } } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 53eb4e6f3e..8aa047a3ab 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -39,6 +39,7 @@ export const notificationTypes = [ 'chatRoomInvitationReceived', 'achievementEarned', 'exportCompleted', + 'importCompleted', 'login', 'createToken', 'scheduledNoteFailed', diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index bded34ea41..8e9bf86705 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -129,6 +129,7 @@ export const notificationTypes = [ 'chatRoomInvitationReceived', 'achievementEarned', 'exportCompleted', + 'importCompleted', 'login', 'createToken', 'test', diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index d42905fd6b..abb109f7bd 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
- +
@@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_exportCompleted]: notification.type === 'exportCompleted', + [$style.t_importCompleted]: notification.type === 'importCompleted', [$style.t_login]: notification.type === 'login', [$style.t_createToken]: notification.type === 'createToken', [$style.t_chatRoomInvitationReceived]: notification.type === 'chatRoomInvitationReceived', @@ -44,6 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -75,6 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._notification.createToken }} {{ i18n.ts._notification.testNotification }} {{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }} + {{ i18n.tsx._notification.importOfXCompleted({ x: importEntityName[notification.importedEntity] }) }} {{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} @@ -122,7 +125,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._achievements._types['_' + notification.achievement].title }} - + {{ i18n.ts.showFile }} @@ -219,6 +222,7 @@ const props = withDefaults(defineProps<{ }); type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' }; +type ImportCompletedNotification = Misskey.entities.Notification & { type: 'importCompleted' }; const exportEntityName = { antenna: i18n.ts.antennas, @@ -232,6 +236,15 @@ const exportEntityName = { userList: i18n.ts.lists, } as const satisfies Record; +const importEntityName = { + antenna: i18n.ts.antennas, + blocking: i18n.ts.blockedUsers, + customEmoji: i18n.ts.customEmojis, + following: i18n.ts.following, + muting: i18n.ts.mutedUsers, + userList: i18n.ts.lists, +} as const satisfies Record; + const followRequestDone = ref(true); const userDetailed: Ref = ref(null); @@ -398,7 +411,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } -.t_exportCompleted { +.t_exportCompleted, .t_importCompleted { background: var(--eventOther); pointer-events: none; } diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 168bff681d..e1cf4dac4d 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -85,7 +85,7 @@ import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; const $i = ensureSignin(); -const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[]; +const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted', 'importCompleted'] satisfies (typeof notificationTypes[number])[] as string[]; const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'createToken', 'scheduledNoteFailed', 'scheduledNotePosted'] satisfies (typeof notificationTypes[number])[] as string[]; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 68b5db54b7..949136d7ee 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -235,6 +235,22 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif }]; } + case 'importCompleted': { + const entityName = { + antenna: i18n.ts.antennas, + blocking: i18n.ts.blockedUsers, + customEmoji: i18n.ts.customEmojis, + following: i18n.ts.following, + muting: i18n.ts.mutedUsers, + userList: i18n.ts.lists, + } as const satisfies Record; + + return [i18n.tsx._notification.importOfXCompleted({ x: entityName[data.body.importedEntity] }), { + badge: iconUrl('circle-check'), + data, + }]; + } + case 'pollEnded': return [i18n.ts._notification.pollEnded, { body: data.body.note.text ?? '', diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 23a3afce82..3d9624fb12 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -314,6 +314,7 @@ _notification: edited: "Note got edited" scheduledNoteFailed: "Posting scheduled note failed" scheduledNotePosted: "Scheduled Note was posted" + importOfXCompleted: "Import of {x} has been completed" _types: renote: "Boosts" edited: "Edits"