diff options
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/api/api.service.js | 4 | ||||
| -rw-r--r-- | src/services/desktop_notification_utils/desktop_notification_utils.js | 39 | ||||
| -rw-r--r-- | src/services/entity_normalizer/entity_normalizer.service.js | 1 | ||||
| -rw-r--r-- | src/services/favicon_service/favicon_service.js | 5 | ||||
| -rw-r--r-- | src/services/notification_utils/notification_utils.js | 81 | ||||
| -rw-r--r-- | src/services/notifications_fetcher/notifications_fetcher.service.js | 14 | ||||
| -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}`)) } |
