Compare commits

...

55 Commits

Author SHA1 Message Date
032e68689d Updated default values 2023-12-21 01:46:37 -08:00
HJ
6391a6a4ea Merge branch 'video-poster' into 'develop'
Fix video posters for Safari

See merge request pleroma/pleroma-fe!1879
2023-12-17 15:09:08 +00:00
Mark Felder
5bd8a78a7a Fix video posters on Safari 2023-12-17 00:51:56 -05:00
HJ
2b41c1cfe8 Merge branch 'notifications-thru-sw' into 'develop'
Notifications improvements.

See merge request pleroma/pleroma-fe!1873
2023-12-13 22:20:59 +00:00
Henry Jameson
55ed65331f changelog 2023-12-14 00:16:25 +02:00
Henry Jameson
ff10834f1a cleanup stray console logs 2023-12-14 00:14:06 +02:00
Henry Jameson
5ad8f2cd5c clarify some bit 2023-12-13 23:55:18 +02:00
Henry Jameson
5ee8fc0aea add setting to always show push notifications 2023-12-13 23:54:12 +02:00
Henry Jameson
99d04bed2b attempt at fixing the extra notification again 2023-12-13 22:04:00 +02:00
Henry Jameson
b394392d0d fix incorrect path for follow request notifications settings 2023-12-13 21:07:23 +02:00
Henry Jameson
febc2aa569 lint 2023-12-13 20:56:27 +02:00
Henry Jameson
0fd1b26fb6 don't include extra notifications 2023-12-13 20:38:39 +02:00
Henry Jameson
4e8bb80dbd fix incorrect title + add counter 2023-12-13 20:37:40 +02:00
Henry Jameson
bcdf336242 try to fix the "website updated in background" notification 2023-12-13 18:36:16 +02:00
HJ
a98e241a81 Merge branch 'allow-apng' into 'develop'
file_type.service: allow apng images to be rendered

See merge request pleroma/pleroma-fe!1877
2023-12-11 12:44:28 +00:00
Yonle
5eed197c3e changelog.d: add add-apng.add for commit fb4d43ce
Signed-off-by: Yonle <yonle@lecturify.net>
2023-12-11 19:08:49 +07:00
HJ
4a7b49181a Merge branch 'renovate/vuelidate-core-2.x' into 'develop'
Update dependency @vuelidate/core to v2.0.3

See merge request pleroma/pleroma-fe!1857
2023-12-11 11:50:40 +00:00
Yonle
fb4d43ced2 file_type.service: allow apng images to be rendered
Signed-off-by: Yonle <yonle@lecturify.net>
2023-12-04 21:52:28 +07:00
Henry Jameson
51f1f05b2d use last favicon instead of first for consistency with browsers 2023-11-22 22:06:56 +02:00
Henry Jameson
c25170d7d9 fix tests and make utils consistent in where they pull configuration from 2023-11-22 21:56:48 +02:00
Henry Jameson
a5f09b7263 don't communicate with serviceworker if there's no support for it 2023-11-22 21:38:54 +02:00
Henry Jameson
33564d8ccc handle no sw registration gracefully 2023-11-22 13:17:55 +02:00
Henry Jameson
92685e37b6 fix infinity case 2023-11-21 15:29:49 +02:00
Henry Jameson
e36548579f fix notifications not catching up with "read" status as intended 2023-11-21 15:26:31 +02:00
Henry Jameson
37e3a23f2a fix badge 2023-11-20 00:20:05 +02:00
Henry Jameson
1931e7c3ba temp console log for mobile debug 2023-11-20 00:17:09 +02:00
Henry Jameson
38b6f0a013 fix notification dot in favicon and mobile nav, minor refactor 2023-11-20 00:14:56 +02:00
Henry Jameson
cdc0959135 fix tests 2023-11-19 17:27:51 +02:00
Henry Jameson
c0e2ba37c8 changelog, small fix 2023-11-19 17:02:46 +02:00
Henry Jameson
072a06fc89 reports visibility setting + actual filtering for desktop notifs 2023-11-19 16:40:30 +02:00
Henry Jameson
d178a56924 improve visual for the description bit 2023-11-19 16:14:24 +02:00
Henry Jameson
fd3ad106be rearrange notification visibility page a bit. 2023-11-19 16:12:43 +02:00
Henry Jameson
e3ee3eacca added some settings for notifications 2023-11-19 15:24:34 +02:00
Henry Jameson
2f90c629b8 fix messages from sw not really being acted upon on mainland 2023-11-19 13:57:47 +02:00
Henry Jameson
a564fc1a1f consistentcy and bugfix 2023-11-19 13:54:26 +02:00
Henry Jameson
c216340001 use dispatch instead of commmit, fix bad copypasta 2023-11-16 22:08:51 +02:00
Henry Jameson
af27e2ca7b fix typo 2023-11-16 21:55:01 +02:00
Henry Jameson
388a7ac175 remove a test 2023-11-16 21:28:02 +02:00
Henry Jameson
f3a859ff9e remove deletion (sic) stuff since we do the .deleted stuff nowadays 2023-11-16 21:24:33 +02:00
Henry Jameson
ce17ebd3d0 use URL for original favicon instead of canvas 2023-11-16 20:45:07 +02:00
Henry Jameson
a17defc5ab handle desktop notifications clicks 2023-11-16 20:41:41 +02:00
Henry Jameson
6ed2cb8f43 continue refactor 2023-11-16 20:09:16 +02:00
Henry Jameson
aad3225d25 refactored notifications into their own module separate from statuses (WIP) 2023-11-16 19:26:18 +02:00
Henry Jameson
fffa7a4f4a fix sw thing 2023-11-13 17:40:55 +02:00
Henry Jameson
e508ca6a1f fix 2023-11-13 17:32:14 +02:00
Henry Jameson
ec2937ec1f add options for marking single notification as read 2023-11-13 17:29:25 +02:00
Henry Jameson
c059f4a7ee Merge remote-tracking branch 'origin/develop' into notifications-thru-sw 2023-11-13 17:26:53 +02:00
Henry Jameson
e0b8ad9f14 add initial structure for notification settings 2023-11-09 01:58:33 +02:00
Henry Jameson
77e270ef58 Don't use notification-badge'd favicon for badges in notifications 2023-11-09 01:53:48 +02:00
Henry Jameson
f449bfe2f1 SW-to-window communication 2023-11-09 01:52:39 +02:00
Henry Jameson
e3bf9a5185 wrong key 2023-10-26 15:47:58 +03:00
Henry Jameson
0628aac664 fallback to old notification method, don't spam if old way of creating
notification fails, try to use favicon
2023-10-26 15:42:21 +03:00
Henry Jameson
1b7e930b2e oops 2023-10-25 19:35:44 +03:00
Henry Jameson
73fbe89a4b initial work on showing notifications through serviceworkers 2023-10-25 18:58:33 +03:00
Pleroma Renovate Bot
4beb29918a Update dependency @vuelidate/core to v2.0.3 2023-10-06 09:10:07 +00:00
54 changed files with 782 additions and 394 deletions

