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
|
@ -2,6 +2,7 @@ var path = require('path')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var projectRoot = path.resolve(__dirname, '../')
|
var projectRoot = path.resolve(__dirname, '../')
|
||||||
|
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||||
|
|
||||||
var env = process.env.NODE_ENV
|
var env = process.env.NODE_ENV
|
||||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||||
|
@ -91,5 +92,10 @@ module.exports = {
|
||||||
browsers: ['last 2 versions']
|
browsers: ['last 2 versions']
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
plugins: [
|
||||||
|
new ServiceWorkerWebpackPlugin({
|
||||||
|
entry: path.join(__dirname, '..', 'src/sw.js')
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
|
"serviceworker-webpack-plugin": "0.2.3",
|
||||||
"shelljs": "^0.7.4",
|
"shelljs": "^0.7.4",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^1.17.3",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^2.8.0",
|
||||||
|
|
|
@ -21,13 +21,17 @@ const afterStoreSetup = ({store, i18n}) => {
|
||||||
window.fetch('/api/statusnet/config.json')
|
window.fetch('/api/statusnet/config.json')
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.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: 'name', value: name })
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
||||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
|
||||||
|
if (vapidPublicKey) {
|
||||||
|
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||||
|
}
|
||||||
|
|
||||||
var apiConfig = data.site.pleromafe
|
var apiConfig = data.site.pleromafe
|
||||||
|
|
||||||
window.fetch('/static/config.json')
|
window.fetch('/static/config.json')
|
||||||
|
|
|
@ -47,6 +47,7 @@ const settings = {
|
||||||
scopeCopyLocal: user.scopeCopy,
|
scopeCopyLocal: user.scopeCopy,
|
||||||
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
||||||
stopGifs: user.stopGifs,
|
stopGifs: user.stopGifs,
|
||||||
|
webPushNotificationsLocal: user.webPushNotifications,
|
||||||
loopSilentAvailable:
|
loopSilentAvailable:
|
||||||
// Firefox
|
// Firefox
|
||||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||||
|
@ -142,6 +143,10 @@ const settings = {
|
||||||
},
|
},
|
||||||
stopGifs (value) {
|
stopGifs (value) {
|
||||||
this.$store.dispatch('setOption', { name: '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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div :label="$t('settings.theme')" >
|
<div :label="$t('settings.theme')" >
|
||||||
|
|
|
@ -190,6 +190,8 @@
|
||||||
"false": "no",
|
"false": "no",
|
||||||
"true": "yes"
|
"true": "yes"
|
||||||
},
|
},
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"enable_web_push_notifications": "Enable web push notifications",
|
||||||
"style": {
|
"style": {
|
||||||
"switcher": {
|
"switcher": {
|
||||||
"keep_color": "Keep colors",
|
"keep_color": "Keep colors",
|
||||||
|
|
28
src/main.js
28
src/main.js
|
@ -50,6 +50,32 @@ const persistedStateOptions = {
|
||||||
'oauth'
|
'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) => {
|
createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
|
@ -62,7 +88,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
chat: chatModule,
|
chat: chatModule,
|
||||||
oauth: oauthModule
|
oauth: oauthModule
|
||||||
},
|
},
|
||||||
plugins: [persistedState],
|
plugins: [persistedState, registerPushNotifications],
|
||||||
strict: false // Socket modifies itself, let's ignore this for now.
|
strict: false // Socket modifies itself, let's ignore this for now.
|
||||||
// strict: process.env.NODE_ENV !== 'production'
|
// strict: process.env.NODE_ENV !== 'production'
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,6 +24,7 @@ const defaultState = {
|
||||||
likes: true,
|
likes: true,
|
||||||
repeats: true
|
repeats: true
|
||||||
},
|
},
|
||||||
|
webPushNotifications: true,
|
||||||
muteWords: [],
|
muteWords: [],
|
||||||
highlight: {},
|
highlight: {},
|
||||||
interfaceLanguage: browserLocale,
|
interfaceLanguage: browserLocale,
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { set, delete as del } from 'vue'
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
settings: {
|
settings: {
|
||||||
currentSaveStateNotice: null,
|
currentSaveStateNotice: null,
|
||||||
noticeClearTimeout: null
|
noticeClearTimeout: null,
|
||||||
|
notificationPermission: null
|
||||||
},
|
},
|
||||||
browserSupport: {
|
browserSupport: {
|
||||||
cssFilter: window.CSS && window.CSS.supports && (
|
cssFilter: window.CSS && window.CSS.supports && (
|
||||||
|
@ -27,6 +28,9 @@ const interfaceMod = {
|
||||||
} else {
|
} else {
|
||||||
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setNotificationPermission (state, permission) {
|
||||||
|
state.notificationPermission = permission
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -35,6 +39,9 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
settingsSaved ({ commit, dispatch }, { success, error }) {
|
settingsSaved ({ commit, dispatch }, { success, error }) {
|
||||||
commit('settingsSaved', { success, error })
|
commit('settingsSaved', { success, error })
|
||||||
|
},
|
||||||
|
setNotificationPermission ({ commit }, permission) {
|
||||||
|
commit('setNotificationPermission', permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { compact, map, each, merge } from 'lodash'
|
import { compact, map, each, merge } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
|
import registerPushNotifications from '../services/push/push.js'
|
||||||
import oauthApi from '../services/new_api/oauth'
|
import oauthApi from '../services/new_api/oauth'
|
||||||
import { humanizeErrors } from './errors'
|
import { humanizeErrors } from './errors'
|
||||||
|
|
||||||
|
@ -20,6 +21,14 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
export const mutations = {
|
||||||
setMuted (state, { user: { id }, muted }) {
|
setMuted (state, { user: { id }, muted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
|
@ -80,6 +89,13 @@ const users = {
|
||||||
store.rootState.api.backendInteractor.fetchUser({ id })
|
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||||
.then((user) => store.commit('addNewUsers', user))
|
.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 }) {
|
addNewStatuses (store, { statuses }) {
|
||||||
const users = map(statuses, 'user')
|
const users = map(statuses, 'user')
|
||||||
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
||||||
|
@ -143,6 +159,9 @@ const users = {
|
||||||
commit('setCurrentUser', user)
|
commit('setCurrentUser', user)
|
||||||
commit('addNewUsers', [user])
|
commit('addNewUsers', [user])
|
||||||
|
|
||||||
|
getNotificationPermission()
|
||||||
|
.then(permission => commit('setNotificationPermission', permission))
|
||||||
|
|
||||||
// Set our new backend interactor
|
// Set our new backend interactor
|
||||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||||
|
|
||||||
|
@ -161,10 +180,6 @@ const users = {
|
||||||
store.commit('addNewUsers', mutedUsers)
|
store.commit('addNewUsers', mutedUsers)
|
||||||
})
|
})
|
||||||
|
|
||||||
if ('Notification' in window && window.Notification.permission === 'default') {
|
|
||||||
window.Notification.requestPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch our friends
|
// Fetch our friends
|
||||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||||
.then((friends) => commit('addNewUsers', friends))
|
.then((friends) => commit('addNewUsers', friends))
|
||||||
|
|
|
@ -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}`))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
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"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5289,6 +5289,12 @@ serve-static@1.13.1:
|
||||||
parseurl "~1.3.2"
|
parseurl "~1.3.2"
|
||||||
send "0.16.1"
|
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:
|
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
|
|
Loading…
Reference in New Issue