Merge branch 'notifications-thru-sw' into shigusegubu-vue3

This commit is contained in:
Henry Jameson 2023-11-19 17:03:11 +02:00
commit 1c2f470e73
23 changed files with 288 additions and 44 deletions

View file

@ -0,0 +1 @@
Fix native notifications appearing as many times as there are open tabs. Clicking on notification will focus last focused tab.

View file

@ -0,0 +1 @@
Focusing into a tab clears all current desktop notifications

View file

@ -0,0 +1 @@
Fixed error that appeared on mobile Chrome(ium) (and derivatives) when native notifications are allowed

View file

@ -0,0 +1 @@
Added option to not mark all notifications when closing notifications drawer on mobile, this creates a new button to mark all as seen.

View file

@ -0,0 +1 @@
Fixed being unable to set notification visibility for reports and follow requests

View file

@ -0,0 +1 @@
Added option to toggle what notification types appear in native notifications, by default less important ones (likes, repeats, etc) will no longer show up in native notifications.

View file

@ -0,0 +1 @@
Native notifications now also have "badge" property that matches instance's favicon (visible in Android Chromium at least)

View file

@ -0,0 +1 @@
Added option to treat non-interactive notifications (likes, repeats et all) as seen for visual purposes (no read mark, ignored in counters, still can show in native notifications)

View file

@ -0,0 +1 @@
Interacting (opening reply box etc) or simply clicking on non-interactive notifications now marks them as read. Clicking on native notifications for non-interactive ones also marks them as seen.

View file

@ -0,0 +1 @@
Notifications are no longer sorted by "seen" status since interacting with them can change their read status and makes UI jumpy. Old behavior can be restored in settings.

View file

@ -0,0 +1 @@
Notifications are now shown through a serviceworker (since mobile chrome does not allow them otherwise), it's always enabled, even if previously we only enabled it for WebPush notifications only. If you don't like websites "running" while closed, check how to disable them in your browser. Old way to show notifications will be used as a fallback but might not have all the new features.

View file