1
changelog.d/add-apng.add Normal file
View File

@ -0,0 +1 @@
Make Pleroma FE to also view apng (Animated PNG) attachment.

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

@ -0,0 +1 @@
unread notifications should now properly catch up (eventually) in polling mode

View File

@ -0,0 +1 @@
Video posters on Safari

View File

@ -0,0 +1 @@
Added option to always "show" notifications when using web push for better compatibility with some browsers (chrome, edge, safari)

View File

@ -25,7 +25,7 @@
"@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0", "@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12", "@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
"@vuelidate/core": "2.0.2", "@vuelidate/core": "2.0.3",
"@vuelidate/validators": "2.0.0", "@vuelidate/validators": "2.0.0",
"body-scroll-lock": "3.1.5", "body-scroll-lock": "3.1.5",
"chromatism": "3.0.0", "chromatism": "3.0.0",

View File

@ -16,6 +16,7 @@ import backendInteractorService from '../services/backend_interactor_service/bac
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js' import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js' import FaviconService from '../services/favicon_service/favicon_service.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
let staticInitialResults = null let staticInitialResults = null
@ -344,6 +345,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('setLayoutHeight', windowHeight()) store.dispatch('setLayoutHeight', windowHeight())
FaviconService.initFaviconService() FaviconService.initFaviconService()
initServiceWorker(store)
window.addEventListener('focus', () => updateFocus())
const overrides = window.___pleromafe_dev_overrides || {} const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin

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 = {
@ -55,6 +57,12 @@ const MobileNav = {
unseenNotificationsCount () { unseenNotificationsCount () {
return this.unseenNotifications.length + countExtraNotifications(this.$store) return this.unseenNotifications.length + countExtraNotifications(this.$store)
}, },
unseenCount () {
return this.unseenNotifications.length
},
unseenCountBadgeText () {
return `${this.unseenCount ? this.unseenCount : ''}`
},
hideSitename () { return this.$store.state.instance.hideSitename }, hideSitename () { return this.$store.state.instance.hideSitename },
sitename () { return this.$store.state.instance.name }, sitename () { return this.$store.state.instance.name },
isChat () { isChat () {
@ -67,6 +75,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 +92,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 +128,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

@ -50,7 +50,13 @@
@touchmove.stop="notificationsTouchMove" @touchmove.stop="notificationsTouchMove"
> >
<div class="mobile-notifications-header"> <div class="mobile-notifications-header">
<span class="title">{{ $t('notifications.notifications') }}</span> <span class="title">
{{ $t('notifications.notifications') }}
<span
v-if="unseenCountBadgeText"
class="badge badge-notification unseen-count"
>{{ unseenCountBadgeText }}</span>
</span>
<span class="spacer" /> <span class="spacer" />
<button <button
v-if="notificationsAtTop" v-if="notificationsAtTop"
@ -66,6 +72,17 @@
/> />
</FALayers> </FALayers>
</button> </button>
<button
v-if="!closingDrawerMarksAsSeen"
class="button-unstyled mobile-nav-button"
:title="$t('nav.mobile_notifications_mark_as_seen')"
@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

@ -50,6 +50,7 @@ const Notification = {
} }
}, },
props: ['notification'], props: ['notification'],
emits: ['interacted'],
components: { components: {
StatusContent, StatusContent,
UserAvatar, UserAvatar,
@ -72,6 +73,9 @@ const Notification = {
getUser (notification) { getUser (notification) {
return this.$store.state.users.usersObject[notification.from_profile.id] return this.$store.state.users.usersObject[notification.from_profile.id]
}, },
interacted () {
this.$emit('interacted')
},
toggleMute () { toggleMute () {
this.unmuted = !this.unmuted this.unmuted = !this.unmuted
}, },
@ -95,6 +99,7 @@ const Notification = {
} }
}, },
doApprove () { doApprove () {
this.$emit('interacted')
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
@ -114,6 +119,7 @@ const Notification = {
} }
}, },
doDeny () { doDeny () {
this.$emit('interacted')
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })

View File

@ -6,6 +6,7 @@
class="Notification" class="Notification"
:compact="true" :compact="true"
:statusoid="notification.status" :statusoid="notification.status"
@interacted="interacted"
/> />
</article> </article>
<article v-else> <article v-else>
@ -248,7 +249,7 @@
<StatusContent <StatusContent
:class="{ faint: !statusExpanded }" :class="{ faint: !statusExpanded }"
:compact="!statusExpanded" :compact="!statusExpanded"
:status="notification.action" :status="notification.status"
/> />
</template> </template>
</div> </div>

View File

@ -8,7 +8,8 @@ import {
notificationsFromStore, notificationsFromStore,
filteredNotificationsFromStore, filteredNotificationsFromStore,
unseenNotificationsFromStore, unseenNotificationsFromStore,
countExtraNotifications countExtraNotifications,
ACTIONABLE_NOTIFICATION_TYPES
} from '../../services/notification_utils/notification_utils.js' } from '../../services/notification_utils/notification_utils.js'
import FaviconService from '../../services/favicon_service/favicon_service.js' import FaviconService from '../../services/favicon_service/favicon_service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -65,13 +66,20 @@ const Notifications = {
return notificationsFromStore(this.$store) return notificationsFromStore(this.$store)
}, },
error () { error () {
return this.$store.state.statuses.notifications.error return this.$store.state.notifications.error
}, },
unseenNotifications () { unseenNotifications () {
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 ? '*' : ''}`
@ -79,6 +87,7 @@ const Notifications = {
unseenCount () { unseenCount () {
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)
}, },
@ -86,7 +95,7 @@ const Notifications = {
return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount
}, },
loading () { loading () {
return this.$store.state.statuses.notifications.loading return this.$store.state.notifications.loading
}, },
noHeading () { noHeading () {
const { layoutType } = this.$store.state.interface const { layoutType } = this.$store.state.interface
@ -108,6 +117,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 +164,28 @@ 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,
* everything else (likes/repeats/reacts) cannot be acted and therefore we just clear
* the "seen" status upon any clicks on them
*/
notificationClicked (notification) {
const { id } = notification
this.$store.dispatch('notificationClicked', { id })
},
notificationInteracted (notification) {
const { id } = notification
this.$store.dispatch('markSingleNotificationAsSeen', { id })
},
markAsSeen () { markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen') this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT

View File

@ -66,10 +66,14 @@
: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)"
> >
<div class="notification-overlay" /> <div class="notification-overlay" />
<notification :notification="notification" /> <notification
:notification="notification"
@interacted="e => notificationInteracted(notification)"
/>
</div> </div>
</div> </div>
<div class="panel-footer"> <div class="panel-footer">

View File

@ -52,7 +52,6 @@ const QuickViewSettings = {
get () { return this.mergedConfig.mentionLinkShowAvatar }, get () { return this.mergedConfig.mentionLinkShowAvatar },
set () { set () {
const value = !this.showUserAvatars const value = !this.showUserAvatars
console.log(value)
this.$store.dispatch('setOption', { name: 'mentionLinkShowAvatar', value }) this.$store.dispatch('setOption', { name: 'mentionLinkShowAvatar', value })
} }
}, },

View File

@ -16,7 +16,6 @@ const Report = {
}, },
computed: { computed: {
report () { report () {
console.log(this.$store.state.reports.reports[this.reportId] || {})
return this.$store.state.reports.reports[this.reportId] || {} return this.$store.state.reports.reports[this.reportId] || {}
}, },
state: { state: {

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" expert="1">
{{ $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,147 @@
{{ $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') }} <p v-if="expertLevel > 0">{{ $t('settings.notification_setting_filters_chrome_push') }}</p>
</BooleanSetting> <ul class="setting-list two-column">
</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.followRequest">
{{ $t('settings.notification_visibility_in_column') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationNative.followRequest">
{{ $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') }}
@ -108,6 +234,21 @@
> >
{{ $t('settings.enable_web_push_notifications') }} {{ $t('settings.enable_web_push_notifications') }}
</BooleanSetting> </BooleanSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="webPushAlwaysShowNotifications"
:disabled="!mergedConfig.webPushNotifications"
>
{{ $t('settings.enable_web_push_always_show') }}
</BooleanSetting>
<div :class="{ faint: !mergedConfig.webPushNotifications }">
<small>
{{ $t('settings.enable_web_push_always_show_tip') }}
</small>
</div>
</li>
</ul>
</li> </li>
<li> <li>
<BooleanSetting <BooleanSetting

View File

@ -755,7 +755,6 @@ export default {
selected () { selected () {
this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => { this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => {
if (Array.isArray(s)) { if (Array.isArray(s)) {
console.log(s[0] === this.selected, this.selected)
return s[0] === this.selected return s[0] === this.selected
} else { } else {
return s.name === this.selected return s.name === this.selected

View File

@ -154,6 +154,7 @@ const Status = {
'controlledSetMediaPlaying', 'controlledSetMediaPlaying',
'dive' 'dive'
], ],
emits: ['interacted'],
data () { data () {
return { return {
uncontrolledReplying: false, uncontrolledReplying: false,
@ -442,9 +443,11 @@ const Status = {
this.error = error this.error = error
}, },
clearError () { clearError () {
this.$emit('interacted')
this.error = undefined this.error = undefined
}, },
toggleReplying () { toggleReplying () {
this.$emit('interacted')
controlledOrUncontrolledToggle(this, 'replying') controlledOrUncontrolledToggle(this, 'replying')
}, },
gotoOriginal (id) { gotoOriginal (id) {

View File

@ -531,14 +531,17 @@
:visibility="status.visibility" :visibility="status.visibility"
:logged-in="loggedIn" :logged-in="loggedIn"
:status="status" :status="status"
@click="$emit('interacted')"
/> />
<favorite-button <favorite-button
:logged-in="loggedIn" :logged-in="loggedIn"
:status="status" :status="status"
@click="$emit('interacted')"
/> />
<ReactButton <ReactButton
v-if="loggedIn" v-if="loggedIn"
:status="status" :status="status"
@click="$emit('interacted')"
/> />
<extra-buttons <extra-buttons
:status="status" :status="status"

View File

@ -2,7 +2,7 @@
<video <video
class="video" class="video"
preload="metadata" preload="metadata"
:src="attachment.url" :src="attachment.url + '#t=0.5'"
:loop="loopVideo" :loop="loopVideo"
:controls="controls" :controls="controls"
:alt="attachment.description" :alt="attachment.description"

View File

@ -189,6 +189,7 @@
"mobile_notifications": "Open notifications", "mobile_notifications": "Open notifications",
"mobile_notifications": "Open notifications (there are unread ones)", "mobile_notifications": "Open notifications (there are unread ones)",
"mobile_notifications_close": "Close notifications", "mobile_notifications_close": "Close notifications",
"mobile_notifications_mark_as_seen": "Mark all as seen",
"announcements": "Announcements" "announcements": "Announcements"
}, },
"notifications": { "notifications": {
@ -561,10 +562,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,13 +693,21 @@
"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_filters_chrome_push": "On some browsers (chrome) it might be impossible to completely filter out notifications by type when they arrive by Push",
"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",
"notification_setting_hide_notification_contents": "Hide the sender and contents of push notifications", "notification_setting_hide_notification_contents": "Hide the sender and contents of push notifications",
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.", "notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
"notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.", "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
"enable_web_push_notifications": "Enable web push notifications", "enable_web_push_notifications": "Enable web push notifications",
"enable_web_push_always_show": "Always show web push notifications",
"enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.",
"more_settings": "More settings", "more_settings": "More settings",
"style": { "style": {
"switcher": { "switcher": {

View File

@ -38,7 +38,7 @@ export default function createPersistedState ({
}, },
setState = (key, state, storage) => { setState = (key, state, storage) => {
if (!loaded) { if (!loaded) {
console.log('waiting for old state to be loaded...') console.info('waiting for old state to be loaded...')
return Promise.resolve() return Promise.resolve()
} else { } else {
return storage.setItem(key, state) return storage.setItem(key, state)
@ -65,7 +65,7 @@ export default function createPersistedState ({
} }
loaded = true loaded = true
} catch (e) { } catch (e) {
console.log("Couldn't load state") console.error("Couldn't load state")
console.error(e) console.error(e)
loaded = true loaded = true
} }
@ -86,8 +86,8 @@ export default function createPersistedState ({
}) })
} }
} catch (e) { } catch (e) {
console.log("Couldn't persist state:") console.error("Couldn't persist state:")
console.log(e) console.error(e)
} }
}) })
} }

View File

@ -6,6 +6,7 @@ import './lib/event_target_polyfill.js'
import interfaceModule from './modules/interface.js' import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js' import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js' import statusesModule from './modules/statuses.js'
import notificationsModule from './modules/notifications.js'
import listsModule from './modules/lists.js' import listsModule from './modules/lists.js'
import usersModule from './modules/users.js' import usersModule from './modules/users.js'
import apiModule from './modules/api.js' import apiModule from './modules/api.js'
@ -78,6 +79,7 @@ const persistedStateOptions = {
// TODO refactor users/statuses modules, they depend on each other // TODO refactor users/statuses modules, they depend on each other
users: usersModule, users: usersModule,
statuses: statusesModule, statuses: statusesModule,
notifications: notificationsModule,
lists: listsModule, lists: listsModule,
api: apiModule, api: apiModule,
config: configModule, config: configModule,

View File

@ -105,7 +105,6 @@ const adminSettingsStorage = {
} }
set(config, path, convert(c.value)) set(config, path, convert(c.value))
}) })
console.log(config[':pleroma'])
commit('updateAdminSettings', { config, modifiedPaths }) commit('updateAdminSettings', { config, modifiedPaths })
commit('resetAdminDraft') commit('resetAdminDraft')
}, },
@ -123,7 +122,6 @@ const adminSettingsStorage = {
const descriptions = {} const descriptions = {}
backendDescriptions.forEach(d => convert(d, '', descriptions)) backendDescriptions.forEach(d => convert(d, '', descriptions))
console.log(descriptions[':pleroma']['Pleroma.Captcha'])
commit('updateAdminDescriptions', { descriptions }) commit('updateAdminDescriptions', { descriptions })
}, },

View File

@ -51,7 +51,7 @@ export const defaultState = {
alwaysShowNewPostButton: false, alwaysShowNewPostButton: false,
autohideFloatingPostButton: false, autohideFloatingPostButton: false,
pauseOnUnfocused: true, pauseOnUnfocused: true,
stopGifs: true, stopGifs: false,
replyVisibility: 'all', replyVisibility: 'all',
thirdColumnMode: 'notifications', thirdColumnMode: 'notifications',
notificationVisibility: { notificationVisibility: {
@ -66,7 +66,20 @@ export const defaultState = {
chatMention: true, chatMention: true,
polls: true polls: true
}, },
notificationNative: {
follows: true,
mentions: true,
likes: false,
repeats: false,
moves: false,
emojiReactions: false,
followRequest: true,
reports: true,
chatMention: true,
polls: true
},
webPushNotifications: false, webPushNotifications: false,
webPushAlwaysShowNotifications: false,
muteWords: [], muteWords: [],
highlight: {}, highlight: {},
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,
@ -91,7 +104,7 @@ export const defaultState = {
modalOnRemoveUserFromFollowers: undefined, // instance default modalOnRemoveUserFromFollowers: undefined, // instance default
playVideosInModal: false, playVideosInModal: false,
useOneClickNsfw: false, useOneClickNsfw: false,
useContainFit: true, useContainFit: false,
disableStickyHeaders: false, disableStickyHeaders: false,
showScrollbars: false, showScrollbars: false,
userPopoverAvatarAction: 'open', userPopoverAvatarAction: 'open',
@ -124,7 +137,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

@ -0,0 +1,169 @@
import apiService from '../services/api/api.service.js'
import {
isStatusNotification,
isValidNotification,
maybeShowNotification
} from '../services/notification_utils/notification_utils.js'
import {
closeDesktopNotification,
closeAllDesktopNotifications
} from '../services/desktop_notification_utils/desktop_notification_utils.js'
const emptyNotifications = () => ({
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false
})
export const defaultState = () => ({
...emptyNotifications()
})
export const notifications = {
state: defaultState(),
mutations: {
addNewNotifications (state, { notifications }) {
notifications.forEach(notification => {
state.data.push(notification)
state.idStore[notification.id] = notification
})
},
clearNotifications (state) {
state = emptyNotifications()
},
updateNotificationsMinMaxId (state, id) {
state.maxId = id > state.maxId ? id : state.maxId
state.minId = id < state.minId ? id : state.minId
},
setNotificationsLoading (state, { value }) {
state.loading = value
},
setNotificationsSilence (state, { value }) {
state.desktopNotificationSilence = value
},
markNotificationsAsSeen (state) {
state.data.forEach((notification) => {
notification.seen = true
})
},
markSingleNotificationAsSeen (state, { id }) {
const notification = state.idStore[id]
if (notification) notification.seen = true
},
dismissNotification (state, { id }) {
state.data = state.data.filter(n => n.id !== id)
delete state.idStore[id]
},
updateNotification (state, { id, updater }) {
const notification = state.idStore[id]
notification && updater(notification)
}
},
actions: {
addNewNotifications (store, { notifications, older }) {
const { commit, dispatch, state, rootState } = store
const validNotifications = notifications.filter((notification) => {
// If invalid notification, update ids but don't add it to store
if (!isValidNotification(notification)) {
console.error('Invalid notification:', notification)
commit('updateNotificationsMinMaxId', notification.id)
return false
}
return true
})
const statusNotifications = validNotifications.filter(notification => isStatusNotification(notification.type) && notification.status)
// Synchronous commit to add all the statuses
commit('addNewStatuses', { statuses: statusNotifications.map(notification => notification.status) })
// Update references to statuses in notifications to ones in the store
statusNotifications.forEach(notification => {
const id = notification.status.id
const referenceStatus = rootState.statuses.allStatusesObject[id]
if (referenceStatus) {
notification.status = referenceStatus
}
})
validNotifications.forEach(notification => {
if (notification.type === 'pleroma:report') {
dispatch('addReport', notification.report)
}
if (notification.type === 'pleroma:emoji_reaction') {
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
// Only add a new notification if we don't have one for the same action
// eslint-disable-next-line no-prototype-builtins
if (!state.idStore.hasOwnProperty(notification.id)) {
commit('updateNotificationsMinMaxId', notification.id)
commit('addNewNotifications', { notifications: [notification] })
maybeShowNotification(store, notification)
} else if (notification.seen) {
state.idStore[notification.id].seen = true
}
})
},
notificationClicked ({ state, dispatch }, { id }) {
const notification = state.idStore[id]
const { type, seen } = notification
if (!seen) {
switch (type) {
case 'mention':
case 'pleroma:report':
case 'follow_request':
break
default:
dispatch('markSingleNotificationAsSeen', { id })
}
}
},
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value })
},
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
markNotificationsAsSeen ({ rootState, state, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
id: state.maxId,
credentials: rootState.users.currentUser.credentials
}).then(() => {
closeAllDesktopNotifications(rootState)
})
},
markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
commit('markSingleNotificationAsSeen', { id })
apiService.markNotificationsAsSeen({
single: true,
id,
credentials: rootState.users.currentUser.credentials
}).then(() => {
closeDesktopNotification(rootState, { id })
})
},
dismissNotificationLocal ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
},
dismissNotification ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
rootState.api.backendInteractor.dismissNotification({ id })
},
updateNotification ({ rootState, commit }, { id, updater }) {
commit('updateNotification', { id, updater })
}
}
}
export default notifications

View File

@ -419,7 +419,6 @@ const serverSideStorage = {
actions: { actions: {
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
const needPush = state.dirty || force const needPush = state.dirty || force
console.log(needPush)
if (!needPush) return if (!needPush) return
commit('updateCache', { username: rootState.users.currentUser.fqn }) commit('updateCache', { username: rootState.users.currentUser.fqn })
const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }

View File

@ -12,11 +12,6 @@ import {
isArray, isArray,
omitBy omitBy
} from 'lodash' } from 'lodash'
import {
isStatusNotification,
isValidNotification,
maybeShowNotification
} from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
const emptyTl = (userId = 0) => ({ const emptyTl = (userId = 0) => ({
@ -36,22 +31,12 @@ const emptyTl = (userId = 0) => ({
flushMarker: 0 flushMarker: 0
}) })
const emptyNotifications = () => ({
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false
})
export const defaultState = () => ({ export const defaultState = () => ({
allStatuses: [], allStatuses: [],
scrobblesNextFetch: {}, scrobblesNextFetch: {},
allStatusesObject: {}, allStatusesObject: {},
conversationsObject: {}, conversationsObject: {},
maxId: 0, maxId: 0,
notifications: emptyNotifications(),
favorites: new Set(), favorites: new Set(),
timelines: { timelines: {
mentions: emptyTl(), mentions: emptyTl(),
@ -154,22 +139,6 @@ const addStatusToGlobalStorage = (state, data) => {
return result return result
} }
// Remove status from the global storages (arrays and objects maintaining statuses) except timelines
const removeStatusFromGlobalStorage = (state, status) => {
remove(state.allStatuses, { id: status.id })
// TODO: Need to remove from allStatusesObject?
// Remove possible notification
remove(state.notifications.data, ({ action: { id } }) => id === status.id)
// Remove from conversation
const conversationId = status.statusnet_conversation_id
if (state.conversationsObject[conversationId]) {
remove(state.conversationsObject[conversationId], { id: status.id })
}
}
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => { const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
// Sanity check // Sanity check
if (!isArray(statuses)) { if (!isArray(statuses)) {
@ -303,20 +272,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
favoriteStatus(favorite) favoriteStatus(favorite)
} }
}, },
deletion: (deletion) => {
const uri = deletion.uri
const status = find(allStatuses, { uri })
if (!status) {
return
}
removeStatusFromGlobalStorage(state, status)
if (timeline) {
remove(timelineObject.statuses, { uri })
remove(timelineObject.visibleStatuses, { uri })
}
},
follow: (follow) => { follow: (follow) => {
// NOOP, it is known status but we don't do anything about it for now // NOOP, it is known status but we don't do anything about it for now
}, },
@ -338,52 +293,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
} }
} }
const updateNotificationsMinMaxId = (state, notification) => {
state.notifications.maxId = notification.id > state.notifications.maxId
? notification.id
: state.notifications.maxId
state.notifications.minId = notification.id < state.notifications.minId
? notification.id
: state.notifications.minId
}
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => {
each(notifications, (notification) => {
// If invalid notification, update ids but don't add it to store
if (!isValidNotification(notification)) {
console.error('Invalid notification:', notification)
updateNotificationsMinMaxId(state, notification)
return
}
if (isStatusNotification(notification.type)) {
notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
if (notification.type === 'pleroma:report') {
dispatch('addReport', notification.report)
}
if (notification.type === 'pleroma:emoji_reaction') {
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
// Only add a new notification if we don't have one for the same action
// eslint-disable-next-line no-prototype-builtins
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
updateNotificationsMinMaxId(state, notification)
state.notifications.data.push(notification)
state.notifications.idStore[notification.id] = notification
newNotificationSideEffects(notification)
} else if (notification.seen) {
state.notifications.idStore[notification.id].seen = true
}
})
}
const removeStatus = (state, { timeline, userId }) => { const removeStatus = (state, { timeline, userId }) => {
const timelineObject = state.timelines[timeline] const timelineObject = state.timelines[timeline]
if (userId) { if (userId) {
@ -396,7 +305,6 @@ const removeStatus = (state, { timeline, userId }) => {
export const mutations = { export const mutations = {
addNewStatuses, addNewStatuses,
addNewNotifications,
removeStatus, removeStatus,
showNewStatuses (state, { timeline }) { showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline]) const oldTimeline = (state.timelines[timeline])
@ -418,9 +326,6 @@ export const mutations = {
const userId = excludeUserId ? state.timelines[timeline].userId : undefined const userId = excludeUserId ? state.timelines[timeline].userId : undefined
state.timelines[timeline] = emptyTl(userId) state.timelines[timeline] = emptyTl(userId)
}, },
clearNotifications (state) {
state.notifications = emptyNotifications()
},
setFavorited (state, { status, value }) { setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
@ -503,31 +408,6 @@ export const mutations = {
const newStatus = state.allStatusesObject[id] const newStatus = state.allStatusesObject[id]
newStatus.nsfw = nsfw newStatus.nsfw = nsfw
}, },
setNotificationsLoading (state, { value }) {
state.notifications.loading = value
},
setNotificationsSilence (state, { value }) {
state.notifications.desktopNotificationSilence = value
},
markNotificationsAsSeen (state) {
each(state.notifications.data, (notification) => {
notification.seen = true
})
},
markSingleNotificationAsSeen (state, { id }) {
const notification = find(state.notifications.data, n => n.id === id)
if (notification) notification.seen = true
},
dismissNotification (state, { id }) {
state.notifications.data = state.notifications.data.filter(n => n.id !== id)
},
dismissNotifications (state, { finder }) {
state.notifications.data = state.notifications.data.filter(n => finder)
},
updateNotification (state, { id, updater }) {
const notification = find(state.notifications.data, n => n.id === id)
notification && updater(notification)
},
queueFlush (state, { timeline, id }) { queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id state.timelines[timeline].flushMarker = id
}, },
@ -609,23 +489,9 @@ export const mutations = {
const statuses = { const statuses = {
state: defaultState(), state: defaultState(),
actions: { actions: {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { addNewStatuses ({ rootState, commit, dispatch, state }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination }) commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
}, },
addNewNotifications (store, { notifications, older }) {
const { commit, dispatch, rootGetters } = store
const newNotificationSideEffects = (notification) => {
maybeShowNotification(store, notification)
}
commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
},
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value })
},
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
fetchStatus ({ rootState, dispatch }, id) { fetchStatus ({ rootState, dispatch }, id) {
return rootState.api.backendInteractor.fetchStatus({ id }) return rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
@ -721,31 +587,6 @@ const statuses = {
queueFlushAll ({ rootState, commit }) { queueFlushAll ({ rootState, commit }) {
commit('queueFlushAll') commit('queueFlushAll')
}, },
markNotificationsAsSeen ({ rootState, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
id: rootState.statuses.notifications.maxId,
credentials: rootState.users.currentUser.credentials
})
},
markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
commit('markSingleNotificationAsSeen', { id })
apiService.markNotificationsAsSeen({
single: true,
id,
credentials: rootState.users.currentUser.credentials
})
},
dismissNotificationLocal ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
},
dismissNotification ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
rootState.api.backendInteractor.dismissNotification({ id })
},
updateNotification ({ rootState, commit }, { id, updater }) {
commit('updateNotification', { id, updater })
},
fetchFavsAndRepeats ({ rootState, commit }, id) { fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([ Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),

View File

@ -2,7 +2,7 @@ import backendInteractorService from '../services/backend_interactor_service/bac
import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import oauthApi from '../services/new_api/oauth.js' import oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'
// TODO: Unify with mergeOrAdd in statuses.js // TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => { export const mergeOrAdd = (arr, obj, item) => {
@ -498,7 +498,7 @@ const users = {
store.commit('addNewUsers', users) store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers) store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.statuses.notifications.idStore const notificationsObject = store.rootState.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject) const relevantNotifications = Object.entries(notificationsObject)
.filter(([k, val]) => notificationIds.includes(k)) .filter(([k, val]) => notificationIds.includes(k))
.map(([k, val]) => val) .map(([k, val]) => val)
@ -667,7 +667,7 @@ const users = {
resolve() resolve()
}) })
.catch((error) => { .catch((error) => {
console.log(error) console.error(error)
commit('endLogin') commit('endLogin')
reject(new Error('Failed to connect to server, try again')) reject(new Error('Failed to connect to server, try again'))
}) })

View File

@ -671,6 +671,7 @@ const fetchTimeline = ({
timeline, timeline,
credentials, credentials,
since = false, since = false,
minId = false,
until = false, until = false,
userId = false, userId = false,
listId = false, listId = false,
@ -705,6 +706,9 @@ const fetchTimeline = ({
url = url(listId) url = url(listId)
} }
if (minId) {
params.push(['min_id', minId])
}
if (since) { if (since) {
params.push(['since_id', since]) params.push(['since_id', since])
} }

View File

@ -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) => { export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
if (!('Notification' in window && window.Notification.permission === 'granted')) return 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) 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) 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
if (isSWSupported()) {
swCloseDesktopNotification({})
}
} }

View File

@ -439,7 +439,6 @@ export const parseNotification = (data) => {
output.type = mastoDict[data.type] || data.type output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen output.seen = data.pleroma.is_seen
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move' output.target = output.type !== 'move'
? null ? null
: parseUser(data.target) : parseUser(data.target)

View File

@ -55,10 +55,13 @@ const createFaviconService = () => {
}) })
} }
const getOriginalFavicons = () => [...favicons]
return { return {
initFaviconService, initFaviconService,
clearFaviconBadge, clearFaviconBadge,
drawFaviconBadge drawFaviconBadge,
getOriginalFavicons
} }
} }

View File

@ -26,7 +26,7 @@ export const fileType = mimetype => {
} }
export const fileTypeExt = url => { export const fileTypeExt = url => {
if (url.match(/\.(png|jpe?g|gif|webp|avif)$/)) { if (url.match(/\.(a?png|jpe?g|gif|webp|avif)$/)) {
return 'image' return 'image'
} }
if (url.match(/\.(ogv|mp4|webm|mov)$/)) { if (url.match(/\.(ogv|mp4|webm|mov)$/)) {

View File

@ -1,28 +1,36 @@
import { filter, sortBy, includes } from 'lodash'
import { muteWordHits } from '../status_parser/status_parser.js' import { muteWordHits } from '../status_parser/status_parser.js'
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.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 => { 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 ([ return ([
rootState.config.notificationVisibility.likes && 'like', notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention', notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat', notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow', notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.followRequest && 'follow_request', notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move', notificationVisibility.moves && 'move',
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction', notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
rootState.config.notificationVisibility.reports && 'pleroma:report', notificationVisibility.reports && 'pleroma:report',
rootState.config.notificationVisibility.polls && 'poll' notificationVisibility.polls && 'poll'
].filter(_ => _)) ].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) => { export const isValidNotification = (notification) => {
if (isStatusNotification(notification.type) && !notification.status) { if (isStatusNotification(notification.type) && !notification.status) {
@ -49,35 +57,57 @@ const sortById = (a, b) => {
const isMutedNotification = (store, notification) => { const isMutedNotification = (store, notification) => {
if (!notification.status) return 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) => { export const maybeShowNotification = (store, notification) => {
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const rootGetters = store.rootGetters || store.getters
if (notification.seen) return if (notification.seen) return
if (!visibleTypes(store).includes(notification.type)) return if (!visibleTypes(store).includes(notification.type)) return
if (notification.type === 'mention' && isMutedNotification(store, notification)) 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) showDesktopNotification(rootState, notificationObject)
} }
export const filteredNotificationsFromStore = (store, types) => { export const filteredNotificationsFromStore = (store, types) => {
// map is just to clone the array since sort mutates it and it causes some issues // map is just to clone the array since sort mutates it and it causes some issues
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById) const sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
sortedNotifications = sortBy(sortedNotifications, 'seen') // TODO implement sorting elsewhere and make it optional
return sortedNotifications.filter( return sortedNotifications.filter(
(notification) => (types || visibleTypes(store)).includes(notification.type) (notification) => (types || visibleTypes(store)).includes(notification.type)
) )
} }
export const unseenNotificationsFromStore = store => export const unseenNotificationsFromStore = store => {
filter(filteredNotificationsFromStore(store), ({ seen }) => !seen) 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) => { 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 = { const notifObj = {
tag: notification.id tag: notification.id,
type: notification.type,
badge: cachedBadgeUrl
} }
const status = notification.status const status = notification.status
const title = notification.from_profile.name const title = notification.from_profile.name
@ -126,15 +156,16 @@ export const prepareNotificationObject = (notification, i18n) => {
} }
export const countExtraNotifications = (store) => { export const countExtraNotifications = (store) => {
const mergedConfig = store.getters.mergedConfig const rootGetters = store.rootGetters || store.getters
const mergedConfig = rootGetters.mergedConfig
if (!mergedConfig.showExtraNotifications) { if (!mergedConfig.showExtraNotifications) {
return 0 return 0
} }
return [ return [
mergedConfig.showChatsInExtraNotifications ? store.getters.unreadChatCount : 0, mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0,
mergedConfig.showAnnouncementsInExtraNotifications ? store.getters.unreadAnnouncementCount : 0, mergedConfig.showAnnouncementsInExtraNotifications ? rootGetters.unreadAnnouncementCount : 0,
mergedConfig.showFollowRequestsInExtraNotifications ? store.getters.followRequestCount : 0 mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0
].reduce((a, c) => a + c, 0) ].reduce((a, c) => a + c, 0)
} }

View File

@ -21,7 +21,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const args = { credentials } const args = { credentials }
const { getters } = store const { getters } = store
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications const timelineData = rootState.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts const hideMutedPosts = getters.mergedConfig.hideMutedPosts
args.includeTypes = mastoApiNotificationTypes args.includeTypes = mastoApiNotificationTypes
@ -49,11 +49,15 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
// The normal maxId-check does not tell if older notifications have changed // The normal maxId-check does not tell if older notifications have changed
const notifications = timelineData.data const notifications = timelineData.data
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id) const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
const numUnseenNotifs = notifications.length - readNotifsIds.length const unreadNotifsIds = notifications.filter(n => !n.seen).map(n => n.id)
if (numUnseenNotifs > 0 && readNotifsIds.length > 0) { if (readNotifsIds.length > 0 && readNotifsIds.length > 0) {
args.since = Math.max(...readNotifsIds) 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 }) fetchNotifications({ store, args, older })
} }
}
return result return result
} }

View File

@ -10,8 +10,12 @@ function urlBase64ToUint8Array (base64String) {
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0))) return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
} }
export function isSWSupported () {
return 'serviceWorker' in navigator
}
function isPushSupported () { function isPushSupported () {
return 'serviceWorker' in navigator && 'PushManager' in window return 'PushManager' in window
} }
function getOrCreateServiceWorker () { function getOrCreateServiceWorker () {
@ -24,7 +28,7 @@ function subscribePush (registration, isEnabled, vapidPublicKey) {
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found')) if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
const subscribeOptions = { const subscribeOptions = {
userVisibleOnly: true, userVisibleOnly: false,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
} }
return registration.pushManager.subscribe(subscribeOptions) return registration.pushManager.subscribe(subscribeOptions)
@ -39,7 +43,7 @@ function unsubscribePush (registration) {
} }
function deleteSubscriptionFromBackEnd (token) { function deleteSubscriptionFromBackEnd (token) {
return window.fetch('/api/v1/push/subscription/', { return fetch('/api/v1/push/subscription/', {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -78,6 +82,44 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
return responseData 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) { export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) {
if (isPushSupported()) { if (isPushSupported()) {
@ -98,13 +140,8 @@ export function unregisterPushNotifications (token) {
}) })
.then(([registration, unsubResult]) => { .then(([registration, unsubResult]) => {
if (!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}`)) ]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
} }

View File

@ -13,9 +13,10 @@ const i18n = createI18n({
messages messages
}) })
function isEnabled () { const state = {
return localForage.getItem('vuex-lz') lastFocused: null,
.then(data => data.config.webPushNotifications) notificationIds: new Set(),
allowedNotificationTypes: null
} }
function getWindowClients () { function getWindowClients () {
@ -23,17 +24,46 @@ 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.webPushAlwaysShowNotifications = vuexState.config.webPushAlwaysShowNotifications
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 maybeShowNotification = async (event) => { const showPushNotification = async (event) => {
const enabled = await isEnabled()
const activeClients = await getWindowClients() const activeClients = await getWindowClients()
await setLocale() await setSettings()
if (enabled && (activeClients.length === 0)) { // Only show push notifications if all tabs/windows are closed
if (state.webPushAlwaysShowNotifications || activeClients.length === 0) {
const data = event.data.json() const data = event.data.json()
const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}` const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}`
@ -43,13 +73,48 @@ const maybeShowNotification = async (event) => {
const res = prepareNotificationObject(parsedNotification, i18n) const res = prepareNotificationObject(parsedNotification, i18n)
self.registration.showNotification(res.title, res) if (state.webPushAlwaysShowNotifications || state.allowedNotificationTypes.has(parsedNotification.type)) {
return self.registration.showNotification(res.title, res)
} }
} }
return Promise.resolve()
}
self.addEventListener('push', async (event) => { self.addEventListener('push', async (event) => {
if (event.data) { if (event.data) {
event.waitUntil(maybeShowNotification(event)) // Supposedly, we HAVE to return a promise inside waitUntil otherwise it will
// show (extra) notification that website is updated in background
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, type } = rest
if (state.notificationIds.has(tag)) return
state.notificationIds.add(tag)
setTimeout(() => state.notificationIds.delete(tag), 10000)
if (state.allowedNotificationTypes.has(type)) {
self.registration.showNotification(title, rest)
}
}
if (type === 'desktopNotificationClose') {
const { id, all } = content
const search = all ? null : { tag: id }
const notifications = await self.registration.getNotifications(search)
notifications.forEach(n => n.close())
}
if (type === 'updateFocus') {
state.lastFocused = event.source.id
const notifications = await self.registration.getNotifications()
notifications.forEach(n => n.close())
} }
}) })
@ -59,7 +124,14 @@ self.addEventListener('notificationclick', (event) => {
event.waitUntil(getWindowClients().then((list) => { event.waitUntil(getWindowClients().then((list) => {
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
const client = list[i] const client = list[i]
if (client.url === '/' && 'focus' in client) { return client.focus() } client.postMessage({ type: 'notificationClicked', id: event.notification.tag })
}
for (let i = 0; i < list.length; i++) {
const client = list[i]
if (state.lastFocused === null || client.id === state.lastFocused) {
if ('focus' in client) return client.focus()
}
} }
if (clients.openWindow) return clients.openWindow('/') if (clients.openWindow) return clients.openWindow('/')

View File

@ -77,24 +77,6 @@ describe('Statuses module', () => {
expect(state.timelines.public.newStatusCount).to.equal(0) expect(state.timelines.public.newStatusCount).to.equal(0)
}) })
it('removes statuses by tag on deletion', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
const otherStatus = makeMockStatus({ id: '3' })
status.uri = 'xxx'
const deletion = makeMockStatus({ id: '2', type: 'deletion' })
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
deletion.uri = 'xxx'
mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' })
mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' })
expect(state.allStatuses).to.eql([otherStatus])
expect(state.timelines.public.statuses).to.eql([otherStatus])
expect(state.timelines.public.visibleStatuses).to.eql([otherStatus])
expect(state.timelines.public.maxId).to.eql('3')
})
it('does not update the maxId when the noIdUpdate flag is set', () => { it('does not update the maxId when the noIdUpdate flag is set', () => {
const state = defaultState() const state = defaultState()
const status = makeMockStatus({ id: '1' }) const status = makeMockStatus({ id: '1' })
@ -315,62 +297,4 @@ describe('Statuses module', () => {
expect(state.timelines.user.userId).to.eql(123) expect(state.timelines.user.userId).to.eql(123)
}) })
}) })
describe('notifications', () => {
it('removes a notification when the notice gets removed', () => {
const user = { id: '1' }
const state = defaultState()
const status = makeMockStatus({ id: '1' })
const otherStatus = makeMockStatus({ id: '3' })
const mentionedStatus = makeMockStatus({ id: '2' })
mentionedStatus.attentions = [user]
mentionedStatus.uri = 'xxx'
otherStatus.attentions = [user]
const deletion = makeMockStatus({ id: '4', type: 'deletion' })
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
deletion.uri = 'xxx'
const newNotificationSideEffects = () => {}
mutations.addNewStatuses(state, { statuses: [status, otherStatus], user })
mutations.addNewNotifications(
state,
{
notifications: [{
from_profile: { id: '2' },
id: '998',
type: 'mention',
status: otherStatus,
action: otherStatus,
seen: false
}],
newNotificationSideEffects
})
expect(state.notifications.data.length).to.eql(1)
mutations.addNewNotifications(
state,
{
notifications: [{
from_profile: { id: '2' },
id: '999',
type: 'mention',
status: mentionedStatus,
action: mentionedStatus,
seen: false
}],
newNotificationSideEffects
})
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
expect(state.allStatuses.length).to.eql(3)
expect(state.notifications.data.length).to.eql(2)
expect(state.notifications.data[1].status).to.eql(mentionedStatus)
expect(state.notifications.data[1].action).to.eql(mentionedStatus)
expect(state.notifications.data[1].type).to.eql('mention')
mutations.addNewStatuses(state, { statuses: [deletion], user })
expect(state.allStatuses.length).to.eql(2)
expect(state.notifications.data.length).to.eql(1)
})
})
}) })

View File

@ -5,7 +5,6 @@ describe('NotificationUtils', () => {
it('should return sorted notifications with configured types', () => { it('should return sorted notifications with configured types', () => {
const store = { const store = {
state: { state: {
statuses: {
notifications: { notifications: {
data: [ data: [
{ {
@ -26,7 +25,8 @@ describe('NotificationUtils', () => {
] ]
} }
}, },
config: { getters: {
mergedConfig: {
notificationVisibility: { notificationVisibility: {
likes: true, likes: true,
repeats: true, repeats: true,
@ -55,7 +55,6 @@ describe('NotificationUtils', () => {
it('should return only notifications not marked as seen', () => { it('should return only notifications not marked as seen', () => {
const store = { const store = {
state: { state: {
statuses: {
notifications: { notifications: {
data: [ data: [
{ {
@ -71,7 +70,8 @@ describe('NotificationUtils', () => {
] ]
} }
}, },
config: { getters: {
mergedConfig: {
notificationVisibility: { notificationVisibility: {
likes: true, likes: true,
repeats: true, repeats: true,

View File

@ -2345,10 +2345,10 @@
dependencies: dependencies:
js-beautify "1.14.6" js-beautify "1.14.6"
"@vuelidate/core@2.0.2": "@vuelidate/core@2.0.3":
version "2.0.2" version "2.0.3"
resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.2.tgz#e874afc830ccc5295e83a0c0a0f0621e084348c9" resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.3.tgz#40468c5ed15b72bde880a026b0699c2f0f1ecede"
integrity sha512-aG1OZWv6xVws3ljyKy/pyxq1rdZZ2ryj+FEREcC9d4GP4qOvNHHZUl/NQxa0Bck3Ooc0RfXU8vwCA9piRoWy6w== integrity sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA==
dependencies: dependencies:
vue-demi "^0.13.11" vue-demi "^0.13.11"