From cd87a921ea0ac8a5b49134b9dcb8976f5a421bd6 Mon Sep 17 00:00:00 2001
From: AkiraFukushima
Date: Thu, 9 Nov 2023 23:29:10 +0900
Subject: [PATCH] Display notification when receving
---
locales/en/translation.json | 10 +++
package.json | 4 +-
.../components/timelines/Notifications.tsx | 1 +
renderer/components/timelines/Timeline.tsx | 1 +
renderer/pages/accounts/[id]/[timeline].tsx | 31 ++++++-
renderer/utils/notification.ts | 74 +++++++++++++++++
yarn.lock | 81 ++++++++++++++++++-
7 files changed, 198 insertions(+), 4 deletions(-)
create mode 100644 renderer/utils/notification.ts
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 4b27a4b863..8ea9603e7a 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -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"
}
}
diff --git a/package.json b/package.json
index 28b4df6c5d..9984cd9d33 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/renderer/components/timelines/Notifications.tsx b/renderer/components/timelines/Notifications.tsx
index 304b434eee..2380d0b6ad 100644
--- a/renderer/components/timelines/Notifications.tsx
+++ b/renderer/components/timelines/Notifications.tsx
@@ -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')
}
}
diff --git a/renderer/components/timelines/Timeline.tsx b/renderer/components/timelines/Timeline.tsx
index c3d044173f..566732b345 100644
--- a/renderer/components/timelines/Timeline.tsx
+++ b/renderer/components/timelines/Timeline.tsx
@@ -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}`)
}
}
diff --git a/renderer/pages/accounts/[id]/[timeline].tsx b/renderer/pages/accounts/[id]/[timeline].tsx
index f53586ab7f..04308e29d9 100644
--- a/renderer/pages/accounts/[id]/[timeline].tsx
+++ b/renderer/pages/accounts/[id]/[timeline].tsx
@@ -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(null)
const [client, setClient] = useState(null)
+ const streaming = useRef(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
diff --git a/renderer/utils/notification.ts b/renderer/utils/notification.ts
new file mode 100644
index 0000000000..1bbd7ab334
--- /dev/null
+++ b/renderer/utils/notification.ts
@@ -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
diff --git a/yarn.lock b/yarn.lock
index bd63224545..52ccb37505 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"