Fix browser notification permission request logic (#13543)
* Add notification permission handling code * Request notification permission when enabling any notification setting * Add badge to notification settings when permissions insufficient * Disable alerts by default, requesting permission and enable them on onboarding
This commit is contained in:
parent
5e1364c448
commit
f54ca3d08e
14 changed files with 215 additions and 15 deletions
|
@ -16,6 +16,7 @@ import { getFiltersRegex } from '../selectors';
|
||||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||||
import compareId from 'mastodon/compare_id';
|
import compareId from 'mastodon/compare_id';
|
||||||
import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer';
|
import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer';
|
||||||
|
import { requestNotificationPermission } from '../utils/notifications';
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||||
|
@ -33,8 +34,12 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
||||||
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
||||||
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
||||||
|
|
||||||
|
|
||||||
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||||
|
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||||
|
@ -235,6 +240,46 @@ export const unmountNotifications = () => ({
|
||||||
type: NOTIFICATIONS_UNMOUNT,
|
type: NOTIFICATIONS_UNMOUNT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const markNotificationsAsRead = () => ({
|
export const markNotificationsAsRead = () => ({
|
||||||
type: NOTIFICATIONS_MARK_AS_READ,
|
type: NOTIFICATIONS_MARK_AS_READ,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Browser support
|
||||||
|
export function setupBrowserNotifications() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch(setBrowserSupport('Notification' in window));
|
||||||
|
if ('Notification' in window) {
|
||||||
|
dispatch(setBrowserPermission(Notification.permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('Notification' in window && 'permissions' in navigator) {
|
||||||
|
navigator.permissions.query({ name: 'notifications' }).then((status) => {
|
||||||
|
status.onchange = () => dispatch(setBrowserPermission(Notification.permission));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestBrowserPermission(callback = noOp) {
|
||||||
|
return dispatch => {
|
||||||
|
requestNotificationPermission((permission) => {
|
||||||
|
dispatch(setBrowserPermission(permission));
|
||||||
|
callback(permission);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function setBrowserSupport (value) {
|
||||||
|
return {
|
||||||
|
type: NOTIFICATIONS_SET_BROWSER_SUPPORT,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBrowserPermission (value) {
|
||||||
|
return {
|
||||||
|
type: NOTIFICATIONS_SET_BROWSER_PERMISSION,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
import { changeSetting, saveSettings } from './settings';
|
import { changeSetting, saveSettings } from './settings';
|
||||||
|
import { requestBrowserPermission } from './notifications';
|
||||||
|
|
||||||
export const INTRODUCTION_VERSION = 20181216044202;
|
export const INTRODUCTION_VERSION = 20181216044202;
|
||||||
|
|
||||||
export const closeOnboarding = () => dispatch => {
|
export const closeOnboarding = () => dispatch => {
|
||||||
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
||||||
dispatch(saveSettings());
|
dispatch(saveSettings());
|
||||||
|
|
||||||
|
dispatch(requestBrowserPermission((permission) => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'follow'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'mention'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'poll'], true));
|
||||||
|
dispatch(saveSettings());
|
||||||
|
}
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,6 +34,7 @@ class ColumnHeader extends React.PureComponent {
|
||||||
onMove: PropTypes.func,
|
onMove: PropTypes.func,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
appendContent: PropTypes.node,
|
appendContent: PropTypes.node,
|
||||||
|
collapseIssues: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -83,7 +84,7 @@ class ColumnHeader extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent } = this.props;
|
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
|
||||||
const { collapsed, animating } = this.state;
|
const { collapsed, animating } = this.state;
|
||||||
|
|
||||||
const wrapperClassName = classNames('column-header__wrapper', {
|
const wrapperClassName = classNames('column-header__wrapper', {
|
||||||
|
@ -145,7 +146,20 @@ class ColumnHeader extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children || (multiColumn && this.props.onPin)) {
|
if (children || (multiColumn && this.props.onPin)) {
|
||||||
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
|
collapseButton = (
|
||||||
|
<button
|
||||||
|
className={collapsibleButtonClassName}
|
||||||
|
title={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
|
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
|
aria-pressed={collapsed ? 'false' : 'true'}
|
||||||
|
onClick={this.handleToggleClick}
|
||||||
|
>
|
||||||
|
<i className='icon-with-badge'>
|
||||||
|
<Icon id='sliders' />
|
||||||
|
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||||
|
</i>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasTitle = icon && title;
|
const hasTitle = icon && title;
|
||||||
|
|
|
@ -4,16 +4,18 @@ import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
const formatNumber = num => num > 40 ? '40+' : num;
|
const formatNumber = num => num > 40 ? '40+' : num;
|
||||||
|
|
||||||
const IconWithBadge = ({ id, count, className }) => (
|
const IconWithBadge = ({ id, count, issueBadge, className }) => (
|
||||||
<i className='icon-with-badge'>
|
<i className='icon-with-badge'>
|
||||||
<Icon id={id} fixedWidth className={className} />
|
<Icon id={id} fixedWidth className={className} />
|
||||||
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
|
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
|
||||||
|
{issueBadge && <i className='icon-with-badge__issue-badge' />}
|
||||||
</i>
|
</i>
|
||||||
);
|
);
|
||||||
|
|
||||||
IconWithBadge.propTypes = {
|
IconWithBadge.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
count: PropTypes.number.isRequired,
|
count: PropTypes.number.isRequired,
|
||||||
|
issueBadge: PropTypes.bool,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ClearColumnButton from './clear_column_button';
|
import ClearColumnButton from './clear_column_button';
|
||||||
import SettingToggle from './setting_toggle';
|
import SettingToggle from './setting_toggle';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
export default class ColumnSettings extends React.PureComponent {
|
export default class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -12,6 +13,10 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
pushSettings: ImmutablePropTypes.map.isRequired,
|
pushSettings: ImmutablePropTypes.map.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onClear: PropTypes.func.isRequired,
|
onClear: PropTypes.func.isRequired,
|
||||||
|
onRequestNotificationPermission: PropTypes.func.isRequired,
|
||||||
|
alertsEnabled: PropTypes.bool,
|
||||||
|
browserSupport: PropTypes.bool,
|
||||||
|
browserPermission: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
onPushChange = (path, checked) => {
|
onPushChange = (path, checked) => {
|
||||||
|
@ -19,7 +24,7 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { settings, pushSettings, onChange, onClear } = this.props;
|
const { settings, pushSettings, onChange, onClear, onRequestNotificationPermission, alertsEnabled, browserSupport, browserPermission } = this.props;
|
||||||
|
|
||||||
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
||||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||||
|
@ -30,8 +35,40 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||||
|
|
||||||
|
const settingsIssues = [];
|
||||||
|
|
||||||
|
if (alertsEnabled && browserSupport && browserPermission !== 'granted') {
|
||||||
|
if (browserPermission === 'denied') {
|
||||||
|
settingsIssues.push(
|
||||||
|
<button
|
||||||
|
className='text-btn column-header__issue-btn'
|
||||||
|
tabIndex='0'
|
||||||
|
onClick={onRequestNotificationPermission}
|
||||||
|
>
|
||||||
|
<Icon id='exclamation-circle' /> <FormattedMessage id='notifications.permission_denied' defaultMessage='Mastodon cannot show notifications because the permission has been denied' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else if (browserPermission === 'default') {
|
||||||
|
settingsIssues.push(
|
||||||
|
<button
|
||||||
|
className='text-btn column-header__issue-btn'
|
||||||
|
tabIndex='0'
|
||||||
|
onClick={onRequestNotificationPermission}
|
||||||
|
>
|
||||||
|
<Icon id='exclamation-circle' /> <FormattedMessage id='notifications.request_permission' defaultMessage='Enable browser notifications' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{settingsIssues && (
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
{settingsIssues}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<ClearColumnButton onClick={onClear} />
|
<ClearColumnButton onClick={onClear} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,28 +3,55 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ColumnSettings from '../components/column_settings';
|
import ColumnSettings from '../components/column_settings';
|
||||||
import { changeSetting } from '../../../actions/settings';
|
import { changeSetting } from '../../../actions/settings';
|
||||||
import { setFilter } from '../../../actions/notifications';
|
import { setFilter } from '../../../actions/notifications';
|
||||||
import { clearNotifications } from '../../../actions/notifications';
|
import { clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
|
||||||
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
|
import { showAlert } from '../../../actions/alerts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
|
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
|
||||||
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
|
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
|
||||||
|
permissionDenied: { id: 'notifications.permission_denied', defaultMessage: 'Cannot enable desktop notifications as permission has been denied.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
settings: state.getIn(['settings', 'notifications']),
|
settings: state.getIn(['settings', 'notifications']),
|
||||||
pushSettings: state.get('push_notifications'),
|
pushSettings: state.get('push_notifications'),
|
||||||
|
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
||||||
|
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
||||||
|
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onChange (path, checked) {
|
onChange (path, checked) {
|
||||||
if (path[0] === 'push') {
|
if (path[0] === 'push') {
|
||||||
dispatch(changePushNotifications(path.slice(1), checked));
|
if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
|
||||||
|
dispatch(requestBrowserPermission((permission) => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
dispatch(changePushNotifications(path.slice(1), checked));
|
||||||
|
} else {
|
||||||
|
dispatch(showAlert(undefined, messages.permissionDenied));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(changePushNotifications(path.slice(1), checked));
|
||||||
|
}
|
||||||
} else if (path[0] === 'quickFilter') {
|
} else if (path[0] === 'quickFilter') {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
dispatch(setFilter('all'));
|
dispatch(setFilter('all'));
|
||||||
|
} else if (path[0] === 'alerts' && checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
|
||||||
|
if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
|
||||||
|
dispatch(requestBrowserPermission((permission) => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
|
} else {
|
||||||
|
dispatch(showAlert(undefined, messages.permissionDenied));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
}
|
}
|
||||||
|
@ -38,6 +65,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRequestNotificationPermission () {
|
||||||
|
dispatch(requestBrowserPermission());
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
||||||
|
|
|
@ -55,6 +55,7 @@ const mapStateToProps = state => ({
|
||||||
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
|
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
|
||||||
lastReadId: state.getIn(['notifications', 'readMarkerId']),
|
lastReadId: state.getIn(['notifications', 'readMarkerId']),
|
||||||
canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
|
canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
|
||||||
|
needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) !== 'granted',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -75,6 +76,7 @@ class Notifications extends React.PureComponent {
|
||||||
numPending: PropTypes.number,
|
numPending: PropTypes.number,
|
||||||
lastReadId: PropTypes.string,
|
lastReadId: PropTypes.string,
|
||||||
canMarkAsRead: PropTypes.bool,
|
canMarkAsRead: PropTypes.bool,
|
||||||
|
needsNotificationPermission: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -250,6 +252,7 @@ class Notifications extends React.PureComponent {
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={extraButton}
|
extraButton={extraButton}
|
||||||
|
collapseIssues={this.props.needsNotificationPermission}
|
||||||
>
|
>
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import IconWithBadge from 'mastodon/components/icon_with_badge';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
count: state.getIn(['notifications', 'unread']),
|
count: state.getIn(['notifications', 'unread']),
|
||||||
|
issueBadge: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) !== 'granted',
|
||||||
id: 'bell',
|
id: 'bell',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -366,10 +366,6 @@ class UI extends React.PureComponent {
|
||||||
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
|
||||||
window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.dispatch(fetchMarkers());
|
this.props.dispatch(fetchMarkers());
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as registerPushNotifications from './actions/push_notifications';
|
import * as registerPushNotifications from './actions/push_notifications';
|
||||||
|
import { setupBrowserNotifications } from './actions/notifications';
|
||||||
import { default as Mastodon, store } 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';
|
||||||
|
@ -22,6 +23,7 @@ function main() {
|
||||||
const props = JSON.parse(mountNode.getAttribute('data-props'));
|
const props = JSON.parse(mountNode.getAttribute('data-props'));
|
||||||
|
|
||||||
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
||||||
|
store.dispatch(setupBrowserNotifications());
|
||||||
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();
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
NOTIFICATIONS_MOUNT,
|
NOTIFICATIONS_MOUNT,
|
||||||
NOTIFICATIONS_UNMOUNT,
|
NOTIFICATIONS_UNMOUNT,
|
||||||
NOTIFICATIONS_MARK_AS_READ,
|
NOTIFICATIONS_MARK_AS_READ,
|
||||||
|
NOTIFICATIONS_SET_BROWSER_SUPPORT,
|
||||||
|
NOTIFICATIONS_SET_BROWSER_PERMISSION,
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
|
@ -40,6 +42,8 @@ const initialState = ImmutableMap({
|
||||||
readMarkerId: '0',
|
readMarkerId: '0',
|
||||||
isTabVisible: true,
|
isTabVisible: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
browserSupport: false,
|
||||||
|
browserPermission: 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationToMap = notification => ImmutableMap({
|
const notificationToMap = notification => ImmutableMap({
|
||||||
|
@ -242,6 +246,10 @@ export default function notifications(state = initialState, action) {
|
||||||
case NOTIFICATIONS_MARK_AS_READ:
|
case NOTIFICATIONS_MARK_AS_READ:
|
||||||
const lastNotification = state.get('items').find(item => item !== null);
|
const lastNotification = state.get('items').find(item => item !== null);
|
||||||
return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;
|
return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;
|
||||||
|
case NOTIFICATIONS_SET_BROWSER_SUPPORT:
|
||||||
|
return state.set('browserSupport', action.value);
|
||||||
|
case NOTIFICATIONS_SET_BROWSER_PERMISSION:
|
||||||
|
return state.set('browserPermission', action.value);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,12 +29,12 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
notifications: ImmutableMap({
|
notifications: ImmutableMap({
|
||||||
alerts: ImmutableMap({
|
alerts: ImmutableMap({
|
||||||
follow: true,
|
follow: false,
|
||||||
follow_request: false,
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: false,
|
||||||
reblog: true,
|
reblog: false,
|
||||||
mention: true,
|
mention: false,
|
||||||
poll: true,
|
poll: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
quickFilter: ImmutableMap({
|
quickFilter: ImmutableMap({
|
||||||
|
|
29
app/javascript/mastodon/utils/notifications.js
Normal file
29
app/javascript/mastodon/utils/notifications.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Handles browser quirks, based on
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
|
||||||
|
|
||||||
|
const checkNotificationPromise = () => {
|
||||||
|
try {
|
||||||
|
Notification.requestPermission().then();
|
||||||
|
} catch(e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePermission = (permission, callback) => {
|
||||||
|
// Whatever the user answers, we make sure Chrome stores the information
|
||||||
|
if(!('permission' in Notification)) {
|
||||||
|
Notification.permission = permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(Notification.permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestNotificationPermission = (callback) => {
|
||||||
|
if (checkNotificationPromise()) {
|
||||||
|
Notification.requestPermission().then((permission) => handlePermission(permission, callback));
|
||||||
|
} else {
|
||||||
|
Notification.requestPermission((permission) => handlePermission(permission, callback));
|
||||||
|
}
|
||||||
|
};
|
|
@ -2418,6 +2418,17 @@ a.account__display-name {
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__issue-badge {
|
||||||
|
position: absolute;
|
||||||
|
left: 11px;
|
||||||
|
bottom: 1px;
|
||||||
|
display: block;
|
||||||
|
background: $error-red;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-link--transparent .icon-with-badge__badge {
|
.column-link--transparent .icon-with-badge__badge {
|
||||||
|
@ -3453,6 +3464,15 @@ a.status-card.compact:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-header__issue-btn {
|
||||||
|
color: $warning-red;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $error-red;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.column-header__icon {
|
.column-header__icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
Loading…
Reference in a new issue