aboutsummaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/services')
-rw-r--r--src/services/api/api.service.js4
-rw-r--r--src/services/desktop_notification_utils/desktop_notification_utils.js39
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js1
-rw-r--r--src/services/favicon_service/favicon_service.js5
-rw-r--r--src/services/notification_utils/notification_utils.js81
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js14
-rw-r--r--src/services/sw/sw.js (renamed from src/services/push/push.js)55
7 files changed, 153 insertions, 46 deletions
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index f45e3958..bde2e163 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -671,6 +671,7 @@ const fetchTimeline = ({
timeline,
credentials,
since = false,
+ minId = false,
until = false,
userId = false,
listId = false,
@@ -705,6 +706,9 @@ const fetchTimeline = ({
url = url(listId)
}
+ if (minId) {
+ params.push(['min_id', minId])
+ }
if (since) {
params.push(['since_id', since])
}
diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js
index b84a1f75..80b8c6e0 100644
--- a/src/services/desktop_notification_utils/desktop_notification_utils.js
+++ b/src/services/desktop_notification_utils/desktop_notification_utils.js
@@ -1,9 +1,38 @@
+import {
+ showDesktopNotification as swDesktopNotification,
+ closeDesktopNotification as swCloseDesktopNotification,
+ isSWSupported
+} from '../sw/sw.js'
+const state = { failCreateNotif: false }
+
export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
if (!('Notification' in window && window.Notification.permission === 'granted')) return
- if (rootState.statuses.notifications.desktopNotificationSilence) { return }
+ if (rootState.notifications.desktopNotificationSilence) { return }
+
+ if (isSWSupported()) {
+ swDesktopNotification(desktopNotificationOpts)
+ } else if (!state.failCreateNotif) {
+ try {
+ const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
+ setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
+ } catch {
+ state.failCreateNotif = true
+ }
+ }
+}
+
+export const closeDesktopNotification = (rootState, { id }) => {
+ if (!('Notification' in window && window.Notification.permission === 'granted')) return
+
+ if (isSWSupported()) {
+ swCloseDesktopNotification({ id })
+ }
+}
+
+export const closeAllDesktopNotifications = (rootState) => {
+ if (!('Notification' in window && window.Notification.permission === 'granted')) return
- const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
- // Chrome is known for not closing notifications automatically
- // according to MDN, anyway.
- setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
+ if (isSWSupported()) {
+ swCloseDesktopNotification({})
+ }
}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 610ba1ab..85da5223 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -439,7 +439,6 @@ export const parseNotification = (data) => {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
- output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
: parseUser(data.target)
diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js
index 7e19629d..df603bb4 100644
--- a/src/services/favicon_service/favicon_service.js
+++ b/src/services/favicon_service/favicon_service.js
@@ -55,10 +55,13 @@ const createFaviconService = () => {
})
}
+ const getOriginalFavicons = () => [...favicons]
+
return {
initFaviconService,
clearFaviconBadge,
- drawFaviconBadge
+ drawFaviconBadge,
+ getOriginalFavicons
}
}
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 815e792d..7b705e65 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,28 +1,36 @@
-import { filter, sortBy, includes } from 'lodash'
import { muteWordHits } from '../status_parser/status_parser.js'
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
-export const notificationsFromStore = store => store.state.statuses.notifications.data
+import FaviconService from 'src/services/favicon_service/favicon_service.js'
+
+export const ACTIONABLE_NOTIFICATION_TYPES = new Set(['mention', 'pleroma:report', 'follow_request'])
+
+let cachedBadgeUrl = null
+
+export const notificationsFromStore = store => store.state.notifications.data
export const visibleTypes = store => {
- const rootState = store.rootState || store.state
+ // When called from within a module we need rootGetters to access wider scope
+ // however when called from a component (i.e. this.$store) we already have wider scope
+ const rootGetters = store.rootGetters || store.getters
+ const { notificationVisibility } = rootGetters.mergedConfig
return ([
- rootState.config.notificationVisibility.likes && 'like',
- rootState.config.notificationVisibility.mentions && 'mention',
- rootState.config.notificationVisibility.repeats && 'repeat',
- rootState.config.notificationVisibility.follows && 'follow',
- rootState.config.notificationVisibility.followRequest && 'follow_request',
- rootState.config.notificationVisibility.moves && 'move',
- rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
- rootState.config.notificationVisibility.reports && 'pleroma:report',
- rootState.config.notificationVisibility.polls && 'poll'
+ notificationVisibility.likes && 'like',
+ notificationVisibility.mentions && 'mention',
+ notificationVisibility.repeats && 'repeat',
+ notificationVisibility.follows && 'follow',
+ notificationVisibility.followRequest && 'follow_request',
+ notificationVisibility.moves && 'move',
+ notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
+ notificationVisibility.reports && 'pleroma:report',
+ notificationVisibility.polls && 'poll'
].filter(_ => _))
}
-const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll']
+const statusNotifications = new Set(['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll'])
-export const isStatusNotification = (type) => includes(statusNotifications, type)
+export const isStatusNotification = (type) => statusNotifications.has(type)
export const isValidNotification = (notification) => {
if (isStatusNotification(notification.type) && !notification.status) {
@@ -49,35 +57,57 @@ const sortById = (a, b) => {
const isMutedNotification = (store, notification) => {
if (!notification.status) return
- return notification.status.muted || muteWordHits(notification.status, store.rootGetters.mergedConfig.muteWords).length > 0
+ const rootGetters = store.rootGetters || store.getters
+ return notification.status.muted || muteWordHits(notification.status, rootGetters.mergedConfig.muteWords).length > 0
}
export const maybeShowNotification = (store, notification) => {
const rootState = store.rootState || store.state
+ const rootGetters = store.rootGetters || store.getters
if (notification.seen) return
if (!visibleTypes(store).includes(notification.type)) return
if (notification.type === 'mention' && isMutedNotification(store, notification)) return
- const notificationObject = prepareNotificationObject(notification, store.rootGetters.i18n)
+ const notificationObject = prepareNotificationObject(notification, rootGetters.i18n)
showDesktopNotification(rootState, notificationObject)
}
export const filteredNotificationsFromStore = (store, types) => {
// map is just to clone the array since sort mutates it and it causes some issues
- let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
- sortedNotifications = sortBy(sortedNotifications, 'seen')
+ const sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
+ // TODO implement sorting elsewhere and make it optional
return sortedNotifications.filter(
(notification) => (types || visibleTypes(store)).includes(notification.type)
)
}
-export const unseenNotificationsFromStore = store =>
- filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
+export const unseenNotificationsFromStore = store => {
+ const rootGetters = store.rootGetters || store.getters
+ const ignoreInactionableSeen = rootGetters.mergedConfig.ignoreInactionableSeen
+
+ return filteredNotificationsFromStore(store).filter(({ seen, type }) => {
+ if (!ignoreInactionableSeen) return !seen
+ if (seen) return false
+ return ACTIONABLE_NOTIFICATION_TYPES.has(type)
+ })
+}
export const prepareNotificationObject = (notification, i18n) => {
+ if (cachedBadgeUrl === null) {
+ const favicons = FaviconService.getOriginalFavicons()
+ const favicon = favicons[favicons.length - 1]
+ if (!favicon) {
+ cachedBadgeUrl = 'about:blank'
+ } else {
+ cachedBadgeUrl = favicon.favimg.src
+ }
+ }
+
const notifObj = {
- tag: notification.id
+ tag: notification.id,
+ type: notification.type,
+ badge: cachedBadgeUrl
}
const status = notification.status
const title = notification.from_profile.name
@@ -126,15 +156,16 @@ export const prepareNotificationObject = (notification, i18n) => {
}
export const countExtraNotifications = (store) => {
- const mergedConfig = store.getters.mergedConfig
+ const rootGetters = store.rootGetters || store.getters
+ const mergedConfig = rootGetters.mergedConfig
if (!mergedConfig.showExtraNotifications) {
return 0
}
return [
- mergedConfig.showChatsInExtraNotifications ? store.getters.unreadChatCount : 0,
- mergedConfig.showAnnouncementsInExtraNotifications ? store.getters.unreadAnnouncementCount : 0,
- mergedConfig.showFollowRequestsInExtraNotifications ? store.getters.followRequestCount : 0
+ mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0,
+ mergedConfig.showAnnouncementsInExtraNotifications ? rootGetters.unreadAnnouncementCount : 0,
+ mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0
].reduce((a, c) => a + c, 0)
}
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 6c247210..c91a86c8 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -21,7 +21,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const args = { credentials }
const { getters } = store
const rootState = store.rootState || store.state
- const timelineData = rootState.statuses.notifications
+ const timelineData = rootState.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
args.includeTypes = mastoApiNotificationTypes
@@ -49,10 +49,14 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
// The normal maxId-check does not tell if older notifications have changed
const notifications = timelineData.data
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
- const numUnseenNotifs = notifications.length - readNotifsIds.length
- if (numUnseenNotifs > 0 && readNotifsIds.length > 0) {
- args.since = Math.max(...readNotifsIds)
- fetchNotifications({ store, args, older })
+ const unreadNotifsIds = notifications.filter(n => !n.seen).map(n => n.id)
+ if (readNotifsIds.length > 0 && readNotifsIds.length > 0) {
+ const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification
+ if (minId !== Infinity) {
+ args.since = false // Don't use since_id since it sorta conflicts with min_id
+ args.minId = minId - 1 // go beyond
+ fetchNotifications({ store, args, older })
+ }
}
return result
diff --git a/src/services/push/push.js b/src/services/sw/sw.js
index 1787ac36..554cc7b8 100644
--- a/src/services/push/push.js
+++ b/src/services/sw/sw.js
@@ -10,8 +10,12 @@ function urlBase64ToUint8Array (base64String) {
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
}
+export function isSWSupported () {
+ return 'serviceWorker' in navigator
+}
+
function isPushSupported () {
- return 'serviceWorker' in navigator && 'PushManager' in window
+ return 'PushManager' in window
}
function getOrCreateServiceWorker () {
@@ -24,7 +28,7 @@ function subscribePush (registration, isEnabled, vapidPublicKey) {
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
const subscribeOptions = {
- userVisibleOnly: true,
+ userVisibleOnly: false,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
}
return registration.pushManager.subscribe(subscribeOptions)
@@ -39,7 +43,7 @@ function unsubscribePush (registration) {
}
function deleteSubscriptionFromBackEnd (token) {
- return window.fetch('/api/v1/push/subscription/', {
+ return fetch('/api/v1/push/subscription/', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@@ -78,6 +82,44 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
return responseData
})
}
+export async function initServiceWorker (store) {
+ if (!isSWSupported()) return
+ await getOrCreateServiceWorker()
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ const { dispatch } = store
+ const { type, ...rest } = event.data
+
+ switch (type) {
+ case 'notificationClicked':
+ dispatch('notificationClicked', { id: rest.id })
+ }
+ })
+}
+
+export async function showDesktopNotification (content) {
+ if (!isSWSupported) return
+ const { active: sw } = await window.navigator.serviceWorker.getRegistration()
+ if (!sw) return console.error('No serviceworker found!')
+ sw.postMessage({ type: 'desktopNotification', content })
+}
+
+export async function closeDesktopNotification ({ id }) {
+ if (!isSWSupported) return
+ const { active: sw } = await window.navigator.serviceWorker.getRegistration()
+ if (!sw) return console.error('No serviceworker found!')
+ if (id >= 0) {
+ sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
+ } else {
+ sw.postMessage({ type: 'desktopNotificationClose', content: { all: true } })
+ }
+}
+
+export async function updateFocus () {
+ if (!isSWSupported) return
+ const { active: sw } = await window.navigator.serviceWorker.getRegistration()
+ if (!sw) return console.error('No serviceworker found!')
+ sw.postMessage({ type: 'updateFocus' })
+}
export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) {
if (isPushSupported()) {
@@ -98,13 +140,8 @@ export function unregisterPushNotifications (token) {
})
.then(([registration, unsubResult]) => {
if (!unsubResult) {
- console.warn('Push subscription cancellation wasn\'t successful, killing SW anyway...')
+ console.warn('Push subscription cancellation wasn\'t successful')
}
- return registration.unregister().then((result) => {
- if (!result) {
- console.warn('Failed to kill SW')
- }
- })
})
]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
}