forked from AkkomaGang/akkoma-fe
Merge branch 'feature/push-subscriptions' into 'develop'
add service worker and push notifications See merge request pleroma/pleroma-fe!404
This commit is contained in:
commit
8e4777ccc6
13 changed files with 213 additions and 21 deletions
|
@ -2,6 +2,7 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
|
@ -91,5 +92,10 @@ module.exports = {
|
|||
browsers: ['last 2 versions']
|
||||
})
|
||||
]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new ServiceWorkerWebpackPlugin({
|
||||
entry: path.join(__dirname, '..', 'src/sw.js')
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
"raw-loader": "^0.5.1",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "^5.3.0",
|
||||
"serviceworker-webpack-plugin": "0.2.3",
|
||||
"shelljs": "^0.7.4",
|
||||
"sinon": "^1.17.3",
|
||||
"sinon-chai": "^2.8.0",
|
||||
|
|
|
@ -17,17 +17,21 @@ import FollowRequests from '../components/follow_requests/follow_requests.vue'
|
|||
import OAuthCallback from '../components/oauth_callback/oauth_callback.vue'
|
||||
import UserSearch from '../components/user_search/user_search.vue'
|
||||
|
||||
const afterStoreSetup = ({store, i18n}) => {
|
||||
const afterStoreSetup = ({ store, i18n }) => {
|
||||
window.fetch('/api/statusnet/config.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const {name, closed: registrationClosed, textlimit, server} = data.site
|
||||
const { name, closed: registrationClosed, textlimit, server, vapidPublicKey } = data.site
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
}
|
||||
|
||||
var apiConfig = data.site.pleromafe
|
||||
|
||||
window.fetch('/static/config.json')
|
||||
|
|
|
@ -47,6 +47,7 @@ const settings = {
|
|||
scopeCopyLocal: user.scopeCopy,
|
||||
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
||||
stopGifs: user.stopGifs,
|
||||
webPushNotificationsLocal: user.webPushNotifications,
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
|
@ -142,6 +143,10 @@ const settings = {
|
|||
},
|
||||
stopGifs (value) {
|
||||
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
||||
},
|
||||
webPushNotificationsLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
|
||||
if (value) this.$store.dispatch('registerPushNotifications')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,6 +143,18 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.notifications')}}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<input type="checkbox" id="webPushNotifications" v-model="webPushNotificationsLocal">
|
||||
<label for="webPushNotifications">
|
||||
{{$t('settings.enable_web_push_notifications')}}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.theme')" >
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
"inputRadius": "Input fields",
|
||||
"checkboxRadius": "Checkboxes",
|
||||
"instance_default": "(default: {value})",
|
||||
"instance_default_simple" : "(default)",
|
||||
"instance_default_simple": "(default)",
|
||||
"interface": "Interface",
|
||||
"interfaceLanguage": "Interface language",
|
||||
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
|
||||
|
@ -190,6 +190,8 @@
|
|||
"false": "no",
|
||||
"true": "yes"
|
||||
},
|
||||
"notifications": "Notifications",
|
||||
"enable_web_push_notifications": "Enable web push notifications",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "Keep colors",
|
||||
|
|
30
src/main.js
30
src/main.js
|
@ -50,6 +50,32 @@ const persistedStateOptions = {
|
|||
'oauth'
|
||||
]
|
||||
}
|
||||
|
||||
const registerPushNotifications = store => {
|
||||
store.subscribe((mutation, state) => {
|
||||
const vapidPublicKey = state.instance.vapidPublicKey
|
||||
const permission = state.interface.notificationPermission === 'granted'
|
||||
const isUserMutation = mutation.type === 'setCurrentUser'
|
||||
|
||||
if (isUserMutation && vapidPublicKey && permission) {
|
||||
return store.dispatch('registerPushNotifications')
|
||||
}
|
||||
|
||||
const user = state.users.currentUser
|
||||
const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey'
|
||||
|
||||
if (isVapidMutation && user && permission) {
|
||||
return store.dispatch('registerPushNotifications')
|
||||
}
|
||||
|
||||
const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted'
|
||||
|
||||
if (isPermMutation && user && vapidPublicKey) {
|
||||
return store.dispatch('registerPushNotifications')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
|
@ -62,10 +88,10 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
|||
chat: chatModule,
|
||||
oauth: oauthModule
|
||||
},
|
||||
plugins: [persistedState],
|
||||
plugins: [persistedState, registerPushNotifications],
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
// strict: process.env.NODE_ENV !== 'production'
|
||||
})
|
||||
|
||||
afterStoreSetup({store, i18n})
|
||||
afterStoreSetup({ store, i18n })
|
||||
})
|
||||
|
|
|
@ -24,6 +24,7 @@ const defaultState = {
|
|||
likes: true,
|
||||
repeats: true
|
||||
},
|
||||
webPushNotifications: true,
|
||||
muteWords: [],
|
||||
highlight: {},
|
||||
interfaceLanguage: browserLocale,
|
||||
|
|
|
@ -3,12 +3,13 @@ import { set, delete as del } from 'vue'
|
|||
const defaultState = {
|
||||
settings: {
|
||||
currentSaveStateNotice: null,
|
||||
noticeClearTimeout: null
|
||||
noticeClearTimeout: null,
|
||||
notificationPermission: null
|
||||
},
|
||||
browserSupport: {
|
||||
cssFilter: window.CSS && window.CSS.supports && (
|
||||
window.CSS.supports('filter', 'drop-shadow(0 0)') ||
|
||||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
||||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -23,10 +24,13 @@ const interfaceMod = {
|
|||
}
|
||||
set(state.settings, 'currentSaveStateNotice', { error: false, data: success })
|
||||
set(state.settings, 'noticeClearTimeout',
|
||||
setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
|
||||
setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
|
||||
} else {
|
||||
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
||||
}
|
||||
},
|
||||
setNotificationPermission (state, permission) {
|
||||
state.notificationPermission = permission
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
@ -35,6 +39,9 @@ const interfaceMod = {
|
|||
},
|
||||
settingsSaved ({ commit, dispatch }, { success, error }) {
|
||||
commit('settingsSaved', { success, error })
|
||||
},
|
||||
setNotificationPermission ({ commit }, permission) {
|
||||
commit('setNotificationPermission', permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { compact, map, each, merge } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import registerPushNotifications from '../services/push/push.js'
|
||||
import oauthApi from '../services/new_api/oauth'
|
||||
import {humanizeErrors} from './errors'
|
||||
import { humanizeErrors } from './errors'
|
||||
|
||||
// TODO: Unify with mergeOrAdd in statuses.js
|
||||
export const mergeOrAdd = (arr, obj, item) => {
|
||||
|
@ -11,17 +12,25 @@ export const mergeOrAdd = (arr, obj, item) => {
|
|||
if (oldItem) {
|
||||
// We already have this, so only merge the new info.
|
||||
merge(oldItem, item)
|
||||
return {item: oldItem, new: false}
|
||||
return { item: oldItem, new: false }
|
||||
} else {
|
||||
// This is a new item, prepare it
|
||||
arr.push(item)
|
||||
obj[item.id] = item
|
||||
return {item, new: true}
|
||||
return { item, new: true }
|
||||
}
|
||||
}
|
||||
|
||||
const getNotificationPermission = () => {
|
||||
const Notification = window.Notification
|
||||
|
||||
if (!Notification) return Promise.resolve(null)
|
||||
if (Notification.permission === 'default') return Notification.requestPermission()
|
||||
return Promise.resolve(Notification.permission)
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setMuted (state, { user: {id}, muted }) {
|
||||
setMuted (state, { user: { id }, muted }) {
|
||||
const user = state.usersObject[id]
|
||||
set(user, 'muted', muted)
|
||||
},
|
||||
|
@ -45,7 +54,7 @@ export const mutations = {
|
|||
setUserForStatus (state, status) {
|
||||
status.user = state.usersObject[status.user.id]
|
||||
},
|
||||
setColor (state, { user: {id}, highlighted }) {
|
||||
setColor (state, { user: { id }, highlighted }) {
|
||||
const user = state.usersObject[id]
|
||||
set(user, 'highlight', highlighted)
|
||||
},
|
||||
|
@ -77,9 +86,16 @@ const users = {
|
|||
mutations,
|
||||
actions: {
|
||||
fetchUser (store, id) {
|
||||
store.rootState.api.backendInteractor.fetchUser({id})
|
||||
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||
.then((user) => store.commit('addNewUsers', user))
|
||||
},
|
||||
registerPushNotifications (store) {
|
||||
const token = store.state.currentUser.credentials
|
||||
const vapidPublicKey = store.rootState.instance.vapidPublicKey
|
||||
const isEnabled = store.rootState.config.webPushNotifications
|
||||
|
||||
registerPushNotifications(isEnabled, vapidPublicKey, token)
|
||||
},
|
||||
addNewStatuses (store, { statuses }) {
|
||||
const users = map(statuses, 'user')
|
||||
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
||||
|
@ -143,6 +159,9 @@ const users = {
|
|||
commit('setCurrentUser', user)
|
||||
commit('addNewUsers', [user])
|
||||
|
||||
getNotificationPermission()
|
||||
.then(permission => commit('setNotificationPermission', permission))
|
||||
|
||||
// Set our new backend interactor
|
||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||
|
||||
|
@ -161,12 +180,8 @@ const users = {
|
|||
store.commit('addNewUsers', mutedUsers)
|
||||
})
|
||||
|
||||
if ('Notification' in window && window.Notification.permission === 'default') {
|
||||
window.Notification.requestPermission()
|
||||
}
|
||||
|
||||
// Fetch our friends
|
||||
store.rootState.api.backendInteractor.fetchFriends({id: user.id})
|
||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||
.then((friends) => commit('addNewUsers', friends))
|
||||
})
|
||||
} else {
|
||||
|
|
69
src/services/push/push.js
Normal file
69
src/services/push/push.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import runtime from 'serviceworker-webpack-plugin/lib/runtime'
|
||||
|
||||
function urlBase64ToUint8Array (base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
|
||||
const rawData = window.atob(base64)
|
||||
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
|
||||
}
|
||||
|
||||
function isPushSupported () {
|
||||
return 'serviceWorker' in navigator && 'PushManager' in window
|
||||
}
|
||||
|
||||
function registerServiceWorker () {
|
||||
return runtime.register()
|
||||
.catch((err) => console.error('Unable to register service worker.', err))
|
||||
}
|
||||
|
||||
function subscribe (registration, isEnabled, vapidPublicKey) {
|
||||
if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config'))
|
||||
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
|
||||
|
||||
const subscribeOptions = {
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
|
||||
}
|
||||
return registration.pushManager.subscribe(subscribeOptions)
|
||||
}
|
||||
|
||||
function sendSubscriptionToBackEnd (subscription, token) {
|
||||
return window.fetch('/api/v1/push/subscription/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription,
|
||||
data: {
|
||||
alerts: {
|
||||
follow: true,
|
||||
favourite: true,
|
||||
mention: true,
|
||||
reblog: true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error('Bad status code from server.')
|
||||
return response.json()
|
||||
})
|
||||
.then((responseData) => {
|
||||
if (!responseData.id) throw new Error('Bad response from server.')
|
||||
return responseData
|
||||
})
|
||||
}
|
||||
|
||||
export default function registerPushNotifications (isEnabled, vapidPublicKey, token) {
|
||||
if (isPushSupported()) {
|
||||
registerServiceWorker()
|
||||
.then((registration) => subscribe(registration, isEnabled, vapidPublicKey))
|
||||
.then((subscription) => sendSubscriptionToBackEnd(subscription, token))
|
||||
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
|
||||
}
|
||||
}
|
38
src/sw.js
Normal file
38
src/sw.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/* eslint-env serviceworker */
|
||||
|
||||
import localForage from 'localforage'
|
||||
|
||||
function isEnabled () {
|
||||
return localForage.getItem('vuex-lz')
|
||||
.then(data => data.config.webPushNotifications)
|
||||
}
|
||||
|
||||
function getWindowClients () {
|
||||
return clients.matchAll({ includeUncontrolled: true })
|
||||
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
|
||||
}
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
if (event.data) {
|
||||
event.waitUntil(isEnabled().then((isEnabled) => {
|
||||
return isEnabled && getWindowClients().then((list) => {
|
||||
const data = event.data.json()
|
||||
|
||||
if (list.length === 0) return self.registration.showNotification(data.title, data)
|
||||
})
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close()
|
||||
|
||||
event.waitUntil(getWindowClients().then((list) => {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var client = list[i]
|
||||
if (client.url === '/' && 'focus' in client) { return client.focus() }
|
||||
}
|
||||
|
||||
if (clients.openWindow) return clients.openWindow('/')
|
||||
}))
|
||||
})
|
|
@ -3925,7 +3925,7 @@ mime@^1.3.4, mime@^1.5.0:
|
|||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
|
||||
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
|
||||
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
dependencies:
|
||||
|
@ -5289,6 +5289,12 @@ serve-static@1.13.1:
|
|||
parseurl "~1.3.2"
|
||||
send "0.16.1"
|
||||
|
||||
serviceworker-webpack-plugin@0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/serviceworker-webpack-plugin/-/serviceworker-webpack-plugin-0.2.3.tgz#1873ed6fc83c873ac8240fac443c615d374feeb2"
|
||||
dependencies:
|
||||
minimatch "^3.0.3"
|
||||
|
||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
|
Loading…
Reference in a new issue