Refactor web_push_subscription (#6047)
* Remove onSave method in mapped properties for column_settings * Make web_push_subscription.register an action
This commit is contained in:
		
				
					committed by
					
						 Eugen Rochko
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							081956742c
						
					
				
				
					commit
					35fdf561be
				
			| @@ -1,57 +0,0 @@ | ||||
| import axios from 'axios'; | ||||
| import { pushNotificationsSetting } from '../settings'; | ||||
|  | ||||
| export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; | ||||
| export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; | ||||
| export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; | ||||
| export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE'; | ||||
|  | ||||
| export function setBrowserSupport (value) { | ||||
|   return { | ||||
|     type: SET_BROWSER_SUPPORT, | ||||
|     value, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function setSubscription (subscription) { | ||||
|   return { | ||||
|     type: SET_SUBSCRIPTION, | ||||
|     subscription, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function clearSubscription () { | ||||
|   return { | ||||
|     type: CLEAR_SUBSCRIPTION, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function changeAlerts(key, value) { | ||||
|   return dispatch => { | ||||
|     dispatch({ | ||||
|       type: ALERTS_CHANGE, | ||||
|       key, | ||||
|       value, | ||||
|     }); | ||||
|  | ||||
|     dispatch(saveSettings()); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function saveSettings() { | ||||
|   return (_, getState) => { | ||||
|     const state = getState().get('push_notifications'); | ||||
|     const subscription = state.get('subscription'); | ||||
|     const alerts = state.get('alerts'); | ||||
|     const data = { alerts }; | ||||
|  | ||||
|     axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { | ||||
|       data, | ||||
|     }).then(() => { | ||||
|       const me = getState().getIn(['meta', 'me']); | ||||
|       if (me) { | ||||
|         pushNotificationsSetting.set(me, data); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										23
									
								
								app/javascript/mastodon/actions/push_notifications/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/javascript/mastodon/actions/push_notifications/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { | ||||
|   SET_BROWSER_SUPPORT, | ||||
|   SET_SUBSCRIPTION, | ||||
|   CLEAR_SUBSCRIPTION, | ||||
|   SET_ALERTS, | ||||
|   setAlerts, | ||||
| } from './setter'; | ||||
| import { register, saveSettings } from './registerer'; | ||||
|  | ||||
| export { | ||||
|   SET_BROWSER_SUPPORT, | ||||
|   SET_SUBSCRIPTION, | ||||
|   CLEAR_SUBSCRIPTION, | ||||
|   SET_ALERTS, | ||||
|   register, | ||||
| }; | ||||
|  | ||||
| export function changeAlerts(key, value) { | ||||
|   return dispatch => { | ||||
|     dispatch(setAlerts(key, value)); | ||||
|     dispatch(saveSettings()); | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										149
									
								
								app/javascript/mastodon/actions/push_notifications/registerer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								app/javascript/mastodon/actions/push_notifications/registerer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| import axios from 'axios'; | ||||
| import { pushNotificationsSetting } from '../../settings'; | ||||
| import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; | ||||
|  | ||||
| // Taken from https://www.npmjs.com/package/web-push | ||||
| const urlBase64ToUint8Array = (base64String) => { | ||||
|   const padding = '='.repeat((4 - base64String.length % 4) % 4); | ||||
|   const base64 = (base64String + padding) | ||||
|     .replace(/\-/g, '+') | ||||
|     .replace(/_/g, '/'); | ||||
|  | ||||
|   const rawData = window.atob(base64); | ||||
|   const outputArray = new Uint8Array(rawData.length); | ||||
|  | ||||
|   for (let i = 0; i < rawData.length; ++i) { | ||||
|     outputArray[i] = rawData.charCodeAt(i); | ||||
|   } | ||||
|   return outputArray; | ||||
| }; | ||||
|  | ||||
| const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); | ||||
|  | ||||
| const getRegistration = () => navigator.serviceWorker.ready; | ||||
|  | ||||
| const getPushSubscription = (registration) => | ||||
|   registration.pushManager.getSubscription() | ||||
|     .then(subscription => ({ registration, subscription })); | ||||
|  | ||||
| const subscribe = (registration) => | ||||
|   registration.pushManager.subscribe({ | ||||
|     userVisibleOnly: true, | ||||
|     applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()), | ||||
|   }); | ||||
|  | ||||
| const unsubscribe = ({ registration, subscription }) => | ||||
|   subscription ? subscription.unsubscribe().then(() => registration) : registration; | ||||
|  | ||||
| const sendSubscriptionToBackend = (subscription, me) => { | ||||
|   const params = { subscription }; | ||||
|  | ||||
|   if (me) { | ||||
|     const data = pushNotificationsSetting.get(me); | ||||
|     if (data) { | ||||
|       params.data = data; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return axios.post('/api/web/push_subscriptions', params).then(response => response.data); | ||||
| }; | ||||
|  | ||||
| // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload | ||||
| const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); | ||||
|  | ||||
| export default function register () { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(setBrowserSupport(supportsPushNotifications)); | ||||
|     const me = getState().getIn(['meta', 'me']); | ||||
|  | ||||
|     if (me && !pushNotificationsSetting.get(me)) { | ||||
|       const alerts = getState().getIn(['push_notifications', 'alerts']); | ||||
|       if (alerts) { | ||||
|         pushNotificationsSetting.set(me, { alerts: alerts }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (supportsPushNotifications) { | ||||
|       if (!getApplicationServerKey()) { | ||||
|         console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       getRegistration() | ||||
|         .then(getPushSubscription) | ||||
|         .then(({ registration, subscription }) => { | ||||
|           if (subscription !== null) { | ||||
|             // We have a subscription, check if it is still valid | ||||
|             const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); | ||||
|             const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString(); | ||||
|             const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']); | ||||
|  | ||||
|             // If the VAPID public key did not change and the endpoint corresponds | ||||
|             // to the endpoint saved in the backend, the subscription is valid | ||||
|             if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { | ||||
|               return subscription; | ||||
|             } else { | ||||
|               // Something went wrong, try to subscribe again | ||||
|               return unsubscribe({ registration, subscription }).then(subscribe).then( | ||||
|                 subscription => sendSubscriptionToBackend(subscription, me)); | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           // No subscription, try to subscribe | ||||
|           return subscribe(registration).then( | ||||
|             subscription => sendSubscriptionToBackend(subscription, me)); | ||||
|         }) | ||||
|         .then(subscription => { | ||||
|           // If we got a PushSubscription (and not a subscription object from the backend) | ||||
|           // it means that the backend subscription is valid (and was set during hydration) | ||||
|           if (!(subscription instanceof PushSubscription)) { | ||||
|             dispatch(setSubscription(subscription)); | ||||
|             if (me) { | ||||
|               pushNotificationsSetting.set(me, { alerts: subscription.alerts }); | ||||
|             } | ||||
|           } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|           if (error.code === 20 && error.name === 'AbortError') { | ||||
|             console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); | ||||
|           } else if (error.code === 5 && error.name === 'InvalidCharacterError') { | ||||
|             console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); | ||||
|           } | ||||
|  | ||||
|           // Clear alerts and hide UI settings | ||||
|           dispatch(clearSubscription()); | ||||
|           if (me) { | ||||
|             pushNotificationsSetting.remove(me); | ||||
|           } | ||||
|  | ||||
|           try { | ||||
|             getRegistration() | ||||
|               .then(getPushSubscription) | ||||
|               .then(unsubscribe); | ||||
|           } catch (e) { | ||||
|  | ||||
|           } | ||||
|         }); | ||||
|     } else { | ||||
|       console.warn('Your browser does not support Web Push Notifications.'); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function saveSettings() { | ||||
|   return (_, getState) => { | ||||
|     const state = getState().get('push_notifications'); | ||||
|     const subscription = state.get('subscription'); | ||||
|     const alerts = state.get('alerts'); | ||||
|     const data = { alerts }; | ||||
|  | ||||
|     axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { | ||||
|       data, | ||||
|     }).then(() => { | ||||
|       const me = getState().getIn(['meta', 'me']); | ||||
|       if (me) { | ||||
|         pushNotificationsSetting.set(me, data); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										34
									
								
								app/javascript/mastodon/actions/push_notifications/setter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/javascript/mastodon/actions/push_notifications/setter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; | ||||
| export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; | ||||
| export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; | ||||
| export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS'; | ||||
|  | ||||
| export function setBrowserSupport (value) { | ||||
|   return { | ||||
|     type: SET_BROWSER_SUPPORT, | ||||
|     value, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function setSubscription (subscription) { | ||||
|   return { | ||||
|     type: SET_SUBSCRIPTION, | ||||
|     subscription, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function clearSubscription () { | ||||
|   return { | ||||
|     type: CLEAR_SUBSCRIPTION, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function setAlerts (key, value) { | ||||
|   return dispatch => { | ||||
|     dispatch({ | ||||
|       type: SET_ALERTS, | ||||
|       key, | ||||
|       value, | ||||
|     }); | ||||
|   }; | ||||
| } | ||||
| @@ -11,7 +11,6 @@ export default class ColumnSettings extends React.PureComponent { | ||||
|     settings: ImmutablePropTypes.map.isRequired, | ||||
|     pushSettings: ImmutablePropTypes.map.isRequired, | ||||
|     onChange: PropTypes.func.isRequired, | ||||
|     onSave: PropTypes.func.isRequired, | ||||
|     onClear: PropTypes.func.isRequired, | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| import ColumnSettings from '../components/column_settings'; | ||||
| import { changeSetting, saveSettings } from '../../../actions/settings'; | ||||
| import { changeSetting } from '../../../actions/settings'; | ||||
| import { clearNotifications } from '../../../actions/notifications'; | ||||
| import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from '../../../actions/push_notifications'; | ||||
| import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; | ||||
| import { openModal } from '../../../actions/modal'; | ||||
|  | ||||
| const messages = defineMessages({ | ||||
| @@ -26,11 +26,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   onSave () { | ||||
|     dispatch(saveSettings()); | ||||
|     dispatch(savePushNotificationSettings()); | ||||
|   }, | ||||
|  | ||||
|   onClear () { | ||||
|     dispatch(openModal('CONFIRM', { | ||||
|       message: intl.formatMessage(messages.clearMessage), | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as WebPushSubscription from './web_push_subscription'; | ||||
| import Mastodon from './containers/mastodon'; | ||||
| import { register as registerPushNotifications } from './actions/push_notifications'; | ||||
| import { default as Mastodon, store } from './containers/mastodon'; | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import ready from './ready'; | ||||
| @@ -25,7 +25,7 @@ function main() { | ||||
|     if (process.env.NODE_ENV === 'production') { | ||||
|       // avoid offline in dev mode because it's harder to debug | ||||
|       require('offline-plugin/runtime').install(); | ||||
|       WebPushSubscription.register(); | ||||
|       store.dispatch(registerPushNotifications.register()); | ||||
|     } | ||||
|     perf.stop('main()'); | ||||
|   }); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { STORE_HYDRATE } from '../actions/store'; | ||||
| import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from '../actions/push_notifications'; | ||||
| import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push_notifications'; | ||||
| import Immutable from 'immutable'; | ||||
|  | ||||
| const initialState = Immutable.Map({ | ||||
| @@ -43,7 +43,7 @@ export default function push_subscriptions(state = initialState, action) { | ||||
|     return state.set('browserSupport', action.value); | ||||
|   case CLEAR_SUBSCRIPTION: | ||||
|     return initialState; | ||||
|   case ALERTS_CHANGE: | ||||
|   case SET_ALERTS: | ||||
|     return state.setIn(action.key, action.value); | ||||
|   default: | ||||
|     return state; | ||||
|   | ||||
| @@ -1,129 +0,0 @@ | ||||
| import axios from 'axios'; | ||||
| import { store } from './containers/mastodon'; | ||||
| import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications'; | ||||
| import { pushNotificationsSetting } from './settings'; | ||||
|  | ||||
| // Taken from https://www.npmjs.com/package/web-push | ||||
| const urlBase64ToUint8Array = (base64String) => { | ||||
|   const padding = '='.repeat((4 - base64String.length % 4) % 4); | ||||
|   const base64 = (base64String + padding) | ||||
|     .replace(/\-/g, '+') | ||||
|     .replace(/_/g, '/'); | ||||
|  | ||||
|   const rawData = window.atob(base64); | ||||
|   const outputArray = new Uint8Array(rawData.length); | ||||
|  | ||||
|   for (let i = 0; i < rawData.length; ++i) { | ||||
|     outputArray[i] = rawData.charCodeAt(i); | ||||
|   } | ||||
|   return outputArray; | ||||
| }; | ||||
|  | ||||
| const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); | ||||
|  | ||||
| const getRegistration = () => navigator.serviceWorker.ready; | ||||
|  | ||||
| const getPushSubscription = (registration) => | ||||
|   registration.pushManager.getSubscription() | ||||
|     .then(subscription => ({ registration, subscription })); | ||||
|  | ||||
| const subscribe = (registration) => | ||||
|   registration.pushManager.subscribe({ | ||||
|     userVisibleOnly: true, | ||||
|     applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()), | ||||
|   }); | ||||
|  | ||||
| const unsubscribe = ({ registration, subscription }) => | ||||
|   subscription ? subscription.unsubscribe().then(() => registration) : registration; | ||||
|  | ||||
| const sendSubscriptionToBackend = (subscription) => { | ||||
|   const params = { subscription }; | ||||
|  | ||||
|   const me = store.getState().getIn(['meta', 'me']); | ||||
|   if (me) { | ||||
|     const data = pushNotificationsSetting.get(me); | ||||
|     if (data) { | ||||
|       params.data = data; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return axios.post('/api/web/push_subscriptions', params).then(response => response.data); | ||||
| }; | ||||
|  | ||||
| // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload | ||||
| const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); | ||||
|  | ||||
| export function register () { | ||||
|   store.dispatch(setBrowserSupport(supportsPushNotifications)); | ||||
|   const me = store.getState().getIn(['meta', 'me']); | ||||
|  | ||||
|   if (me && !pushNotificationsSetting.get(me)) { | ||||
|     const alerts = store.getState().getIn(['push_notifications', 'alerts']); | ||||
|     if (alerts) { | ||||
|       pushNotificationsSetting.set(me, { alerts: alerts }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (supportsPushNotifications) { | ||||
|     if (!getApplicationServerKey()) { | ||||
|       console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     getRegistration() | ||||
|       .then(getPushSubscription) | ||||
|       .then(({ registration, subscription }) => { | ||||
|         if (subscription !== null) { | ||||
|           // We have a subscription, check if it is still valid | ||||
|           const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); | ||||
|           const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString(); | ||||
|           const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']); | ||||
|  | ||||
|           // If the VAPID public key did not change and the endpoint corresponds | ||||
|           // to the endpoint saved in the backend, the subscription is valid | ||||
|           if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { | ||||
|             return subscription; | ||||
|           } else { | ||||
|             // Something went wrong, try to subscribe again | ||||
|             return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // No subscription, try to subscribe | ||||
|         return subscribe(registration).then(sendSubscriptionToBackend); | ||||
|       }) | ||||
|       .then(subscription => { | ||||
|         // If we got a PushSubscription (and not a subscription object from the backend) | ||||
|         // it means that the backend subscription is valid (and was set during hydration) | ||||
|         if (!(subscription instanceof PushSubscription)) { | ||||
|           store.dispatch(setSubscription(subscription)); | ||||
|           if (me) { | ||||
|             pushNotificationsSetting.set(me, { alerts: subscription.alerts }); | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|       .catch(error => { | ||||
|         if (error.code === 20 && error.name === 'AbortError') { | ||||
|           console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); | ||||
|         } else if (error.code === 5 && error.name === 'InvalidCharacterError') { | ||||
|           console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); | ||||
|         } | ||||
|  | ||||
|         // Clear alerts and hide UI settings | ||||
|         store.dispatch(clearSubscription()); | ||||
|         if (me) { | ||||
|           pushNotificationsSetting.remove(me); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|           getRegistration() | ||||
|             .then(getPushSubscription) | ||||
|             .then(unsubscribe); | ||||
|         } catch (e) { | ||||
|  | ||||
|         } | ||||
|       }); | ||||
|   } else { | ||||
|     console.warn('Your browser does not support Web Push Notifications.'); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user