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

View file

@ -66,6 +66,17 @@
/>
</FALayers>
</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
class="button-unstyled mobile-nav-button"
:title="$t('nav.mobile_notifications_close')"

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,30 @@
<template>
<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">
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<ul class="setting-list">
@ -11,43 +36,143 @@
{{ $t('settings.notification_setting_block_from_strangers') }}
</BooleanSetting>
</li>
<li class="select-multiple">
<span class="label">{{ $t('settings.notification_visibility') }}</span>
<ul class="option-list">
<li>
<h3> {{ $t('settings.notification_visibility') }}</h3>
<ul class="setting-list two-column">
<li>
<BooleanSetting path="notificationVisibility.likes">
{{ $t('settings.notification_visibility_likes') }}
</BooleanSetting>
<h4> {{ $t('settings.notification_visibility_mentions') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.mentions" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<BooleanSetting path="notificationVisibility.repeats">
{{ $t('settings.notification_visibility_repeats') }}
</BooleanSetting>
<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>
<BooleanSetting path="notificationVisibility.follows">
{{ $t('settings.notification_visibility_follows') }}
</BooleanSetting>
<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>
<BooleanSetting path="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_mentions') }}
</BooleanSetting>
<h4> {{ $t('settings.notification_visibility_emoji_reactions') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting path="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.emojiReactions" >
{{ $t('settings.notification_visibility_native_notifications') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<BooleanSetting path="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</BooleanSetting>
<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>
<BooleanSetting path="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_emoji_reactions') }}
</BooleanSetting>
<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>
<BooleanSetting path="notificationVisibility.polls">
{{ $t('settings.notification_visibility_polls') }}
</BooleanSetting>
<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>

View file

@ -561,10 +561,14 @@
"posts": "Posts",
"user_profiles": "User Profiles",
"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_follow_requests": "Follow requests",
"notification_visibility_likes": "Favorites",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"notification_visibility_reports": "Reports",
"notification_visibility_moves": "User Migrates",
"notification_visibility_emoji_reactions": "Reactions",
"notification_visibility_polls": "Ends of polls you voted in",
@ -688,6 +692,11 @@
"greentext": "Meme arrows",
"show_yous": "Show (You)s",
"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_block_from_strangers": "Block notifications from users who you do not follow",
"notification_setting_privacy": "Privacy",

View file

@ -66,8 +66,17 @@ export const defaultState = {
chatMention: true,
polls: true
},
notificationSettings: {
nativeNotifications: ['follows', 'mentions', 'followRequest', 'reports', 'chatMention', 'polls']
notificationNative: {
follows: true,
mentions: true,
likes: false,
repeats: false,
moves: false,
emojiReactions: false,
followRequest: true,
reports: true,
chatMention: true,
polls: true
},
webPushNotifications: false,
muteWords: [],
@ -127,7 +136,10 @@ export const defaultState = {
showAnnouncementsInExtraNotifications: undefined, // instance default
showFollowRequestsInExtraNotifications: 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

View file

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

View file

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

View file

@ -15,7 +15,8 @@ const i18n = createI18n({
const state = {
lastFocused: null,
notificationIds: new Set()
notificationIds: new Set(),
allowedNotificationTypes: null
}
function getWindowClients () {
@ -23,15 +24,43 @@ function getWindowClients () {
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
}
const setLocale = async () => {
const state = await localForage.getItem('vuex-lz')
const locale = state.config.interfaceLanguage || 'en'
const setSettings = async () => {
const vuexState = await localForage.getItem('vuex-lz')
const locale = vuexState.config.interfaceLanguage || 'en'
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 activeClients = await getWindowClients()
await setLocale()
await setSettings()
// Only show push notifications if all tabs/windows are closed
if (activeClients.length === 0) {
const data = event.data.json()
@ -43,27 +72,31 @@ const showPushNotification = async (event) => {
const res = prepareNotificationObject(parsedNotification, i18n)
self.registration.showNotification(res.title, res)
if (state.allowedNotificationTypes.has(parsedNotification.type)) {
self.registration.showNotification(res.title, res)
}
}
}
self.addEventListener('push', async (event) => {
console.log(event)
if (event.data) {
event.waitUntil(showPushNotification(event))
}
})
self.addEventListener('message', async (event) => {
await setSettings()
const { type, content } = event.data
if (type === 'desktopNotification') {
const { title, ...rest } = content
const { tag } = rest
const { tag, type } = rest
if (state.notificationIds.has(tag)) return
state.notificationIds.add(tag)
setTimeout(() => state.notificationIds.delete(tag), 10000)
self.registration.showNotification(title, rest)
if (state.allowedNotificationTypes.has(type)) {
self.registration.showNotification(title, rest)
}
}
if (type === 'desktopNotificationClose') {