feat: Add Badge Image to Push Notification (#8012)
* fix * nanka iroiro * wip * wip * fix lint * fix loginId * fix * refactor * refactor * remove follow action * clean up * Revert "remove follow action" This reverts commit defbb416480905af2150d1c92f10d8e1d1288c0a. * Revert "clean up" This reverts commit f94919cb9cff41e274044fc69c56ad36a33974f2. * remove fetch specification * renoteの条件追加 * apiFetch => cli * bypass fetch? * fix * refactor: use path alias * temp: add submodule * remove submodule * enhane: unison-reloadに指定したパスに移動できるように * null * null * feat: ログインするアカウントのIDをクエリ文字列で指定する機能 * null * await? * rename * rename * Update read.ts * merge * get-note-summary * fix * swパッケージに * add missing packages * fix getNoteSummary * add webpack-cli * ✌️ * remove plugins * sw-inject分離したがテストしてない * fix notification.vue * remove a blank line * disconnect intersection observer * disconnect2 * fix notification.vue * remove a blank line * disconnect intersection observer * disconnect2 * fix * ✌️ * clean up config * typesを戻した * backend/src/web/index.ts * notification-badges * add scripts * change create-notification.ts * Update packages/client/src/components/notification.vue Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * disconnect * oops * Failed to load the script unexpectedly回避 sw.jsとlib.tsを分離してみた * truncate notification * Update packages/client/src/ui/_common_/common.vue Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * clean up * clean up * refactor * キャッシュ対策 * Truncate push notification message * fix * クライアントがあったらストリームに接続しているということなので通知しない判定の位置を修正 * components/drive-file-thumbnail.vue * components/drive-select-dialog.vue * components/drive-window.vue * merge * fix * Service Workerのビルドにesbuildを使うようにする * return createEmptyNotification() * fix * fix * i18n.ts * update * ✌️ * remove ts-loader * fix * fix * enhance: Service Workerを常に登録するように * pollEnded * pollEnded * URLをsw.jsに戻す * clean up * fix lint * changelog * alpha-test * also with twemoji * add isMimeImage function * catch * Colour => Color * char2file => char2filePath * Update autocomplete.vue * remove clone? Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		
							parent
							
								
									54465d36a7
								
							
						
					
					
						commit
						b70473ed60
					
				
					 24 changed files with 197 additions and 21 deletions
				
			
		|  | @ -9,6 +9,10 @@ import { pushNotificationDataMap } from '@/types'; | |||
| import getUserName from '@/scripts/get-user-name'; | ||||
| import { I18n } from '@/scripts/i18n'; | ||||
| import { getAccountFromId } from '@/scripts/get-account-from-id'; | ||||
| import { char2fileName } from '@/scripts/twemoji-base'; | ||||
| import * as url from '@/scripts/url'; | ||||
| 
 | ||||
| const iconUrl = (name: string) => `/static-assets/notification-badges/${name}.png`; | ||||
| 
 | ||||
| export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) { | ||||
| 	const n = await composeNotification(data); | ||||
|  | @ -44,6 +48,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.youWereFollowed'), { | ||||
| 						body: getUserName(data.body.user), | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('user-plus'), | ||||
| 						data, | ||||
| 						actions: userDetail.isFollowing ? [] : [ | ||||
| 							{ | ||||
|  | @ -57,6 +62,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { | ||||
| 						body: data.body.note.text || '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('at'), | ||||
| 						data, | ||||
| 						actions: [ | ||||
| 							{ | ||||
|  | @ -70,6 +76,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { | ||||
| 						body: data.body.note.text || '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('reply'), | ||||
| 						data, | ||||
| 						actions: [ | ||||
| 							{ | ||||
|  | @ -83,6 +90,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { | ||||
| 						body: data.body.note.text || '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('retweet'), | ||||
| 						data, | ||||
| 						actions: [ | ||||
| 							{ | ||||
|  | @ -96,6 +104,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { | ||||
| 						body: data.body.note.text || '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('quote-right'), | ||||
| 						data, | ||||
| 						actions: [ | ||||
| 							{ | ||||
|  | @ -112,9 +121,44 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					}]; | ||||
| 
 | ||||
| 				case 'reaction': | ||||
| 					return [`${data.body.reaction} ${getUserName(data.body.user)}`, { | ||||
| 					let reaction = data.body.reaction; | ||||
| 					let badge: string | undefined; | ||||
| 
 | ||||
| 					if (reaction.startsWith(':')) { | ||||
| 						// カスタム絵文字の場合
 | ||||
| 						const customEmoji = data.body.note.emojis.find(x => x.name === reaction.substr(1, reaction.length - 2)); | ||||
| 						if (customEmoji) { | ||||
| 							if (reaction.includes('@')) { | ||||
| 								reaction = `:${reaction.substr(1, reaction.indexOf('@') - 1)}:`; | ||||
| 							} | ||||
| 
 | ||||
| 							const u = new URL(customEmoji.url); | ||||
| 							if (u.href.startsWith(`${origin}/proxy/`)) { | ||||
| 								// もう既にproxyっぽそうだったらsearchParams付けるだけ
 | ||||
| 								u.searchParams.set('badge', '1'); | ||||
| 								badge = u.href; | ||||
| 							} else { | ||||
| 								const dummy = `${u.host}${u.pathname}`;	// 拡張子がないとキャッシュしてくれないCDNがあるので
 | ||||
| 								badge = `${origin}/proxy/${dummy}?${url.query({ | ||||
| 									url: u.href, | ||||
| 									badge: '1' | ||||
| 								})}`;
 | ||||
| 							} | ||||
| 						} | ||||
| 					} else { | ||||
| 						// Unicode絵文字の場合
 | ||||
| 						badge = `/twemoji-badge/${char2fileName(reaction)}.png`; | ||||
| 					} | ||||
| 
 | ||||
| 
 | ||||
| 					if (badge ? await fetch(badge).then(res => res.status !== 200).catch(() => true) : true) { | ||||
| 						badge = iconUrl('plus'); | ||||
| 					} | ||||
| 
 | ||||
| 					return [`${reaction} ${getUserName(data.body.user)}`, { | ||||
| 						body: data.body.note.text || '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge, | ||||
| 						data, | ||||
| 						actions: [ | ||||
| 							{ | ||||
|  | @ -128,12 +172,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), { | ||||
| 						body: data.body.note.text || '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('poll-h'), | ||||
| 						data, | ||||
| 					}]; | ||||
| 
 | ||||
| 				case 'pollEnded': | ||||
| 					return [t('_notification.pollEnded'), { | ||||
| 						body: data.body.note.text || '', | ||||
| 						badge: iconUrl('clipboard-check-solid'), | ||||
| 						data, | ||||
| 					}]; | ||||
| 
 | ||||
|  | @ -141,6 +187,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.youReceivedFollowRequest'), { | ||||
| 						body: getUserName(data.body.user), | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('clock'), | ||||
| 						data, | ||||
| 						actions: [ | ||||
| 							{ | ||||
|  | @ -158,12 +205,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 					return [t('_notification.yourFollowRequestAccepted'), { | ||||
| 						body: getUserName(data.body.user), | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('check'), | ||||
| 						data, | ||||
| 					}]; | ||||
| 
 | ||||
| 				case 'groupInvited': | ||||
| 					return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), { | ||||
| 						body: data.body.invitation.group.name, | ||||
| 						badge: iconUrl('id-card-alt'), | ||||
| 						data, | ||||
| 						actions: [ | ||||
| 							{ | ||||
|  | @ -191,6 +240,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 			if (data.body.groupId === null) { | ||||
| 				return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), { | ||||
| 					icon: data.body.user.avatarUrl, | ||||
| 					badge: iconUrl('comments'), | ||||
| 					tag: `messaging:user:${data.body.userId}`, | ||||
| 					data, | ||||
| 					renotify: true, | ||||
|  | @ -198,6 +248,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | |||
| 			} | ||||
| 			return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), { | ||||
| 				icon: data.body.user.avatarUrl, | ||||
| 				badge: iconUrl('comments'), | ||||
| 				tag: `messaging:group:${data.body.groupId}`, | ||||
| 				data, | ||||
| 				renotify: true, | ||||
|  | @ -217,6 +268,7 @@ export async function createEmptyNotification() { | |||
| 			t('_notification.emptyPushNotificationMessage'), | ||||
| 			{ | ||||
| 				silent: true, | ||||
| 				badge: iconUrl('null'), | ||||
| 				tag: 'read_notification', | ||||
| 			} | ||||
| 		); | ||||
|  |  | |||
							
								
								
									
										12
									
								
								packages/sw/src/scripts/twemoji-base.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/sw/src/scripts/twemoji-base.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| export const twemojiSvgBase = '/twemoji'; | ||||
| 
 | ||||
| export function char2fileName(char: string): string { | ||||
| 	let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); | ||||
| 	if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); | ||||
| 	codes = codes.filter(x => x && x.length); | ||||
| 	return codes.join('-'); | ||||
| } | ||||
| 
 | ||||
| export function char2filePath(char: string): string { | ||||
| 	return `${twemojiSvgBase}/${char2fileName(char)}.svg`; | ||||
| } | ||||
							
								
								
									
										13
									
								
								packages/sw/src/scripts/url.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/sw/src/scripts/url.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| export function query(obj: {}): string { | ||||
| 	const params = Object.entries(obj) | ||||
| 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | ||||
| 		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); | ||||
| 
 | ||||
| 	return Object.entries(params) | ||||
| 		.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`) | ||||
| 		.join('&'); | ||||
| } | ||||
| 
 | ||||
| export function appendQuery(url: string, query: string): string { | ||||
| 	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue