Compare commits
55 Commits
4e3268a073
...
032e68689d
Author | SHA1 | Date | |
---|---|---|---|
032e68689d | |||
|
6391a6a4ea | ||
|
5bd8a78a7a | ||
|
2b41c1cfe8 | ||
|
55ed65331f | ||
|
ff10834f1a | ||
|
5ad8f2cd5c | ||
|
5ee8fc0aea | ||
|
99d04bed2b | ||
|
b394392d0d | ||
|
febc2aa569 | ||
|
0fd1b26fb6 | ||
|
4e8bb80dbd | ||
|
bcdf336242 | ||
|
a98e241a81 | ||
|
5eed197c3e | ||
|
4a7b49181a | ||
|
fb4d43ced2 | ||
|
51f1f05b2d | ||
|
c25170d7d9 | ||
|
a5f09b7263 | ||
|
33564d8ccc | ||
|
92685e37b6 | ||
|
e36548579f | ||
|
37e3a23f2a | ||
|
1931e7c3ba | ||
|
38b6f0a013 | ||
|
cdc0959135 | ||
|
c0e2ba37c8 | ||
|
072a06fc89 | ||
|
d178a56924 | ||
|
fd3ad106be | ||
|
e3ee3eacca | ||
|
2f90c629b8 | ||
|
a564fc1a1f | ||
|
c216340001 | ||
|
af27e2ca7b | ||
|
388a7ac175 | ||
|
f3a859ff9e | ||
|
ce17ebd3d0 | ||
|
a17defc5ab | ||
|
6ed2cb8f43 | ||
|
aad3225d25 | ||
|
fffa7a4f4a | ||
|
e508ca6a1f | ||
|
ec2937ec1f | ||
|
c059f4a7ee | ||
|
e0b8ad9f14 | ||
|
77e270ef58 | ||
|
f449bfe2f1 | ||
|
e3bf9a5185 | ||
|
0628aac664 | ||
|
1b7e930b2e | ||
|
73fbe89a4b | ||
|
4beb29918a |
1
changelog.d/add-apng.add
Normal file
1
changelog.d/add-apng.add
Normal file
@ -0,0 +1 @@
|
|||||||
|
Make Pleroma FE to also view apng (Animated PNG) attachment.
|
1
changelog.d/double-notifications.fix
Normal file
1
changelog.d/double-notifications.fix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix native notifications appearing as many times as there are open tabs. Clicking on notification will focus last focused tab.
|
1
changelog.d/focus-clear.add
Normal file
1
changelog.d/focus-clear.add
Normal file
@ -0,0 +1 @@
|
|||||||
|
Focusing into a tab clears all current desktop notifications
|
1
changelog.d/mobile-chrome-notifs.fix
Normal file
1
changelog.d/mobile-chrome-notifs.fix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fixed error that appeared on mobile Chrome(ium) (and derivatives) when native notifications are allowed
|
1
changelog.d/mobile-drawer-notifications.change
Normal file
1
changelog.d/mobile-drawer-notifications.change
Normal 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.
|
1
changelog.d/more-notification-types-setting.fix
Normal file
1
changelog.d/more-notification-types-setting.fix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fixed being unable to set notification visibility for reports and follow requests
|
1
changelog.d/native-filtering.add
Normal file
1
changelog.d/native-filtering.add
Normal 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.
|
1
changelog.d/native-notifications.add
Normal file
1
changelog.d/native-notifications.add
Normal file
@ -0,0 +1 @@
|
|||||||
|
Native notifications now also have "badge" property that matches instance's favicon (visible in Android Chromium at least)
|
1
changelog.d/noninteractive-ignore-read.add
Normal file
1
changelog.d/noninteractive-ignore-read.add
Normal 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)
|
1
changelog.d/notification-read.add
Normal file
1
changelog.d/notification-read.add
Normal 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.
|
1
changelog.d/notifications-sorting.change
Normal file
1
changelog.d/notifications-sorting.change
Normal 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.
|
1
changelog.d/serviceworkers.change
Normal file
1
changelog.d/serviceworkers.change
Normal 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.
|
1
changelog.d/unreads-sync.fix
Normal file
1
changelog.d/unreads-sync.fix
Normal file
@ -0,0 +1 @@
|
|||||||
|
unread notifications should now properly catch up (eventually) in polling mode
|
1
changelog.d/video-poster.fix
Normal file
1
changelog.d/video-poster.fix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Video posters on Safari
|
1
changelog.d/web-push-always.add
Normal file
1
changelog.d/web-push-always.add
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added option to always "show" notifications when using web push for better compatibility with some browsers (chrome, edge, safari)
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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 } }) {
|
||||||
|
@ -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')"
|
||||||
|
@ -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 })
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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: {
|
||||||
|
@ -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 {
|
||||||
|
@ -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: {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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": {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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,
|
||||||
@ -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
|
||||||
|
@ -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: [],
|
||||||
|
169
src/modules/notifications.js
Normal file
169
src/modules/notifications.js
Normal 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
|
@ -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 } }
|
||||||
|
@ -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 }),
|
||||||
|
@ -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'))
|
||||||
})
|
})
|
||||||
|
@ -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])
|
||||||
}
|
}
|
||||||
|
@ -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({})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -55,10 +55,13 @@ const createFaviconService = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOriginalFavicons = () => [...favicons]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initFaviconService,
|
initFaviconService,
|
||||||
clearFaviconBadge,
|
clearFaviconBadge,
|
||||||
drawFaviconBadge
|
drawFaviconBadge,
|
||||||
|
getOriginalFavicons
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)$/)) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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}`))
|
||||||
}
|
}
|
98
src/sw.js
98
src/sw.js
@ -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('/')
|
||||||
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user