Display notification when receving

This commit is contained in:
AkiraFukushima 2023-11-09 23:29:10 +09:00
parent 5d81133b19
commit cd87a921ea
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
7 changed files with 198 additions and 4 deletions

View File

@ -24,34 +24,44 @@
},
"notification": {
"favourite": {
"title": "Favourite",
"body": "{user} favourited your post"
},
"reblog": {
"title": "Boost",
"body": "{user} boosted your post"
},
"quote": {
"title": "Quote",
"body": "{user} quoted your post"
},
"poll_expired": {
"title": "Poll",
"body": "{user} poll has ended"
},
"poll_vote": {
"title": "Poll",
"body": "{user} voted your poll"
},
"status": {
"title": "Status",
"body": "{user} just posted"
},
"update": {
"title": "Update",
"body": "{user} updated the post"
},
"emoji_reaction": {
"title": "Reaction",
"body": "{user} reacted your post"
},
"follow": {
"title": "Follow",
"body": "{user} followed you",
"followers": "{num} followers"
},
"follow_request": {
"title": "Follow Request",
"body": "{user} requested to follow you"
}
}

View File

@ -21,13 +21,15 @@
"megalodon": "^9.1.1",
"react-icons": "^4.11.0",
"react-intl": "^6.5.1",
"react-virtuoso": "^4.6.2"
"react-virtuoso": "^4.6.2",
"sanitize-html": "^2.11.0"
},
"devDependencies": {
"@babel/runtime-corejs3": "^7.23.2",
"@electron/notarize": "^2.1.0",
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.4",
"autoprefixer": "^10.4.16",
"electron": "^26.2.2",
"electron-builder": "^24.6.4",

View File

@ -47,6 +47,7 @@ export default function Notifications(props: Props) {
if (streaming.current) {
streaming.current.removeAllListeners()
streaming.current.stop()
streaming.current = null
console.log('closed notifications')
}
}

View File

@ -62,6 +62,7 @@ export default function Timeline(props: Props) {
if (streaming.current) {
streaming.current.removeAllListeners()
streaming.current.stop()
streaming.current = null
console.log(`closed ${props.timeline}`)
}
}

View File

@ -1,14 +1,18 @@
import { useRouter } from 'next/router'
import Timeline from '@/components/timelines/Timeline'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Account, db } from '@/db'
import generator, { MegalodonInterface } from 'megalodon'
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
import Notifications from '@/components/timelines/Notifications'
import generateNotification from '@/utils/notification'
import { useIntl } from 'react-intl'
export default function Page() {
const router = useRouter()
const [account, setAccount] = useState<Account | null>(null)
const [client, setClient] = useState<MegalodonInterface>(null)
const streaming = useRef<WebSocketInterface | null>(null)
const { formatMessage } = useIntl()
useEffect(() => {
if (router.query.id) {
@ -18,10 +22,33 @@ export default function Page() {
setAccount(a)
const c = generator(a.sns, a.url, a.access_token, 'Whalebird')
setClient(c)
// Start user streaming for notification
const instance = await c.getInstance()
const ws = generator(a.sns, instance.data.urls.streaming_api, a.access_token, 'Whalebird')
streaming.current = ws.userSocket()
streaming.current.on('connect', () => {
console.log('connect to user streaming')
})
streaming.current.on('notification', (notification: Entity.Notification) => {
const [title, body] = generateNotification(notification, formatMessage)
if (title.length > 0) {
new window.Notification(title, { body: body })
}
})
}
}
f()
}
return () => {
if (streaming.current) {
streaming.current.removeAllListeners()
streaming.current.stop()
streaming.current = null
console.log('close user streaming')
}
}
}, [router.query.id])
if (!account || !client) return null

View File

@ -0,0 +1,74 @@
import sanitizeHtml from 'sanitize-html'
import { Entity } from 'megalodon'
import { MessageDescriptor } from 'react-intl'
const generateNotification = (
notification: Entity.Notification,
formatMessage: (descriptor: MessageDescriptor, values?: any, opts?: any) => string
): [string, string] => {
switch (notification.type) {
case 'follow':
return [
formatMessage({ id: 'timeline.notification.follow.title' }),
formatMessage({ id: 'timeline.notification.follow.body' }, { user: notification.account.acct })
]
case 'follow_request':
return [
formatMessage({ id: 'timeline.notification.follow_request.title' }),
formatMessage({ id: 'timeline.notification.follow_requested.body' }, { user: notification.account.acct })
]
case 'favourite':
return [
formatMessage({ id: 'timeline.notification.favourite.title' }),
formatMessage({ id: 'timeline.notification.favourite.body' }, { user: notification.account.acct })
]
case 'reblog':
return [
formatMessage({ id: 'timeline.notification.reblog.title' }),
formatMessage({ id: 'timeline.notification.reblog.body' }, { user: notification.account.acct })
]
case 'poll_expired':
return [
formatMessage({ id: 'timeline.notification.poll_expired.title' }),
formatMessage({ id: 'timeline.notification.poll_expired.body' }, { user: notification.account.acct })
]
case 'poll_vote':
return [
formatMessage({ id: 'timeline.notification.poll_vote.title' }),
formatMessage({ id: 'timeline.notification.poll_vote.body' }, { user: notification.account.acct })
]
case 'quote':
return [
formatMessage({ id: 'timeline.notification.quote.title' }),
formatMessage({ id: 'timeline.notification.quote.body' }, { user: notification.account.acct })
]
case 'status':
return [
formatMessage({ id: 'timeline.notification.status.title' }),
formatMessage({ id: 'timeline.notification.status.body' }, { user: notification.account.acct })
]
case 'update':
return [
formatMessage({ id: 'timeline.notification.update.title' }),
formatMessage({ id: 'timeline.notification.update.body' }, { user: notification.account.acct })
]
case 'emoji_reaction':
case 'reaction':
return [
formatMessage({ id: 'timeline.notification.emoji_reaction.title' }),
formatMessage({ id: 'timeline.notification.emoji_reaction.body' }, { user: notification.account.acct })
]
case 'mention':
return [
`${notification.account.acct}`,
sanitizeHtml(notification.status!.content, {
allowedTags: [],
allowedAttributes: false
})
]
default:
return ['', '']
}
}
export default generateNotification

View File

@ -1480,6 +1480,13 @@
dependencies:
"@types/node" "*"
"@types/sanitize-html@^2.9.4":
version "2.9.4"
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.9.4.tgz#bfc2df463ec35904fecc57b29ba080e53732a140"
integrity sha512-Ym4hjmAFxF/eux7nW2yDPAj2o9RYh0vP/9V5ECoHtgJ/O9nPGslUd20CMn6WatRMlFVfjMTg3lMcWq8YyO6QnA==
dependencies:
htmlparser2 "^8.0.0"
"@types/scheduler@*":
version "0.16.5"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af"
@ -2271,6 +2278,11 @@ decompress-response@^6.0.0:
dependencies:
mimic-response "^3.1.0"
deepmerge@^4.2.2:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
defer-to-connect@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
@ -2355,6 +2367,36 @@ dmg-license@^1.0.11:
smart-buffer "^4.0.2"
verror "^1.10.0"
dom-serializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
entities "^4.2.0"
domelementtype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
domutils@^3.0.1:
version "3.1.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
dependencies:
dom-serializer "^2.0.0"
domelementtype "^2.3.0"
domhandler "^5.0.3"
dot-prop@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083"
@ -2461,6 +2503,11 @@ enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0:
graceful-fs "^4.2.4"
tapable "^2.2.0"
entities@^4.2.0, entities@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
env-paths@^2.2.0, env-paths@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
@ -2915,6 +2962,16 @@ hosted-git-info@^4.1.0:
dependencies:
lru-cache "^6.0.0"
htmlparser2@^8.0.0:
version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.3"
domutils "^3.0.1"
entities "^4.4.0"
http-cache-semantics@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
@ -3048,6 +3105,11 @@ is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@ -3563,6 +3625,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
parse-srcset@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@ -3689,7 +3756,7 @@ postcss@8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.23, postcss@^8.4.31:
postcss@^8.3.11, postcss@^8.4.23, postcss@^8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
@ -3954,6 +4021,18 @@ sanitize-filename@^1.6.3:
dependencies:
truncate-utf8-bytes "^1.0.0"
sanitize-html@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.11.0.tgz#9a6434ee8fcaeddc740d8ae7cd5dd71d3981f8f6"
integrity sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==
dependencies:
deepmerge "^4.2.2"
escape-string-regexp "^4.0.0"
htmlparser2 "^8.0.0"
is-plain-object "^5.0.0"
parse-srcset "^1.0.2"
postcss "^8.3.11"
sax@^1.2.4:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"