diff --git a/locales/index.d.ts b/locales/index.d.ts index eee9de8f0d..f7d875d2c8 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9883,6 +9883,10 @@ export interface Locale extends ILocale { * ロールが付与されました */ "roleAssigned": string; + /** + * チャットルームへ招待されました + */ + "chatRoomInvitationReceived": string; /** * プッシュ通知の更新をしました */ @@ -9992,6 +9996,10 @@ export interface Locale extends ILocale { * ロールが付与された */ "roleAssigned": string; + /** + * チャットルームへ招待された + */ + "chatRoomInvitationReceived": string; /** * 実績の獲得 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e4c17276b1..efaa279454 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2612,6 +2612,7 @@ _notification: newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" roleAssigned: "ロールが付与されました" + chatRoomInvitationReceived: "チャットルームへ招待されました" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" testNotification: "通知テスト" @@ -2641,6 +2642,7 @@ _notification: receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" roleAssigned: "ロールが付与された" + chatRoomInvitationReceived: "チャットルームへ招待された" achievementEarned: "実績の獲得" exportCompleted: "エクスポートが完了した" login: "ログイン" diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 062ca96f17..4c010b2ef7 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -26,6 +26,7 @@ import { Packed } from '@/misc/json-schema.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { emojiRegex } from '@/misc/emoji-regex.js'; +import { NotificationService } from '@/core/NotificationService.js'; const MAX_ROOM_MEMBERS = 30; const MAX_REACTIONS_PER_MESSAGE = 100; @@ -68,6 +69,7 @@ export class ChatService { private apRendererService: ApRendererService, private queueService: QueueService, private pushNotificationService: PushNotificationService, + private notificationService: NotificationService, private userBlockingService: UserBlockingService, private queryService: QueryService, private roleService: RoleService, @@ -544,6 +546,10 @@ export class ChatService { const created = await this.chatRoomInvitationsRepository.insertOne(invitation); + this.notificationService.createNotification(inviteeId, 'chatRoomInvitationReceived', { + invitationId: invitation.id, + }, inviterId); + return created; } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index dff6968f9c..d8b947839f 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js'; import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js'; import { CacheService } from '@/core/CacheService.js'; import { RoleEntityService } from './RoleEntityService.js'; +import { ChatEntityService } from './ChatEntityService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; @@ -27,6 +28,7 @@ export class NotificationEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; private roleEntityService: RoleEntityService; + private chatEntityService: ChatEntityService; constructor( private moduleRef: ModuleRef, @@ -41,9 +43,6 @@ export class NotificationEntityService implements OnModuleInit { private followRequestsRepository: FollowRequestsRepository, private cacheService: CacheService, - - //private userEntityService: UserEntityService, - //private noteEntityService: NoteEntityService, ) { } @@ -51,6 +50,7 @@ export class NotificationEntityService implements OnModuleInit { this.userEntityService = this.moduleRef.get('UserEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); this.roleEntityService = this.moduleRef.get('RoleEntityService'); + this.chatEntityService = this.moduleRef.get('ChatEntityService'); } /** @@ -59,7 +59,6 @@ export class NotificationEntityService implements OnModuleInit { async #packInternal <T extends MiNotification | MiGroupedNotification> ( src: T, meId: MiUser['id'], - options: { checkValidNotifier?: boolean; }, @@ -92,7 +91,7 @@ export class NotificationEntityService implements OnModuleInit { // if the user has been deleted, don't show this notification if (needsUser && !userIfNeed) return null; - // #region Grouped notifications + //#region Grouped notifications if (notification.type === 'reaction:grouped') { const reactions = (await Promise.all(notification.reactions.map(async reaction => { const user = hint?.packedUsers != null @@ -137,7 +136,7 @@ export class NotificationEntityService implements OnModuleInit { users, }); } - // #endregion + //#endregion const needsRole = notification.type === 'roleAssigned'; const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined; @@ -146,6 +145,13 @@ export class NotificationEntityService implements OnModuleInit { return null; } + const needsChatRoomInvitation = notification.type === 'chatRoomInvitationReceived'; + const chatRoomInvitation = needsChatRoomInvitation ? await this.chatEntityService.packRoomInvitation(notification.invitationId, { id: meId }) : undefined; + // if the invitation has been deleted, don't show this notification + if (needsChatRoomInvitation && !chatRoomInvitation) { + return null; + } + return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), @@ -159,6 +165,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'roleAssigned' ? { role: role, } : {}), + ...(notification.type === 'chatRoomInvitationReceived' ? { + invitation: chatRoomInvitation, + } : {}), ...(notification.type === 'followRequestAccepted' ? { message: notification.message, } : {}), @@ -236,7 +245,7 @@ export class NotificationEntityService implements OnModuleInit { public async pack( src: MiNotification | MiGroupedNotification, meId: MiUser['id'], - + options: { checkValidNotifier?: boolean; }, diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 5772ace338..5764a307b0 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -75,6 +75,12 @@ export type MiNotification = { id: string; createdAt: string; roleId: MiRole['id']; +} | { + type: 'chatRoomInvitationReceived'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + invitationId: string; } | { type: 'achievementEarned'; id: string; diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 1638b2b3c7..7f23d2d6a1 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -287,6 +287,21 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['chatRoomInvitationReceived'], + }, + invitation: { + type: 'object', + ref: 'ChatRoomInvitation', + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index b10430782b..0094f25e34 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -608,6 +608,7 @@ export const packedMeDetailedOnlySchema = { receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, + chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig }, achievementEarned: { optional: true, ...notificationRecieveConfig }, app: { optional: true, ...notificationRecieveConfig }, test: { optional: true, ...notificationRecieveConfig }, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 655bd32bce..1ba6853dbe 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -106,6 +106,7 @@ export const meta = { receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, + chatRoomInvitationReceived: { optional: true, ...notificationRecieveConfig }, achievementEarned: { optional: true, ...notificationRecieveConfig }, app: { optional: true, ...notificationRecieveConfig }, test: { optional: true, ...notificationRecieveConfig }, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index baf397bf06..082d97f5d4 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -212,6 +212,7 @@ export const paramDef = { receiveFollowRequest: notificationRecieveConfig, followRequestAccepted: notificationRecieveConfig, roleAssigned: notificationRecieveConfig, + chatRoomInvitationReceived: notificationRecieveConfig, achievementEarned: notificationRecieveConfig, app: notificationRecieveConfig, test: notificationRecieveConfig, diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index c6b1035554..4e215c93c6 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -15,6 +15,7 @@ * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された * roleAssigned - ロールが付与された + * chatRoomInvitationReceived - チャットルームに招待された * achievementEarned - 実績を獲得 * exportCompleted - エクスポートが完了 * login - ログイン @@ -34,6 +35,7 @@ export const notificationTypes = [ 'receiveFollowRequest', 'followRequestAccepted', 'roleAssigned', + 'chatRoomInvitationReceived', 'achievementEarned', 'exportCompleted', 'login', diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 0241446784..fa60476b69 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -66,6 +66,7 @@ export const notificationTypes = [ 'receiveFollowRequest', 'followRequestAccepted', 'roleAssigned', + 'chatRoomInvitationReceived', 'achievementEarned', 'exportCompleted', 'login', diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 2824701579..c2e8b8e2fe 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -43,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> <i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i> <i v-else-if="notification.type === 'createToken'" class="ti ti-key"></i> + <i v-else-if="notification.type === 'chatRoomInvitationReceived'" class="ti ti-messages"></i> <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=""/> <i v-else class="ti ti-badges"></i> @@ -61,6 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="notification.type === 'pollEnded'">{{ 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 === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> + <span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span> <span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span> @@ -104,6 +106,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text"> {{ notification.role.name }} </div> + <div v-else-if="notification.type === 'chatRoomInvitationReceived'" :class="$style.text"> + {{ notification.invitation.room.name }} + </div> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> {{ i18n.ts._achievements._types['_' + notification.achievement].title }} </MkA> diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 8610a21304..7e73b64bb0 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3040,7 +3040,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"]; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned"]; // @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 3acef22101..3b52a70ccf 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4201,6 +4201,15 @@ export type components = { /** Format: misskey:id */ userListId: string; }]>; + chatRoomInvitationReceived?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; achievementEarned?: OneOf<[{ /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; @@ -4538,6 +4547,14 @@ export type components = { /** @enum {string} */ type: 'roleAssigned'; role: components['schemas']['Role']; + } | { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + type: 'chatRoomInvitationReceived'; + invitation: components['schemas']['ChatRoomInvitation']; } | ({ /** Format: id */ id: string; @@ -10084,6 +10101,15 @@ export type operations = { /** Format: misskey:id */ userListId: string; }]>; + chatRoomInvitationReceived?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; achievementEarned?: OneOf<[{ /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; @@ -21573,8 +21599,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -21641,8 +21667,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -22738,6 +22764,15 @@ export type operations = { /** Format: misskey:id */ userListId: string; }]>; + chatRoomInvitationReceived?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; achievementEarned?: OneOf<[{ /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 9d0fe5e781..9a39e619b7 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'] as const; +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'chatRoomInvitationReceived', 'achievementEarned'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;