@ -14,7 +14,8 @@ import {
faBell, faBell,
faBars, faBars,
faArrowUp, faArrowUp,
faMinus faMinus,
faCheckDouble
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -22,7 +23,8 @@ library.add(
faBell, faBell,
faBars, faBars,
faArrowUp, faArrowUp,
faMinus faMinus,
faCheckDouble
) )
const MobileNav = { const MobileNav = {
@ -67,6 +69,9 @@ const MobileNav = {
shouldConfirmLogout () { shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout return this.$store.getters.mergedConfig.modalOnLogout
}, },
closingDrawerMarksAsSeen () {
return this.$store.getters.mergedConfig.closingDrawerMarksAsSeen
},
...mapGetters(['unreadChatCount']) ...mapGetters(['unreadChatCount'])
}, },
methods: { methods: {
@ -81,7 +86,7 @@ const MobileNav = {
// make sure to mark notifs seen only when the notifs were open and not // make sure to mark notifs seen only when the notifs were open and not
// from close-calls. // from close-calls.
this.notificationsOpen = false this.notificationsOpen = false
if (markRead) { if (markRead && this.closingDrawerMarksAsSeen) {
this.markNotificationsAsSeen() this.markNotificationsAsSeen()
} }
} }
@ -117,7 +122,6 @@ const MobileNav = {
this.hideConfirmLogout() this.hideConfirmLogout()
}, },
markNotificationsAsSeen () { markNotificationsAsSeen () {
// this.$refs.notifications.markAsSeen()
this.$store.dispatch('markNotificationsAsSeen') this.$store.dispatch('markNotificationsAsSeen')
}, },
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {

View file

@ -66,6 +66,17 @@
/> />
</FALayers> </FALayers>
</button> </button>
<button
v-if="!closingDrawerMarksAsSeen"
class="button-unstyled mobile-nav-button"
:title="$t('nav.mobile_notifications_close')"
@click.stop.prevent="markNotificationsAsSeen()"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="check-double"
/>
</button>
<button <button
class="button-unstyled mobile-nav-button" class="button-unstyled mobile-nav-button"
:title="$t('nav.mobile_notifications_close')" :title="$t('nav.mobile_notifications_close')"

View file

@ -21,6 +21,7 @@ library.add(
) )
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
const ACTIONABLE_NOTIFICATION_TYPES = new Set(['mention', 'pleroma:report', 'follow_request'])
const Notifications = { const Notifications = {
components: { components: {
@ -71,14 +72,26 @@ const Notifications = {
return unseenNotificationsFromStore(this.$store) return unseenNotificationsFromStore(this.$store)
}, },
filteredNotifications () { filteredNotifications () {
if (this.unseenAtTop) {
return [
...filteredNotificationsFromStore(this.$store).filter(n => this.shouldShowUnseen(n)),
...filteredNotificationsFromStore(this.$store).filter(n => !this.shouldShowUnseen(n))
]
} else {
return filteredNotificationsFromStore(this.$store, this.filterMode) return filteredNotificationsFromStore(this.$store, this.filterMode)
}
}, },
unseenCountBadgeText () { unseenCountBadgeText () {
return `${this.unseenCount ? this.unseenCount : ''}${this.extraNotificationsCount ? '*' : ''}` return `${this.unseenCount ? this.unseenCount : ''}${this.extraNotificationsCount ? '*' : ''}`
}, },
unseenCount () { unseenCount () {
if (this.ignoreInactionableSeen) {
return this.unseenNotifications.filter(n => ACTIONABLE_NOTIFICATION_TYPES.has(n.type)).length
} else {
return this.unseenNotifications.length return this.unseenNotifications.length
}
}, },
ignoreInactionableSeen () { return this.$store.getters.mergedConfig.ignoreInactionableSeen },
extraNotificationsCount () { extraNotificationsCount () {
return countExtraNotifications(this.$store) return countExtraNotifications(this.$store)
}, },
@ -108,6 +121,7 @@ const Notifications = {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
}, },
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
unseenAtTop () { return this.$store.getters.mergedConfig.unseenAtTop },
showExtraNotifications () { showExtraNotifications () {
return !this.noExtra return !this.noExtra
}, },
@ -154,11 +168,16 @@ const Notifications = {
scrollToTop () { scrollToTop () {
const scrollable = this.scrollerRef const scrollable = this.scrollerRef
scrollable.scrollTo({ top: this.$refs.root.offsetTop }) scrollable.scrollTo({ top: this.$refs.root.offsetTop })
// this.$refs.root.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, },
updateScrollPosition () { updateScrollPosition () {
this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop
}, },
shouldShowUnseen (notification) {
if (notification.seen) return false
const actionable = ACTIONABLE_NOTIFICATION_TYPES.has(notification.type)
return this.ignoreInactionableSeen ? actionable : true
},
/* "Interacted" really refers to "actionable" notifications that require user input, /* "Interacted" really refers to "actionable" notifications that require user input,
* everything else (likes/repeats/reacts) cannot be acted and therefore we just clear * everything else (likes/repeats/reacts) cannot be acted and therefore we just clear
* the "seen" status upon any clicks on them * the "seen" status upon any clicks on them

View file

@ -66,7 +66,7 @@
:key="notification.id" :key="notification.id"
role="listitem" role="listitem"
class="notification" class="notification"
:class="{unseen: !minimalMode && !notification.seen}" :class="{unseen: !minimalMode && shouldShowUnseen(notification)}"
@click="e => notificationClicked(notification)" @click="e => notificationClicked(notification)"
> >
<div class="notification-overlay" /> <div class="notification-overlay" />

View file

@ -3,6 +3,10 @@
.settings-modal { .settings-modal {
overflow: hidden; overflow: hidden;
h4 {
margin-bottom: 0.5em;
}
.setting-list, .setting-list,
.option-list { .option-list {
list-style-type: none; list-style-type: none;
@ -15,6 +19,14 @@
.suboptions { .suboptions {
margin-top: 0.3em; margin-top: 0.3em;
} }
&.two-column {
column-count: 2;
> li {
break-inside: avoid;
}
}
} }
.setting-description { .setting-description {

View file

@ -16,6 +16,10 @@ const NotificationsTab = {
user () { user () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
canReceiveReports () {
if (!this.user) { return false }
return this.user.privileges.includes('reports_manage_reports')
},
...SharedComputedObject() ...SharedComputedObject()
}, },
methods: { methods: {

View file

@ -1,5 +1,30 @@
<template> <template>
<div :label="$t('settings.notifications')"> <div :label="$t('settings.notifications')">
<div class="setting-item">
<h2>{{ $t('settings.notification_setting_annoyance') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path="closingDrawerMarksAsSeen">
{{ $t('settings.notification_setting_drawer_marks_as_seen') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="ignoreInactionableSeen">
{{ $t('settings.notification_setting_ignore_inactionable_seen') }}
</BooleanSetting>
<div>
<small>
{{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
</small>
</div>
</li>
<li>
<BooleanSetting path="unseenAtTop">
{{ $t('settings.notification_setting_unseen_at_top') }}
</BooleanSetting>
</li>
</ul>
</div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.notification_setting_filters') }}</h2> <h2>{{ $t('settings.notification_setting_filters') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
@ -11,46 +36,146 @@
{{ $t('settings.notification_setting_block_from_strangers') }} {{ $t('settings.notification_setting_block_from_strangers') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li class="select-multiple">
<span class="label">{{ $t('settings.notification_visibility') }}</span>
<ul class="option-list">
<li> <li>
<BooleanSetting path="notificationVisibility.likes"> <h3> {{ $t('settings.notification_visibility') }}</h3>
{{ $t('settings.notification_visibility_likes') }} <ul class="setting-list two-column">
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="notificationVisibility.repeats"> <h4> {{ $t('settings.notification_visibility_mentions') }}</h4>
{{ $t('settings.notification_visibility_repeats') }} <ul class="setting-list">
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationVisibility.follows">
{{ $t('settings.notification_visibility_follows') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="notificationVisibility.mentions"> <BooleanSetting path="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_mentions') }} {{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting path="notificationVisibility.moves"> <BooleanSetting path="notificationNative.mentions" >
{{ $t('settings.notification_visibility_moves') }} {{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
</ul>
</li>
<li>
<h4> {{ $t('settings.notification_visibility_likes') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.likes">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.likes" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<h4> {{ $t('settings.notification_visibility_repeats') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.repeats">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.repeats" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<h4> {{ $t('settings.notification_visibility_emoji_reactions') }}</h4>
<ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.emojiReactions"> <BooleanSetting path="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_emoji_reactions') }} {{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting path="notificationVisibility.polls"> <BooleanSetting path="notificationNative.emojiReactions" >
{{ $t('settings.notification_visibility_polls') }} {{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
</ul> </ul>
</li> </li>
<li>
<h4> {{ $t('settings.notification_visibility_follows') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.follows">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.follows" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<h4> {{ $t('settings.notification_visibility_follow_requests') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.follow_request">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.follow_request" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<h4> {{ $t('settings.notification_visibility_moves') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.moves">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.moves" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<h4> {{ $t('settings.notification_visibility_polls') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.polls">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.polls" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li v-if="canReceiveReports">
<h4> {{ $t('settings.notification_visibility_reports') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.reports">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.reports" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
</ul>
</li>
<li> <li>
<BooleanSetting path="showExtraNotifications"> <BooleanSetting path="showExtraNotifications">
{{ $t('settings.notification_show_extra') }} {{ $t('settings.notification_show_extra') }}

View file

@ -561,10 +561,14 @@
"posts": "Posts", "posts": "Posts",
"user_profiles": "User Profiles", "user_profiles": "User Profiles",
"notification_visibility": "Types of notifications to show", "notification_visibility": "Types of notifications to show",
"notification_visibility_in_column": "Show in notifications column/drawer",
"notification_visibility_native_notifications": "Show a native notification",
"notification_visibility_follows": "Follows", "notification_visibility_follows": "Follows",
"notification_visibility_follow_requests": "Follow requests",
"notification_visibility_likes": "Favorites", "notification_visibility_likes": "Favorites",
"notification_visibility_mentions": "Mentions", "notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"notification_visibility_reports": "Reports",
"notification_visibility_moves": "User Migrates", "notification_visibility_moves": "User Migrates",
"notification_visibility_emoji_reactions": "Reactions", "notification_visibility_emoji_reactions": "Reactions",
"notification_visibility_polls": "Ends of polls you voted in", "notification_visibility_polls": "Ends of polls you voted in",
@ -688,6 +692,11 @@
"greentext": "Meme arrows", "greentext": "Meme arrows",
"show_yous": "Show (You)s", "show_yous": "Show (You)s",
"notifications": "Notifications", "notifications": "Notifications",
"notification_setting_annoyance": "Annoyance",
"notification_setting_drawer_marks_as_seen": "Closing drawer (mobile) marks all notifications as read",
"notification_setting_ignore_inactionable_seen": "Ignore read state of inactionable notifications (likes, repeats etc)",
"notification_setting_ignore_inactionable_seen_tip": "This will not actually mark those notifications as read, and you'll still get desktop notifications about them if you chose so",
"notification_setting_unseen_at_top": "Show unread notifications above others",
"notification_setting_filters": "Filters", "notification_setting_filters": "Filters",
"notification_setting_block_from_strangers": "Block notifications from users who you do not follow", "notification_setting_block_from_strangers": "Block notifications from users who you do not follow",
"notification_setting_privacy": "Privacy", "notification_setting_privacy": "Privacy",

View file

@ -66,8 +66,17 @@ export const defaultState = {
chatMention: true, chatMention: true,
polls: true polls: true
}, },
notificationSettings: { notificationNative: {
nativeNotifications: ['follows', 'mentions', 'followRequest', 'reports', 'chatMention', 'polls'] follows: true,
mentions: true,
likes: false,
repeats: false,
moves: false,
emojiReactions: false,
followRequest: true,
reports: true,
chatMention: true,
polls: true
}, },
webPushNotifications: false, webPushNotifications: false,
muteWords: [], muteWords: [],
@ -127,7 +136,10 @@ export const defaultState = {
showAnnouncementsInExtraNotifications: undefined, // instance default showAnnouncementsInExtraNotifications: undefined, // instance default
showFollowRequestsInExtraNotifications: undefined, // instance default showFollowRequestsInExtraNotifications: undefined, // instance default
maxDepthInThread: undefined, // instance default maxDepthInThread: undefined, // instance default
autocompleteSelect: undefined // instance default autocompleteSelect: undefined, // instance default
closingDrawerMarksAsSeen: undefined, // instance default
unseenAtTop: undefined, // instance default
ignoreInactionableSeen: undefined // instance default
} }
// caching the instance default properties // caching the instance default properties

View file

@ -110,6 +110,9 @@ const defaultState = {
showFollowRequestsInExtraNotifications: true, showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6, maxDepthInThread: 6,
autocompleteSelect: false, autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
// Nasty stuff // Nasty stuff
customEmoji: [], customEmoji: [],

View file

@ -91,6 +91,7 @@ export const prepareNotificationObject = (notification, i18n) => {
const notifObj = { const notifObj = {
tag: notification.id, tag: notification.id,
type: notification.type,
badge: cachedBadgeUrl badge: cachedBadgeUrl
} }
const status = notification.status const status = notification.status

View file

@ -15,7 +15,8 @@ const i18n = createI18n({
const state = { const state = {
lastFocused: null, lastFocused: null,
notificationIds: new Set() notificationIds: new Set(),
allowedNotificationTypes: null
} }
function getWindowClients () { function getWindowClients () {
@ -23,15 +24,43 @@ function getWindowClients () {
.then((clientList) => clientList.filter(({ type }) => type === 'window')) .then((clientList) => clientList.filter(({ type }) => type === 'window'))
} }
const setLocale = async () => { const setSettings = async () => {
const state = await localForage.getItem('vuex-lz') const vuexState = await localForage.getItem('vuex-lz')
const locale = state.config.interfaceLanguage || 'en' const locale = vuexState.config.interfaceLanguage || 'en'
i18n.locale = locale i18n.locale = locale
const notificationsNativeArray = Object.entries(vuexState.config.notificationNative)
state.allowedNotificationTypes = new Set(
notificationsNativeArray
.filter(([k, v]) => v)
.map(([k]) => {
switch (k) {
case 'mentions':
return 'mention'
case 'likes':
return 'like'
case 'repeats':
return 'repeat'
case 'emojiReactions':
return 'pleroma:emoji_reaction'
case 'reports':
return 'pleroma:report'
case 'followRequest':
return 'follow_request'
case 'follows':
return 'follow'
case 'polls':
return 'poll'
default:
return k
}
})
)
} }
const showPushNotification = async (event) => { const showPushNotification = async (event) => {
const activeClients = await getWindowClients() const activeClients = await getWindowClients()
await setLocale() await setSettings()
// Only show push notifications if all tabs/windows are closed // Only show push notifications if all tabs/windows are closed
if (activeClients.length === 0) { if (activeClients.length === 0) {
const data = event.data.json() const data = event.data.json()
@ -43,28 +72,32 @@ const showPushNotification = async (event) => {
const res = prepareNotificationObject(parsedNotification, i18n) const res = prepareNotificationObject(parsedNotification, i18n)
if (state.allowedNotificationTypes.has(parsedNotification.type)) {
self.registration.showNotification(res.title, res) self.registration.showNotification(res.title, res)
} }
} }
}
self.addEventListener('push', async (event) => { self.addEventListener('push', async (event) => {
console.log(event)
if (event.data) { if (event.data) {
event.waitUntil(showPushNotification(event)) event.waitUntil(showPushNotification(event))
} }
}) })
self.addEventListener('message', async (event) => { self.addEventListener('message', async (event) => {
await setSettings()
const { type, content } = event.data const { type, content } = event.data
if (type === 'desktopNotification') { if (type === 'desktopNotification') {
const { title, ...rest } = content const { title, ...rest } = content
const { tag } = rest const { tag, type } = rest
if (state.notificationIds.has(tag)) return if (state.notificationIds.has(tag)) return
state.notificationIds.add(tag) state.notificationIds.add(tag)
setTimeout(() => state.notificationIds.delete(tag), 10000) setTimeout(() => state.notificationIds.delete(tag), 10000)
if (state.allowedNotificationTypes.has(type)) {
self.registration.showNotification(title, rest) self.registration.showNotification(title, rest)
} }
}
if (type === 'desktopNotificationClose') { if (type === 'desktopNotificationClose') {
const { id, all } = content const { id, all } = content