Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master
This commit is contained in:
		
						commit
						503e7fb4d8
					
				
					 15 changed files with 311 additions and 207 deletions
				
			
		| 
						 | 
					@ -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,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,7 @@ export default class Account extends ImmutablePureComponent {
 | 
				
			||||||
    onFollow: PropTypes.func.isRequired,
 | 
					    onFollow: PropTypes.func.isRequired,
 | 
				
			||||||
    onBlock: PropTypes.func.isRequired,
 | 
					    onBlock: PropTypes.func.isRequired,
 | 
				
			||||||
    onMute: PropTypes.func.isRequired,
 | 
					    onMute: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onMuteNotifications: PropTypes.func.isRequired,
 | 
				
			||||||
    intl: PropTypes.object.isRequired,
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
    hidden: PropTypes.bool,
 | 
					    hidden: PropTypes.bool,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,19 +71,22 @@ export default class GettingStarted extends ImmutablePureComponent {
 | 
				
			||||||
    navItems = navItems.concat([
 | 
					    navItems = navItems.concat([
 | 
				
			||||||
      <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
 | 
					      <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
 | 
				
			||||||
      <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
 | 
					      <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
 | 
				
			||||||
      <ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />,
 | 
					      <ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />,
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (myAccount.get('locked')) {
 | 
					    if (myAccount.get('locked')) {
 | 
				
			||||||
      navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
 | 
					      navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    navItems = navItems.concat([
 | 
					    navItems = navItems.concat([
 | 
				
			||||||
      <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
 | 
					      <ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
 | 
				
			||||||
      <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
 | 
					      <ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
 | 
				
			||||||
      <ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
 | 
					 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (multiColumn) {
 | 
				
			||||||
 | 
					      navItems.push(<ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
 | 
					      <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
 | 
				
			||||||
        <div className='getting-started__wrapper'>
 | 
					        <div className='getting-started__wrapper'>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,6 @@ export default class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
    settings: ImmutablePropTypes.map.isRequired,
 | 
					    settings: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
    pushSettings: ImmutablePropTypes.map.isRequired,
 | 
					    pushSettings: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
    onChange: PropTypes.func.isRequired,
 | 
					    onChange: PropTypes.func.isRequired,
 | 
				
			||||||
    onSave: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onClear: PropTypes.func.isRequired,
 | 
					    onClear: PropTypes.func.isRequired,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
import ColumnSettings from '../components/column_settings';
 | 
					import ColumnSettings from '../components/column_settings';
 | 
				
			||||||
import { changeSetting, saveSettings } from '../../../actions/settings';
 | 
					import { changeSetting } from '../../../actions/settings';
 | 
				
			||||||
import { clearNotifications } from '../../../actions/notifications';
 | 
					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';
 | 
					import { openModal } from '../../../actions/modal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
| 
						 | 
					@ -26,11 +26,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onSave () {
 | 
					 | 
				
			||||||
    dispatch(saveSettings());
 | 
					 | 
				
			||||||
    dispatch(savePushNotificationSettings());
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onClear () {
 | 
					  onClear () {
 | 
				
			||||||
    dispatch(openModal('CONFIRM', {
 | 
					    dispatch(openModal('CONFIRM', {
 | 
				
			||||||
      message: intl.formatMessage(messages.clearMessage),
 | 
					      message: intl.formatMessage(messages.clearMessage),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,6 @@ ColumnLink.propTypes = {
 | 
				
			||||||
  to: PropTypes.string,
 | 
					  to: PropTypes.string,
 | 
				
			||||||
  href: PropTypes.string,
 | 
					  href: PropTypes.string,
 | 
				
			||||||
  method: PropTypes.string,
 | 
					  method: PropTypes.string,
 | 
				
			||||||
  hideOnMobile: PropTypes.bool,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default ColumnLink;
 | 
					export default ColumnLink;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import * as WebPushSubscription from './web_push_subscription';
 | 
					import { register as registerPushNotifications } from './actions/push_notifications';
 | 
				
			||||||
import Mastodon from './containers/mastodon';
 | 
					import { default as Mastodon, store } from './containers/mastodon';
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import ReactDOM from 'react-dom';
 | 
					import ReactDOM from 'react-dom';
 | 
				
			||||||
import ready from './ready';
 | 
					import ready from './ready';
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ function main() {
 | 
				
			||||||
    if (process.env.NODE_ENV === 'production') {
 | 
					    if (process.env.NODE_ENV === 'production') {
 | 
				
			||||||
      // avoid offline in dev mode because it's harder to debug
 | 
					      // avoid offline in dev mode because it's harder to debug
 | 
				
			||||||
      require('offline-plugin/runtime').install();
 | 
					      require('offline-plugin/runtime').install();
 | 
				
			||||||
      WebPushSubscription.register();
 | 
					      store.dispatch(registerPushNotifications.register());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    perf.stop('main()');
 | 
					    perf.stop('main()');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { STORE_HYDRATE } from '../actions/store';
 | 
					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';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = Immutable.Map({
 | 
					const initialState = Immutable.Map({
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ export default function push_subscriptions(state = initialState, action) {
 | 
				
			||||||
    return state.set('browserSupport', action.value);
 | 
					    return state.set('browserSupport', action.value);
 | 
				
			||||||
  case CLEAR_SUBSCRIPTION:
 | 
					  case CLEAR_SUBSCRIPTION:
 | 
				
			||||||
    return initialState;
 | 
					    return initialState;
 | 
				
			||||||
  case ALERTS_CHANGE:
 | 
					  case SET_ALERTS:
 | 
				
			||||||
    return state.setIn(action.key, action.value);
 | 
					    return state.setIn(action.key, action.value);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    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.');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -214,6 +214,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-menu {
 | 
					.dropdown-menu {
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  transform-origin: 50% 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown--active .icon-button {
 | 
					.dropdown--active .icon-button {
 | 
				
			||||||
| 
						 | 
					@ -2148,7 +2149,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import 'boost';
 | 
					@import 'boost';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
button.icon-button i.fa-retweet {
 | 
					.no-reduce-motion button.icon-button i.fa-retweet {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  background-position: 0 0;
 | 
					  background-position: 0 0;
 | 
				
			||||||
  height: 19px;
 | 
					  height: 19px;
 | 
				
			||||||
  transition: background-position 0.9s steps(10);
 | 
					  transition: background-position 0.9s steps(10);
 | 
				
			||||||
| 
						 | 
					@ -2159,13 +2161,23 @@ button.icon-button i.fa-retweet {
 | 
				
			||||||
  &::before {
 | 
					  &::before {
 | 
				
			||||||
    display: none !important;
 | 
					    display: none !important;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
button.icon-button.active i.fa-retweet {
 | 
					.no-reduce-motion button.icon-button.active i.fa-retweet {
 | 
				
			||||||
  transition-duration: 0.9s;
 | 
					  transition-duration: 0.9s;
 | 
				
			||||||
  background-position: 0 100%;
 | 
					  background-position: 0 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reduce-motion button.icon-button i.fa-retweet {
 | 
				
			||||||
 | 
					  color: $ui-base-lighter-color;
 | 
				
			||||||
 | 
					  transition: color 100ms ease-in;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reduce-motion button.icon-button.active i.fa-retweet {
 | 
				
			||||||
 | 
					  color: $ui-highlight-color;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.status-card {
 | 
					.status-card {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
| 
						 | 
					@ -2943,6 +2955,7 @@ button.icon-button.active i.fa-retweet {
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  margin-left: 40px;
 | 
					  margin-left: 40px;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  transform-origin: 50% 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.privacy-dropdown__option {
 | 
					.privacy-dropdown__option {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - body_classes ||= @body_classes || ''
 | 
					  - body_classes ||= @body_classes || ''
 | 
				
			||||||
  - body_classes += ' system-font' if current_account&.user&.setting_system_font_ui
 | 
					  - body_classes += ' system-font' if current_account&.user&.setting_system_font_ui
 | 
				
			||||||
 | 
					  - body_classes += current_account&.user&.setting_reduce_motion ? ' reduce-motion' : ' no-reduce-motion'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %body{ class: add_rtl_body_class(body_classes) }
 | 
					  %body{ class: add_rtl_body_class(body_classes) }
 | 
				
			||||||
    = content_for?(:content) ? yield(:content) : yield
 | 
					    = content_for?(:content) ? yield(:content) : yield
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require 'optparse'
 | 
				
			||||||
 | 
					require 'colorize'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace :mastodon do
 | 
					namespace :mastodon do
 | 
				
			||||||
  desc 'Execute daily tasks (deprecated)'
 | 
					  desc 'Execute daily tasks (deprecated)'
 | 
				
			||||||
  task :daily do
 | 
					  task :daily do
 | 
				
			||||||
| 
						 | 
					@ -338,5 +341,75 @@ namespace :mastodon do
 | 
				
			||||||
      PreviewCard.where(embed_url: '', type: :photo).delete_all
 | 
					      PreviewCard.where(embed_url: '', type: :photo).delete_all
 | 
				
			||||||
      LinkCrawlWorker.push_bulk status_ids
 | 
					      LinkCrawlWorker.push_bulk status_ids
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    desc 'Check every known remote account and delete those that no longer exist in origin'
 | 
				
			||||||
 | 
					    task purge_removed_accounts: :environment do
 | 
				
			||||||
 | 
					      prepare_for_options!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      options = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      OptionParser.new do |opts|
 | 
				
			||||||
 | 
					        opts.banner = 'Usage: rails mastodon:maintenance:purge_removed_accounts [options]'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        opts.on('-f', '--force', 'Remove all encountered accounts without asking for confirmation') do
 | 
				
			||||||
 | 
					          options[:force] = true
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        opts.on('-h', '--help', 'Display this message') do
 | 
				
			||||||
 | 
					          puts opts
 | 
				
			||||||
 | 
					          exit
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end.parse!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      disable_log_stdout!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      total        = Account.remote.where(protocol: :activitypub).count
 | 
				
			||||||
 | 
					      progress_bar = ProgressBar.create(total: total, format: '%c/%C |%w>%i| %e')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
 | 
				
			||||||
 | 
					        progress_bar.increment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        begin
 | 
				
			||||||
 | 
					          res = Request.new(:head, account.uri).perform
 | 
				
			||||||
 | 
					        rescue StandardError
 | 
				
			||||||
 | 
					          # This could happen due to network timeout, DNS timeout, wrong SSL cert, etc,
 | 
				
			||||||
 | 
					          # which should probably not lead to perceiving the account as deleted, so
 | 
				
			||||||
 | 
					          # just skip till next time
 | 
				
			||||||
 | 
					          next
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if [404, 410].include?(res.code)
 | 
				
			||||||
 | 
					          if options[:force]
 | 
				
			||||||
 | 
					            account.destroy
 | 
				
			||||||
 | 
					          else
 | 
				
			||||||
 | 
					            progress_bar.pause
 | 
				
			||||||
 | 
					            progress_bar.clear
 | 
				
			||||||
 | 
					            print "\nIt seems like #{account.acct} no longer exists. Purge the account from the database? [Y/n]: ".colorize(:yellow)
 | 
				
			||||||
 | 
					            confirm = STDIN.gets.chomp
 | 
				
			||||||
 | 
					            puts ''
 | 
				
			||||||
 | 
					            progress_bar.resume
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if confirm.casecmp('n').zero?
 | 
				
			||||||
 | 
					              next
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					              account.destroy
 | 
				
			||||||
            end
 | 
					            end
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def disable_log_stdout!
 | 
				
			||||||
 | 
					  dev_null = Logger.new('/dev/null')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Rails.logger                 = dev_null
 | 
				
			||||||
 | 
					  ActiveRecord::Base.logger    = dev_null
 | 
				
			||||||
 | 
					  HttpLog.configuration.logger = dev_null
 | 
				
			||||||
 | 
					  Paperclip.options[:log]      = false
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def prepare_for_options!
 | 
				
			||||||
 | 
					  2.times { ARGV.shift }
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue