parent
							
								
									8bb586c1fd
								
							
						
					
					
						commit
						2442592ef1
					
				
					 14 changed files with 108 additions and 10 deletions
				
			
		|  | @ -21,6 +21,7 @@ You should also include the user name that made the change. | ||||||
| ### Improvements | ### Improvements | ||||||
| - インスタンスデフォルトテーマを設定できるように @syuilo | - インスタンスデフォルトテーマを設定できるように @syuilo | ||||||
| - ミュートに期限を設定できるように @syuilo | - ミュートに期限を設定できるように @syuilo | ||||||
|  | - アンケートが終了したときに通知が作成されるように @syuilo | ||||||
| - プロフィールの追加情報を最大16まで保存できるように @syuilo | - プロフィールの追加情報を最大16まで保存できるように @syuilo | ||||||
| - 連合チャートにPub&Subを追加 @syuilo | - 連合チャートにPub&Subを追加 @syuilo | ||||||
| - デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo | - デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo | ||||||
|  |  | ||||||
|  | @ -1667,6 +1667,7 @@ _notification: | ||||||
|   youReceivedFollowRequest: "フォローリクエストが来ました" |   youReceivedFollowRequest: "フォローリクエストが来ました" | ||||||
|   yourFollowRequestAccepted: "フォローリクエストが承認されました" |   yourFollowRequestAccepted: "フォローリクエストが承認されました" | ||||||
|   youWereInvitedToGroup: "グループに招待されました" |   youWereInvitedToGroup: "グループに招待されました" | ||||||
|  |   pollEnded: "アンケートの結果が出ました" | ||||||
| 
 | 
 | ||||||
|   _types: |   _types: | ||||||
|     all: "すべて" |     all: "すべて" | ||||||
|  | @ -1677,6 +1678,7 @@ _notification: | ||||||
|     quote: "引用" |     quote: "引用" | ||||||
|     reaction: "リアクション" |     reaction: "リアクション" | ||||||
|     pollVote: "アンケートに投票された" |     pollVote: "アンケートに投票された" | ||||||
|  |     pollEnded: "アンケートが終了" | ||||||
|     receiveFollowRequest: "フォロー申請を受け取った" |     receiveFollowRequest: "フォロー申請を受け取った" | ||||||
|     followRequestAccepted: "フォローが受理された" |     followRequestAccepted: "フォローが受理された" | ||||||
|     groupInvited: "グループに招待された" |     groupInvited: "グループに招待された" | ||||||
|  |  | ||||||
|  | @ -0,0 +1,18 @@ | ||||||
|  | 
 | ||||||
|  | export class pollEndedNotification1646549089451 { | ||||||
|  |     name = 'pollEndedNotification1646549089451' | ||||||
|  | 
 | ||||||
|  |     async up(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`); | ||||||
|  |         await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`); | ||||||
|  |         await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async down(queryRunner) { | ||||||
|  |         await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`); | ||||||
|  |         await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`); | ||||||
|  |         await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -59,7 +59,8 @@ export class Notification { | ||||||
| 	 * renote - (自分または自分がWatchしている)投稿がRenoteされた | 	 * renote - (自分または自分がWatchしている)投稿がRenoteされた | ||||||
| 	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた | 	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた | ||||||
| 	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた | 	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた | ||||||
| 	 * pollVote - (自分または自分がWatchしている)投稿の投票に投票された | 	 * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された | ||||||
|  | 	 * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した | ||||||
| 	 * receiveFollowRequest - フォローリクエストされた | 	 * receiveFollowRequest - フォローリクエストされた | ||||||
| 	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された | 	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された | ||||||
| 	 * groupInvited - グループに招待された | 	 * groupInvited - グループに招待された | ||||||
|  |  | ||||||
|  | @ -67,6 +67,12 @@ export class NotificationRepository extends Repository<Notification> { | ||||||
| 				}), | 				}), | ||||||
| 				choice: notification.choice, | 				choice: notification.choice, | ||||||
| 			} : {}), | 			} : {}), | ||||||
|  | 			...(notification.type === 'pollEnded' ? { | ||||||
|  | 				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { | ||||||
|  | 					detail: true, | ||||||
|  | 					_hint_: options._hintForEachNotes_, | ||||||
|  | 				}), | ||||||
|  | 			} : {}), | ||||||
| 			...(notification.type === 'groupInvited' ? { | 			...(notification.type === 'groupInvited' ? { | ||||||
| 				invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), | 				invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), | ||||||
| 			} : {}), | 			} : {}), | ||||||
|  |  | ||||||
|  | @ -8,10 +8,11 @@ import processInbox from './processors/inbox.js'; | ||||||
| import processDb from './processors/db/index.js'; | import processDb from './processors/db/index.js'; | ||||||
| import processObjectStorage from './processors/object-storage/index.js'; | import processObjectStorage from './processors/object-storage/index.js'; | ||||||
| import processSystemQueue from './processors/system/index.js'; | import processSystemQueue from './processors/system/index.js'; | ||||||
|  | import { endedPollNotification } from './processors/ended-poll-notification.js'; | ||||||
| import { queueLogger } from './logger.js'; | import { queueLogger } from './logger.js'; | ||||||
| import { DriveFile } from '@/models/entities/drive-file.js'; | import { DriveFile } from '@/models/entities/drive-file.js'; | ||||||
| import { getJobInfo } from './get-job-info.js'; | import { getJobInfo } from './get-job-info.js'; | ||||||
| import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues.js'; | import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue } from './queues.js'; | ||||||
| import { ThinUser } from './types.js'; | import { ThinUser } from './types.js'; | ||||||
| import { IActivity } from '@/remote/activitypub/type.js'; | import { IActivity } from '@/remote/activitypub/type.js'; | ||||||
| 
 | 
 | ||||||
|  | @ -255,6 +256,7 @@ export default function() { | ||||||
| 
 | 
 | ||||||
| 	deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); | 	deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); | ||||||
| 	inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); | 	inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); | ||||||
|  | 	endedPollNotificationQueue.process(endedPollNotification); | ||||||
| 	processDb(dbQueue); | 	processDb(dbQueue); | ||||||
| 	processObjectStorage(objectStorageQueue); | 	processObjectStorage(objectStorageQueue); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,33 @@ | ||||||
|  | import Bull from 'bull'; | ||||||
|  | import { In } from 'typeorm'; | ||||||
|  | import { Notes, Polls, PollVotes } from '@/models/index.js'; | ||||||
|  | import { queueLogger } from '../logger.js'; | ||||||
|  | import { EndedPollNotificationJobData } from '@/queue/types.js'; | ||||||
|  | import { createNotification } from '@/services/create-notification.js'; | ||||||
|  | 
 | ||||||
|  | const logger = queueLogger.createSubLogger('ended-poll-notification'); | ||||||
|  | 
 | ||||||
|  | export async function endedPollNotification(job: Bull.Job<EndedPollNotificationJobData>, done: any): Promise<void> { | ||||||
|  | 	const note = await Notes.findOne(job.data.noteId); | ||||||
|  | 	if (note == null || !note.hasPoll) { | ||||||
|  | 		done(); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const votes = await PollVotes.createQueryBuilder('vote') | ||||||
|  | 		.select('vote.userId') | ||||||
|  | 		.where('vote.noteId = :noteId', { noteId: note.id }) | ||||||
|  | 		.innerJoinAndSelect('vote.user', 'user') | ||||||
|  | 		.andWhere('user.host IS NULL') | ||||||
|  | 		.getMany(); | ||||||
|  | 
 | ||||||
|  | 	const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; | ||||||
|  | 
 | ||||||
|  | 	for (const userId of userIds) { | ||||||
|  | 		createNotification(userId, 'pollEnded', { | ||||||
|  | 			noteId: note.id, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	done(); | ||||||
|  | } | ||||||
|  | @ -1,8 +1,9 @@ | ||||||
| import config from '@/config/index.js'; | import config from '@/config/index.js'; | ||||||
| import { initialize as initializeQueue } from './initialize.js'; | import { initialize as initializeQueue } from './initialize.js'; | ||||||
| import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types.js'; | import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData } from './types.js'; | ||||||
| 
 | 
 | ||||||
| export const systemQueue = initializeQueue<Record<string, unknown>>('system'); | export const systemQueue = initializeQueue<Record<string, unknown>>('system'); | ||||||
|  | export const endedPollNotificationQueue = initializeQueue<EndedPollNotificationJobData>('endedPollNotification'); | ||||||
| export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128); | export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128); | ||||||
| export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16); | export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16); | ||||||
| export const dbQueue = initializeQueue<DbJobData>('db'); | export const dbQueue = initializeQueue<DbJobData>('db'); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { DriveFile } from '@/models/entities/drive-file.js'; | import { DriveFile } from '@/models/entities/drive-file.js'; | ||||||
|  | import { Note } from '@/models/entities/note'; | ||||||
| import { User } from '@/models/entities/user.js'; | import { User } from '@/models/entities/user.js'; | ||||||
| import { IActivity } from '@/remote/activitypub/type.js'; | import { IActivity } from '@/remote/activitypub/type.js'; | ||||||
| import httpSignature from 'http-signature'; | import httpSignature from 'http-signature'; | ||||||
|  | @ -41,6 +42,10 @@ export type ObjectStorageFileJobData = { | ||||||
| 	key: string; | 	key: string; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export type EndedPollNotificationJobData = { | ||||||
|  | 	noteId: Note['id']; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export type ThinUser = { | export type ThinUser = { | ||||||
| 	id: User['id']; | 	id: User['id']; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -34,6 +34,7 @@ import { deliverToRelays } from '../relay.js'; | ||||||
| import { Channel } from '@/models/entities/channel.js'; | import { Channel } from '@/models/entities/channel.js'; | ||||||
| import { normalizeForSearch } from '@/misc/normalize-for-search.js'; | import { normalizeForSearch } from '@/misc/normalize-for-search.js'; | ||||||
| import { getAntennas } from '@/misc/antenna-cache.js'; | import { getAntennas } from '@/misc/antenna-cache.js'; | ||||||
|  | import { endedPollNotificationQueue } from '@/queue/queues.js'; | ||||||
| 
 | 
 | ||||||
| type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; | type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; | ||||||
| 
 | 
 | ||||||
|  | @ -296,6 +297,15 @@ export default async (user: { id: User['id']; username: User['username']; host: | ||||||
| 		incRenoteCount(data.renote); | 		incRenoteCount(data.renote); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if (data.poll && data.poll.expiresAt) { | ||||||
|  | 		const delay = data.poll.expiresAt.getTime() - Date.now(); | ||||||
|  | 		endedPollNotificationQueue.add({ | ||||||
|  | 			noteId: note.id, | ||||||
|  | 		}, { | ||||||
|  | 			delay | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if (!silent) { | 	if (!silent) { | ||||||
| 		if (Users.isLocalUser(user)) activeUsersChart.write(user); | 		if (Users.isLocalUser(user)) activeUsersChart.write(user); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; | export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; | ||||||
| 
 | 
 | ||||||
| export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; | export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <div ref="elRef" v-size="{ max: [500, 600] }" class="qglefbjs" :class="notification.type"> | <div ref="elRef" v-size="{ max: [500, 600] }" class="qglefbjs" :class="notification.type"> | ||||||
| 	<div class="head"> | 	<div class="head"> | ||||||
| 		<MkAvatar v-if="notification.user" class="icon" :user="notification.user"/> | 		<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/> | ||||||
|  | 		<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/> | ||||||
| 		<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/> | 		<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/> | ||||||
| 		<div class="sub-icon" :class="notification.type"> | 		<div class="sub-icon" :class="notification.type"> | ||||||
| 			<i v-if="notification.type === 'follow'" class="fas fa-plus"></i> | 			<i v-if="notification.type === 'follow'" class="fas fa-plus"></i> | ||||||
|  | @ -13,6 +14,7 @@ | ||||||
| 			<i v-else-if="notification.type === 'mention'" class="fas fa-at"></i> | 			<i v-else-if="notification.type === 'mention'" class="fas fa-at"></i> | ||||||
| 			<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i> | 			<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i> | ||||||
| 			<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i> | 			<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i> | ||||||
|  | 			<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i> | ||||||
| 			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> | 			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> | ||||||
| 			<XReactionIcon v-else-if="notification.type === 'reaction'" | 			<XReactionIcon v-else-if="notification.type === 'reaction'" | ||||||
| 				ref="reactionRef" | 				ref="reactionRef" | ||||||
|  | @ -24,7 +26,8 @@ | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="tail"> | 	<div class="tail"> | ||||||
| 		<header> | 		<header> | ||||||
| 			<MkA v-if="notification.user" v-user-preview="notification.user.id" class="name" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> | 			<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> | ||||||
|  | 			<MkA v-else-if="notification.user" v-user-preview="notification.user.id" class="name" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> | ||||||
| 			<span v-else>{{ notification.header }}</span> | 			<span v-else>{{ notification.header }}</span> | ||||||
| 			<MkTime v-if="withTime" :time="notification.createdAt" class="time"/> | 			<MkTime v-if="withTime" :time="notification.createdAt" class="time"/> | ||||||
| 		</header> | 		</header> | ||||||
|  | @ -52,6 +55,11 @@ | ||||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | ||||||
| 			<i class="fas fa-quote-right"></i> | 			<i class="fas fa-quote-right"></i> | ||||||
| 		</MkA> | 		</MkA> | ||||||
|  | 		<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
|  | 			<i class="fas fa-quote-left"></i> | ||||||
|  | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | ||||||
|  | 			<i class="fas fa-quote-right"></i> | ||||||
|  | 		</MkA> | ||||||
| 		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> | 		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> | ||||||
| 		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $ts.followRequestAccepted }}</span> | 		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $ts.followRequestAccepted }}</span> | ||||||
| 		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $ts.reject }}</button></div></span> | 		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $ts.reject }}</button></div></span> | ||||||
|  | @ -169,6 +177,7 @@ export default defineComponent({ | ||||||
| 			rejectGroupInvitation, | 			rejectGroupInvitation, | ||||||
| 			elRef, | 			elRef, | ||||||
| 			reactionRef, | 			reactionRef, | ||||||
|  | 			i18n, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
|  | @ -274,6 +283,12 @@ export default defineComponent({ | ||||||
| 				background: #88a6b7; | 				background: #88a6b7; | ||||||
| 				pointer-events: none; | 				pointer-events: none; | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			&.pollEnded { | ||||||
|  | 				padding: 3px; | ||||||
|  | 				background: #88a6b7; | ||||||
|  | 				pointer-events: none; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -35,19 +35,18 @@ const props = defineProps<{ | ||||||
| 
 | 
 | ||||||
| const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | ||||||
| 
 | 
 | ||||||
| const allIncludeTypes = computed(() => props.includeTypes ?? notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x))); |  | ||||||
| 
 |  | ||||||
| const pagination: Paging = { | const pagination: Paging = { | ||||||
| 	endpoint: 'i/notifications' as const, | 	endpoint: 'i/notifications' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| 	params: computed(() => ({ | 	params: computed(() => ({ | ||||||
| 		includeTypes: allIncludeTypes.value || undefined, | 		includeTypes: props.includeTypes ?? undefined, | ||||||
|  | 		excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes, | ||||||
| 		unreadOnly: props.unreadOnly, | 		unreadOnly: props.unreadOnly, | ||||||
| 	})), | 	})), | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const onNotification = (notification) => { | const onNotification = (notification) => { | ||||||
| 	const isMuted = !allIncludeTypes.value.includes(notification.type); | 	const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type); | ||||||
| 	if (isMuted || document.visibilityState === 'visible') { | 	if (isMuted || document.visibilityState === 'visible') { | ||||||
| 		stream.send('readNotification', { | 		stream.send('readNotification', { | ||||||
| 			id: notification.id | 			id: notification.id | ||||||
|  |  | ||||||
|  | @ -59,6 +59,11 @@ export default async function(type, data, i18n): Promise<[string, NotificationOp | ||||||
| 						icon: data.user.avatarUrl | 						icon: data.user.avatarUrl | ||||||
| 					}]; | 					}]; | ||||||
| 
 | 
 | ||||||
|  | 				case 'pollEnded': | ||||||
|  | 					return [i18n.t('_notification.pollEnded'), { | ||||||
|  | 						body: data.note.text, | ||||||
|  | 					}]; | ||||||
|  | 
 | ||||||
| 				case 'follow': | 				case 'follow': | ||||||
| 					return [i18n.t('_notification.youWereFollowed'), { | 					return [i18n.t('_notification.youWereFollowed'), { | ||||||
| 						body: getUserName(data.user), | 						body: getUserName(data.user), | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue