mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-26 19:14:12 +00:00 
			
		
		
		
	Note Edited notification type
This commit is contained in:
		
							parent
							
								
									15665d1533
								
							
						
					
					
						commit
						4a13508da0
					
				
					 17 changed files with 94 additions and 63 deletions
				
			
		|  | @ -2334,6 +2334,7 @@ _notification: | |||
|   reactedBySomeUsers: "{n} users reacted" | ||||
|   renotedBySomeUsers: "Boosted by {n} users" | ||||
|   followedBySomeUsers: "Followed by {n} users" | ||||
|   edited: "Note got edited" | ||||
|   _types: | ||||
|     all: "All" | ||||
|     note: "New notes" | ||||
|  |  | |||
							
								
								
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -9048,6 +9048,10 @@ export interface Locale extends ILocale { | |||
|          * アンケートの結果が出ました | ||||
|          */ | ||||
|         "pollEnded": string; | ||||
|         /** | ||||
|          * 注記が編集されました | ||||
|          */ | ||||
|         "edited": string; | ||||
|         /** | ||||
|          * 新しい投稿 | ||||
|          */ | ||||
|  |  | |||
|  | @ -2389,6 +2389,7 @@ _notification: | |||
|   youReceivedFollowRequest: "フォローリクエストが来ました" | ||||
|   yourFollowRequestAccepted: "フォローリクエストが承認されました" | ||||
|   pollEnded: "アンケートの結果が出ました" | ||||
|   edited: "投稿が編集されました" | ||||
|   newNote: "新しい投稿" | ||||
|   unreadAntennaNote: "アンテナ {name}" | ||||
|   roleAssigned: "ロールが付与されました" | ||||
|  |  | |||
|  | @ -96,6 +96,7 @@ export interface MainEventTypes { | |||
| 	announcementCreated: { | ||||
| 		announcement: Packed<'Announcement'>; | ||||
| 	}; | ||||
| 	edited: Packed<'Note'>; | ||||
| } | ||||
| 
 | ||||
| export interface DriveEventTypes { | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ import { isReply } from '@/misc/is-reply.js'; | |||
| import { trackPromise } from '@/misc/promise-tracker.js'; | ||||
| import { isUserRelated } from '@/misc/is-user-related.js'; | ||||
| 
 | ||||
| type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; | ||||
| type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited'; | ||||
| 
 | ||||
| class NotificationManager { | ||||
| 	private notifier: { id: MiUser['id']; }; | ||||
|  | @ -586,7 +586,7 @@ export class NoteEditService implements OnApplicationShutdown { | |||
| 			} | ||||
| 
 | ||||
| 			// Pack the note
 | ||||
| 			const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true }); | ||||
| 			const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); | ||||
| 			if (data.poll != null) { | ||||
| 				this.globalEventService.publishNoteStream(note.id, 'updated', { | ||||
| 					cw: note.cw, | ||||
|  | @ -612,7 +612,7 @@ export class NoteEditService implements OnApplicationShutdown { | |||
| 
 | ||||
| 			const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note); | ||||
| 
 | ||||
| 			await this.createMentionedEvents(mentionedUsers, note, nm); | ||||
| 			//await this.createMentionedEvents(mentionedUsers, note, nm);
 | ||||
| 
 | ||||
| 			// If has in reply to note
 | ||||
| 			if (data.reply) { | ||||
|  | @ -634,12 +634,12 @@ export class NoteEditService implements OnApplicationShutdown { | |||
| 					const muted = isUserRelated(note, userIdsWhoMeMuting); | ||||
| 
 | ||||
| 					if (!isThreadMuted && !muted) { | ||||
| 						nm.push(data.reply.userId, 'reply'); | ||||
| 						this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); | ||||
| 						nm.push(data.reply.userId, 'edited'); | ||||
| 						this.globalEventService.publishMainStream(data.reply.userId, 'edited', noteObj); | ||||
| 
 | ||||
| 						const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); | ||||
| 						const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('edited')); | ||||
| 						for (const webhook of webhooks) { | ||||
| 							this.queueService.webhookDeliver(webhook, 'reply', { | ||||
| 							this.queueService.webhookDeliver(webhook, 'edited', { | ||||
| 								note: noteObj, | ||||
| 							}); | ||||
| 						} | ||||
|  | @ -647,45 +647,6 @@ export class NoteEditService implements OnApplicationShutdown { | |||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			// If it is renote
 | ||||
| 			if (data.renote) { | ||||
| 				const type = this.isQuote(data) ? 'quote' : 'renote'; | ||||
| 
 | ||||
| 				// Notify
 | ||||
| 				if (data.renote.userHost === null) { | ||||
| 					const isThreadMuted = await this.noteThreadMutingsRepository.exists({ | ||||
| 						where: { | ||||
| 							userId: data.renote.userId, | ||||
| 							threadId: data.renote.threadId ?? data.renote.id, | ||||
| 						}, | ||||
| 					}); | ||||
| 
 | ||||
| 					const [ | ||||
| 						userIdsWhoMeMuting, | ||||
| 					] = data.renote.userId ? await Promise.all([ | ||||
| 						this.cacheService.userMutingsCache.fetch(data.renote.userId), | ||||
| 					]) : [new Set<string>()]; | ||||
| 
 | ||||
| 					const muted = isUserRelated(note, userIdsWhoMeMuting); | ||||
| 
 | ||||
| 					if (!isThreadMuted && !muted) { | ||||
| 						nm.push(data.renote.userId, type); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				// Publish event
 | ||||
| 				if ((user.id !== data.renote.userId) && data.renote.userHost === null) { | ||||
| 					this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj); | ||||
| 
 | ||||
| 					const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); | ||||
| 					for (const webhook of webhooks) { | ||||
| 						this.queueService.webhookDeliver(webhook, 'renote', { | ||||
| 							note: noteObj, | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			nm.notify(); | ||||
| 
 | ||||
| 			//#region AP deliver
 | ||||
|  | @ -780,17 +741,17 @@ export class NoteEditService implements OnApplicationShutdown { | |||
| 				detail: true, | ||||
| 			}); | ||||
| 
 | ||||
| 			this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote); | ||||
| 			this.globalEventService.publishMainStream(u.id, 'edited', detailPackedNote); | ||||
| 
 | ||||
| 			const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); | ||||
| 			const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('edited')); | ||||
| 			for (const webhook of webhooks) { | ||||
| 				this.queueService.webhookDeliver(webhook, 'mention', { | ||||
| 				this.queueService.webhookDeliver(webhook, 'edited', { | ||||
| 					note: detailPackedNote, | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			// Create notification
 | ||||
| 			nm.push(u.id, 'mention'); | ||||
| 			nm.push(u.id, 'edited'); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,8 +20,8 @@ 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', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); | ||||
| const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']); | ||||
| const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'edited'] as (typeof notificationTypes[number])[]); | ||||
| const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited']); | ||||
| 
 | ||||
| @Injectable() | ||||
| export class NotificationEntityService implements OnModuleInit { | ||||
|  |  | |||
|  | @ -107,6 +107,12 @@ export type MiNotification = { | |||
| 	type: 'test'; | ||||
| 	id: string; | ||||
| 	createdAt: string; | ||||
| } | { | ||||
| 	type: 'edited'; | ||||
| 	id: string; | ||||
| 	createdAt: string; | ||||
| 	notifierId: MiUser['id']; | ||||
| 	noteId: MiNote['id']; | ||||
| }; | ||||
| 
 | ||||
| export type MiGroupedNotification = MiNotification | { | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ | |||
| import { id } from './util/id.js'; | ||||
| import { MiUser } from './User.js'; | ||||
| 
 | ||||
| export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; | ||||
| export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const; | ||||
| 
 | ||||
| @Entity('webhook') | ||||
| export class MiWebhook { | ||||
|  |  | |||
|  | @ -318,6 +318,31 @@ export const packedNotificationSchema = { | |||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, { | ||||
| 		type: 'object', | ||||
| 		properties: { | ||||
| 			...baseSchema.properties, | ||||
| 			type: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: false, | ||||
| 				enum: ['edited'], | ||||
| 			}, | ||||
| 			user: { | ||||
| 				type: 'object', | ||||
| 				ref: 'UserLite', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			userId: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: false, | ||||
| 				format: 'id', | ||||
| 			}, | ||||
| 			note: { | ||||
| 				type: 'object', | ||||
| 				ref: 'Note', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, { | ||||
| 		type: 'object', | ||||
| 		properties: { | ||||
|  |  | |||
|  | @ -164,7 +164,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 			groupedNotifications = groupedNotifications.slice(0, ps.limit); | ||||
| 
 | ||||
| 			const noteIds = groupedNotifications | ||||
| 				.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) | ||||
| 				.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].includes(notification.type)) | ||||
| 				.map(notification => notification.noteId!); | ||||
| 
 | ||||
| 			if (noteIds.length > 0) { | ||||
|  |  | |||
|  | @ -113,7 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 			} | ||||
| 
 | ||||
| 			const noteIds = notifications | ||||
| 				.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) | ||||
| 				.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].includes(notification.type)) | ||||
| 				.map(notification => notification.noteId); | ||||
| 
 | ||||
| 			if (noteIds.length > 0) { | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| <template> | ||||
| <div :class="$style.root"> | ||||
| 	<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', 'edited'].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/> | ||||
| 		<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="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div> | ||||
|  | @ -25,8 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				[$style.t_pollEnded]: notification.type === 'pollEnded', | ||||
| 				[$style.t_achievementEarned]: notification.type === 'achievementEarned', | ||||
| 				[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, | ||||
| 				[$style.t_pollEnded]: notification.type === 'edited', | ||||
| 			}]" | ||||
| 		> | ||||
| 		> <!-- we re-use t_pollEnded for "edited" instead of making an identical style --> | ||||
| 			<i v-if="notification.type === 'follow'" class="ph-plus ph-bold ph-lg"></i> | ||||
| 			<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold ph-lg"></i> | ||||
| 			<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold ph-lg"></i> | ||||
|  | @ -40,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> | ||||
| 				<i v-else class="ph-seal-check ph-bold ph-lg"></i> | ||||
| 			</template> | ||||
| 			<i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i> | ||||
| 			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> | ||||
| 			<MkReactionIcon | ||||
| 				v-else-if="notification.type === 'reaction'" | ||||
|  | @ -61,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span> | ||||
| 			<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> | ||||
| 			<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> | ||||
| 		</header> | ||||
| 		<div> | ||||
|  | @ -131,6 +134,12 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 					<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<MkA v-else-if="notification.type === 'edited'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> | ||||
| 				<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> | ||||
| 				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> | ||||
| 			</MkA> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -4273,6 +4273,17 @@ export type components = { | |||
|       body: string; | ||||
|       header: string; | ||||
|       icon: string; | ||||
|     } | { | ||||
|       /** Format: id */ | ||||
|       id: string; | ||||
|       /** Format: date-time */ | ||||
|       createdAt: string; | ||||
|       /** @enum {string} */ | ||||
|       type: 'edited'; | ||||
|       user: components['schemas']['UserLite']; | ||||
|       /** Format: id */ | ||||
|       userId: string; | ||||
|       note: components['schemas']['Note']; | ||||
|     } | { | ||||
|       /** Format: id */ | ||||
|       id: string; | ||||
|  | @ -19511,7 +19522,7 @@ export type operations = { | |||
|           url: string; | ||||
|           /** @default */ | ||||
|           secret?: string; | ||||
|           on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; | ||||
|           on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[]; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
|  | @ -19525,7 +19536,7 @@ export type operations = { | |||
|             /** Format: misskey:id */ | ||||
|             userId: string; | ||||
|             name: string; | ||||
|             on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; | ||||
|             on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[]; | ||||
|             url: string; | ||||
|             secret: string; | ||||
|             active: boolean; | ||||
|  | @ -19584,7 +19595,7 @@ export type operations = { | |||
|               /** Format: misskey:id */ | ||||
|               userId: string; | ||||
|               name: string; | ||||
|               on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; | ||||
|               on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[]; | ||||
|               url: string; | ||||
|               secret: string; | ||||
|               active: boolean; | ||||
|  | @ -19651,7 +19662,7 @@ export type operations = { | |||
|             /** Format: misskey:id */ | ||||
|             userId: string; | ||||
|             name: string; | ||||
|             on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; | ||||
|             on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[]; | ||||
|             url: string; | ||||
|             secret: string; | ||||
|             active: boolean; | ||||
|  | @ -19709,7 +19720,7 @@ export type operations = { | |||
|           url: string; | ||||
|           /** @default */ | ||||
|           secret?: string; | ||||
|           on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; | ||||
|           on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[]; | ||||
|           active: boolean; | ||||
|         }; | ||||
|       }; | ||||
|  |  | |||
|  | @ -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', 'edited'] as const; | ||||
| 
 | ||||
| export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; | ||||
| 
 | ||||
|  |  | |||
|  | @ -56,6 +56,7 @@ export type Channels = { | |||
| 			readAntenna: (payload: Antenna) => void; | ||||
| 			receiveFollowRequest: (payload: User) => void; | ||||
| 			announcementCreated: (payload: AnnouncementCreated) => void; | ||||
| 			edited: (payload: Note) => void; | ||||
| 		}; | ||||
| 		receives: null; | ||||
| 	}; | ||||
|  |  | |||
|  | @ -232,6 +232,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif | |||
| 						data, | ||||
| 					}]; | ||||
| 
 | ||||
| 				case 'edited': | ||||
| 					return [t('_notification.edited', { name: getUserName(data.body.user) }), { | ||||
| 						body: data.body.note.text ?? '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('messages'), | ||||
| 						data, | ||||
| 					}]; | ||||
| 
 | ||||
| 				default: | ||||
| 					return null; | ||||
| 			} | ||||
|  |  | |||
|  | @ -133,6 +133,9 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv | |||
| 					case 'showFollowRequests': | ||||
| 						client = await swos.openClient('push', '/my/follow-requests', loginId); | ||||
| 						break; | ||||
| 					case 'edited': | ||||
| 						if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); | ||||
| 						break; | ||||
| 					default: | ||||
| 						switch (data.body.type) { | ||||
| 							case 'receiveFollowRequest': | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue