diff options
Diffstat (limited to 'app/javascript/flavours/glitch')
39 files changed, 495 insertions, 173 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 428b62f68..912a3d179 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -274,11 +274,11 @@ export function unblockAccountFail(error) { }; -export function muteAccount(id, notifications) { +export function muteAccount(id, notifications, duration=0) { return (dispatch, getState) => { dispatch(muteAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index 80bcada6e..c0e7a93af 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -100,8 +100,12 @@ export function submitMarkersSuccess({ home, notifications }) { }; }; -export function submitMarkers() { - return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); +export function submitMarkers(params = {}) { + const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); + if (params.immediate === true) { + debouncedSubmitMarkers.flush(); + } + return result; }; export const fetchMarkers = () => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index 927fc7415..2bacfadb7 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -13,6 +13,7 @@ export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; +export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; export function fetchMutes() { return (dispatch, getState) => { @@ -104,3 +105,12 @@ export function toggleHideNotifications() { dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); }; } + +export function changeMuteDuration(duration) { + return dispatch => { + dispatch({ + type: MUTES_CHANGE_DURATION, + duration, + }); + }; +} diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 7f311153b..eb7087027 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -16,6 +16,7 @@ import { getFiltersRegex } from 'flavours/glitch/selectors'; import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state'; import compareId from 'flavours/glitch/util/compare_id'; import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer'; +import { requestNotificationPermission } from 'flavours/glitch/util/notifications'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -46,8 +47,12 @@ export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY'; + 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({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, }); @@ -327,3 +332,42 @@ export function markNotificationsAsRead() { 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, + }; +} diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index f3e58dfe3..23399c630 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -8,6 +8,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from 'flavours/glitch/util/initial_state'; +import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -116,6 +117,11 @@ class Account extends ImmutablePureComponent { } } + let mute_expires_at; + if (account.get('mute_expires_at')) { + mute_expires_at = <div><RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></div>; + } + return small ? ( <Permalink className='account small' @@ -138,6 +144,7 @@ class Account extends ImmutablePureComponent { <div className='account__wrapper'> <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> + {mute_expires_at} <DisplayName account={account} /> </Permalink> {buttons ? diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.js index c8609e48f..d04c1eb68 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_emoji.js +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light'; -const assetHost = process.env.CDN_HOST || ''; +import { assetHost } from 'flavours/glitch/util/config'; export default class AutosuggestEmoji extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index 01bd4a246..ccd0714f1 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -34,6 +34,7 @@ class ColumnHeader extends React.PureComponent { onMove: PropTypes.func, onClick: PropTypes.func, appendContent: PropTypes.node, + collapseIssues: PropTypes.bool, }; state = { @@ -88,7 +89,7 @@ class ColumnHeader extends React.PureComponent { } 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 wrapperClassName = classNames('column-header__wrapper', { @@ -150,7 +151,20 @@ class ColumnHeader extends React.PureComponent { } 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; diff --git a/app/javascript/flavours/glitch/components/icon_with_badge.js b/app/javascript/flavours/glitch/components/icon_with_badge.js index 219efc28c..a42ba4589 100644 --- a/app/javascript/flavours/glitch/components/icon_with_badge.js +++ b/app/javascript/flavours/glitch/components/icon_with_badge.js @@ -4,16 +4,18 @@ import Icon from 'flavours/glitch/components/icon'; const formatNumber = num => num > 40 ? '40+' : num; -const IconWithBadge = ({ id, count, className }) => ( +const IconWithBadge = ({ id, count, issueBadge, className }) => ( <i className='icon-with-badge'> <Icon id={id} fixedWidth className={className} /> {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>} + {issueBadge && <i className='icon-with-badge__issue-badge' />} </i> ); IconWithBadge.propTypes = { id: PropTypes.string.isRequired, count: PropTypes.number.isRequired, + issueBadge: PropTypes.bool, className: PropTypes.string, }; diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index 8101be87e..762280bec 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -32,13 +32,6 @@ export default class Mastodon extends React.PureComponent { componentDidMount() { this.disconnect = store.dispatch(connectUserStream()); - - // Desktop notifications - // Ask after 1 minute - if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { - window.setTimeout(() => Notification.requestPermission(), 60 * 1000); - } - store.dispatch(showOnboardingOnce()); } diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index d0d9714a8..89219d739 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -13,6 +13,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji'; import { useSystemEmojiFont } from 'flavours/glitch/util/initial_state'; +import { assetHost } from 'flavours/glitch/util/config'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -105,7 +106,6 @@ const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ }, }); -const assetHost = process.env.CDN_HOST || ''; let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js index acaa78fe3..cd81d07de 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -15,6 +15,7 @@ import EmojiPickerDropdown from 'flavours/glitch/features/emoji_picker'; import AnimatedNumber from 'flavours/glitch/components/animated_number'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; +import { assetHost } from 'flavours/glitch/util/config'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -153,8 +154,6 @@ class Content extends ImmutablePureComponent { } -const assetHost = process.env.CDN_HOST || ''; - class Emoji extends React.PureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js index e4d5d0eda..9748219dd 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js @@ -12,6 +12,10 @@ export default class ColumnSettings extends React.PureComponent { pushSettings: ImmutablePropTypes.map.isRequired, onChange: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, + onRequestNotificationPermission: PropTypes.func, + alertsEnabled: PropTypes.bool, + browserSupport: PropTypes.bool, + browserPermission: PropTypes.bool, }; onPushChange = (path, checked) => { @@ -19,7 +23,7 @@ export default class ColumnSettings extends React.PureComponent { } render () { - const { settings, pushSettings, onChange, onClear } = this.props; + const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission } = this.props; 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' />; @@ -33,6 +37,12 @@ export default class ColumnSettings extends React.PureComponent { return ( <div> + {alertsEnabled && browserSupport && browserPermission === 'denied' && ( + <div className='column-settings__row column-settings__row--with-margin'> + <span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span> + </div> + )} + <div className='column-settings__row'> <ClearColumnButton onClick={onClear} /> </div> @@ -41,6 +51,7 @@ export default class ColumnSettings extends React.PureComponent { <span id='notifications-filter-bar' className='column-settings__section'> <FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /> </span> + <div className='column-settings__row'> <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} /> <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} /> @@ -51,7 +62,7 @@ export default class ColumnSettings extends React.PureComponent { <span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} /> + <SettingToggle disabled={browserPermission !== 'granted'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} /> {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} /> @@ -62,7 +73,7 @@ export default class ColumnSettings extends React.PureComponent { <span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} /> + <SettingToggle disabled={browserPermission !== 'granted'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} /> {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} /> @@ -73,7 +84,7 @@ export default class ColumnSettings extends React.PureComponent { <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> + <SettingToggle disabled={browserPermission !== 'granted'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> @@ -84,7 +95,7 @@ export default class ColumnSettings extends React.PureComponent { <span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} /> + <SettingToggle disabled={browserPermission !== 'granted'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} /> {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} /> @@ -95,7 +106,7 @@ export default class ColumnSettings extends React.PureComponent { <span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> + <SettingToggle disabled={browserPermission !== 'granted'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> @@ -106,12 +117,23 @@ export default class ColumnSettings extends React.PureComponent { <span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} /> + <SettingToggle disabled={browserPermission !== 'granted'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} /> {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'poll']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} /> </div> </div> + + <div role='group' aria-labelledby='notifications-status'> + <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New toots:' /></span> + + <div className='column-settings__row'> + <SettingToggle disabled={browserPermission !== 'granted'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} /> + {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'status']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} + <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} /> + <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} /> + </div> + </div> </div> ); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js new file mode 100644 index 000000000..8e77f5a03 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js @@ -0,0 +1,30 @@ +import React from 'react'; +import Icon from 'flavours/glitch/components/icon'; +import Button from 'flavours/glitch/components/button'; +import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +export default @connect(() => {}) +class NotificationsPermissionBanner extends React.PureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + }; + + handleClick = () => { + this.props.dispatch(requestBrowserPermission()); + } + + render () { + return ( + <div className='notifications-permission-banner'> + <h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2> + <p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p> + <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js index 0264b6815..e472f7c4f 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js +++ b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js @@ -13,6 +13,7 @@ export default class SettingToggle extends React.PureComponent { meta: PropTypes.node, onChange: PropTypes.func.isRequired, defaultValue: PropTypes.bool, + disabled: PropTypes.bool, } onChange = ({ target }) => { @@ -20,12 +21,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label, meta, defaultValue } = this.props; + const { prefix, settings, settingPath, label, meta, defaultValue, disabled } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return ( <div className='setting-toggle'> - <Toggle id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> + <Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> <label htmlFor={id} className='setting-toggle__label'>{label}</label> {meta && <span className='setting-meta__label'>{meta}</span>} </div> diff --git a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js index 4b863712a..c2564f44e 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js @@ -3,28 +3,55 @@ import { defineMessages, injectIntl } from 'react-intl'; import ColumnSettings from '../components/column_settings'; import { changeSetting } from 'flavours/glitch/actions/settings'; import { setFilter } from 'flavours/glitch/actions/notifications'; -import { clearNotifications } from 'flavours/glitch/actions/notifications'; +import { clearNotifications, requestBrowserPermission } from 'flavours/glitch/actions/notifications'; import { changeAlerts as changePushNotifications } from 'flavours/glitch/actions/push_notifications'; import { openModal } from 'flavours/glitch/actions/modal'; +import { showAlert } from 'flavours/glitch/actions/alerts'; const messages = defineMessages({ clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }, + permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' }, }); const mapStateToProps = state => ({ settings: state.getIn(['settings', '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 }) => ({ onChange (path, checked) { 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') { dispatch(changeSetting(['notifications', ...path], checked)); 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 { dispatch(changeSetting(['notifications', ...path], checked)); } @@ -38,6 +65,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); }, + onRequestNotificationPermission () { + dispatch(requestBrowserPermission()); + }, + }); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings)); diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 475968caa..af928594d 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -27,6 +27,7 @@ import ScrollableList from 'flavours/glitch/components/scrollable_list'; import LoadGap from 'flavours/glitch/components/load_gap'; import Icon from 'flavours/glitch/components/icon'; import compareId from 'flavours/glitch/util/compare_id'; +import NotificationsPermissionBanner from './components/notifications_permission_banner'; import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; @@ -62,6 +63,7 @@ const mapStateToProps = state => ({ notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), 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), + needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default', }); /* glitch */ @@ -71,7 +73,7 @@ const mapDispatchToProps = dispatch => ({ }, onMarkAsRead() { dispatch(markNotificationsAsRead()); - dispatch(submitMarkers()); + dispatch(submitMarkers({ immediate: true })); }, onMount() { dispatch(mountNotifications()); @@ -105,6 +107,7 @@ class Notifications extends React.PureComponent { onUnmount: PropTypes.func, lastReadId: PropTypes.string, canMarkAsRead: PropTypes.bool, + needsNotificationPermission: PropTypes.bool, }; static defaultProps = { @@ -211,7 +214,7 @@ class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props; const { notifCleaning, notifCleaningActive } = this.props; const { animatingNCD } = this.state; const pinned = !!columnId; @@ -257,6 +260,8 @@ class Notifications extends React.PureComponent { showLoading={isLoading && notifications.size === 0} hasMore={hasMore} numPending={numPending} + prepend={needsNotificationPermission && <NotificationsPermissionBanner />} + alwaysPrepend emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} onLoadPending={this.handleLoadPending} diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js index b790b29a0..5de3e26d5 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js @@ -20,6 +20,7 @@ import GIFV from 'flavours/glitch/components/gifv'; import { me } from 'flavours/glitch/util/initial_state'; import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js'; import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js'; +import { assetHost } from 'flavours/glitch/util/config'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -50,8 +51,6 @@ const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') .replace(/\n/g, ' ') .replace(/\*\*\*\*\*\*/g, '\n\n'); -const assetHost = process.env.CDN_HOST || ''; - class ImageLoader extends React.PureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js index 2aab82751..7d25db316 100644 --- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js @@ -1,25 +1,32 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Toggle from 'react-toggle'; import Button from 'flavours/glitch/components/button'; import { closeModal } from 'flavours/glitch/actions/modal'; import { muteAccount } from 'flavours/glitch/actions/accounts'; -import { toggleHideNotifications } from 'flavours/glitch/actions/mutes'; +import { toggleHideNotifications, changeMuteDuration } from 'flavours/glitch/actions/mutes'; +const messages = defineMessages({ + minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, + hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, + days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, + indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Indefinite' }, +}); const mapStateToProps = state => { return { account: state.getIn(['mutes', 'new', 'account']), notifications: state.getIn(['mutes', 'new', 'notifications']), + muteDuration: state.getIn(['mutes', 'new', 'duration']), }; }; const mapDispatchToProps = dispatch => { return { - onConfirm(account, notifications) { - dispatch(muteAccount(account.get('id'), notifications)); + onConfirm(account, notifications, muteDuration) { + dispatch(muteAccount(account.get('id'), notifications, muteDuration)); }, onClose() { @@ -29,6 +36,10 @@ const mapDispatchToProps = dispatch => { onToggleNotifications() { dispatch(toggleHideNotifications()); }, + + onChangeMuteDuration(e) { + dispatch(changeMuteDuration(e.target.value)); + }, }; }; @@ -43,6 +54,8 @@ class MuteModal extends React.PureComponent { onConfirm: PropTypes.func.isRequired, onToggleNotifications: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + muteDuration: PropTypes.number.isRequired, + onChangeMuteDuration: PropTypes.func.isRequired, }; componentDidMount() { @@ -51,7 +64,7 @@ class MuteModal extends React.PureComponent { handleClick = () => { this.props.onClose(); - this.props.onConfirm(this.props.account, this.props.notifications); + this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration); } handleCancel = () => { @@ -66,8 +79,12 @@ class MuteModal extends React.PureComponent { this.props.onToggleNotifications(); } + changeMuteDuration = (e) => { + this.props.onChangeMuteDuration(e); + } + render () { - const { account, notifications } = this.props; + const { account, notifications, muteDuration, intl } = this.props; return ( <div className='modal-root__modal mute-modal'> @@ -91,6 +108,21 @@ class MuteModal extends React.PureComponent { <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> </label> </div> + <div> + <span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span> + + {/* eslint-disable-next-line jsx-a11y/no-onchange */} + <select value={muteDuration} onChange={this.changeMuteDuration}> + <option value={0}>{intl.formatMessage(messages.indefinite)}</option> + <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> + <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> + <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option> + <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option> + <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option> + <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option> + <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option> + </select> + </div> </div> <div className='mute-modal__action-bar'> diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index a399fc2b3..1294a8a16 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -359,7 +359,7 @@ class UI extends React.Component { const visibility = !document[this.visibilityHiddenProp]; this.props.dispatch(notificationsSetVisibility(visibility)); if (visibility) { - this.props.dispatch(submitMarkers()); + this.props.dispatch(submitMarkers({ immediate: true })); } } @@ -385,7 +385,7 @@ class UI extends React.Component { componentDidMount () { this.hotkeys.__mousetrap__.stopCallback = (e, element) => { - return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName) && !e.altKey; + return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); }; if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support diff --git a/app/javascript/flavours/glitch/packs/about.js b/app/javascript/flavours/glitch/packs/about.js index bc0a4887b..2e2cce501 100644 --- a/app/javascript/flavours/glitch/packs/about.js +++ b/app/javascript/flavours/glitch/packs/about.js @@ -1,3 +1,4 @@ +import 'packs/public-path'; import loadPolyfills from 'flavours/glitch/util/load_polyfills'; function loaded() { diff --git a/app/javascript/flavours/glitch/packs/common.js b/app/javascript/flavours/glitch/packs/common.js index 1fedc890a..7dc34eba9 100644 --- a/app/javascript/flavours/glitch/packs/common.js +++ b/app/javascript/flavours/glitch/packs/common.js @@ -1,3 +1,4 @@ +import 'packs/public-path'; import { start } from '@rails/ujs'; start(); diff --git a/app/javascript/flavours/glitch/packs/error.js b/app/javascript/flavours/glitch/packs/error.js index 81c86c3ab..9f692ad37 100644 --- a/app/javascript/flavours/glitch/packs/error.js +++ b/app/javascript/flavours/glitch/packs/error.js @@ -1,3 +1,4 @@ +import 'packs/public-path'; import ready from 'flavours/glitch/util/ready'; ready(() => { diff --git a/app/javascript/flavours/glitch/packs/home.js b/app/javascript/flavours/glitch/packs/home.js index b8f7b7d8e..d06688985 100644 --- a/app/javascript/flavours/glitch/packs/home.js +++ b/app/javascript/flavours/glitch/packs/home.js @@ -1,3 +1,4 @@ +import 'packs/public-path'; import loadPolyfills from 'flavours/glitch/util/load_polyfills'; loadPolyfills().then(() => { diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index b9b588dc6..c74e5c9af 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -1,3 +1,4 @@ +import 'packs/public-path'; import loadPolyfills from 'flavours/glitch/util/load_polyfills'; import ready from 'flavours/glitch/util/ready'; import loadKeyboardExtensions from 'flavours/glitch/util/load_keyboard_extensions'; diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js index 8a9f23505..9c4d119c1 100644 --- a/app/javascript/flavours/glitch/packs/settings.js +++ b/app/javascript/flavours/glitch/packs/settings.js @@ -1,3 +1,4 @@ +import 'packs/public-path'; import loadPolyfills from 'flavours/glitch/util/load_polyfills'; import ready from 'flavours/glitch/util/ready'; import loadKeyboardExtensions from 'flavours/glitch/util/load_keyboard_extensions'; diff --git a/app/javascript/flavours/glitch/packs/share.js b/app/javascript/flavours/glitch/packs/share.js index 9f2aa2553..f4a97e201 100644 --- a/app/javascript/flavours/glitch/packs/share.js +++ b/app/javascript/flavours/glitch/packs/share.js @@ -1,3 +1,4 @@ +import 'packs/public-path'; import loadPolyfills from 'flavours/glitch/util/load_polyfills'; function loaded() { diff --git a/app/javascript/flavours/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js index 7111bb710..d346d9a78 100644 --- a/app/javascript/flavours/glitch/reducers/mutes.js +++ b/app/javascript/flavours/glitch/reducers/mutes.js @@ -3,12 +3,14 @@ import Immutable from 'immutable'; import { MUTES_INIT_MODAL, MUTES_TOGGLE_HIDE_NOTIFICATIONS, + MUTES_CHANGE_DURATION, } from 'flavours/glitch/actions/mutes'; const initialState = Immutable.Map({ new: Immutable.Map({ account: null, notifications: true, + duration: 0, }), }); @@ -21,6 +23,8 @@ export default function mutes(state = initialState, action) { }); case MUTES_TOGGLE_HIDE_NOTIFICATIONS: return state.updateIn(['new', 'notifications'], (old) => !old); + case MUTES_CHANGE_DURATION: + return state.setIn(['new', 'duration'], Number(action.duration)); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index e136369ae..b4c5ef71a 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -17,6 +17,8 @@ import { NOTIFICATIONS_ENTER_CLEARING_MODE, NOTIFICATIONS_MARK_ALL_FOR_DELETE, NOTIFICATIONS_MARK_AS_READ, + NOTIFICATIONS_SET_BROWSER_SUPPORT, + NOTIFICATIONS_SET_BROWSER_PERMISSION, } from 'flavours/glitch/actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -44,6 +46,8 @@ const initialState = ImmutableMap({ isLoading: false, cleaningMode: false, isTabVisible: true, + browserSupport: false, + browserPermission: 'default', // notification removal mark of new notifs loaded whilst cleaningMode is true. markNewForDelete: false, }); @@ -185,7 +189,7 @@ const deleteMarkedNotifs = (state) => { const updateMounted = (state) => { state = state.update('mounted', count => count + 1); - if (!shouldCountUnreadNotifications(state)) { + if (!shouldCountUnreadNotifications(state, state.get('mounted') === 1)) { state = state.set('readMarkerId', state.get('lastReadId')); state = clearUnread(state); } @@ -201,7 +205,7 @@ const updateVisibility = (state, visibility) => { return state; }; -const shouldCountUnreadNotifications = (state) => { +const shouldCountUnreadNotifications = (state, ignoreScroll = false) => { const isTabVisible = state.get('isTabVisible'); const isOnTop = state.get('top'); const isMounted = state.get('mounted') > 0; @@ -209,7 +213,7 @@ const shouldCountUnreadNotifications = (state) => { const lastItem = state.get('items').findLast(item => item !== null); const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (lastItem && compareId(lastItem.get('id'), lastReadId) <= 0); - return !(isTabVisible && isOnTop && isMounted && lastItemReached); + return !(isTabVisible && (ignoreScroll || isOnTop) && isMounted && lastItemReached); }; const recountUnread = (state, last_read_id) => { @@ -275,6 +279,10 @@ export default function notifications(state = initialState, action) { return action.timeline === 'home' ? state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; + case NOTIFICATIONS_SET_BROWSER_SUPPORT: + return state.set('browserSupport', action.value); + case NOTIFICATIONS_SET_BROWSER_PERMISSION: + return state.set('browserPermission', action.value); case NOTIFICATION_MARK_FOR_DELETE: return markForDelete(state, action.id, action.yes); diff --git a/app/javascript/flavours/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js index 33eb5b425..49dd77ef5 100644 --- a/app/javascript/flavours/glitch/reducers/relationships.js +++ b/app/javascript/flavours/glitch/reducers/relationships.js @@ -45,7 +45,7 @@ const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { case ACCOUNT_FOLLOW_REQUEST: - return state.setIn([action.id, action.locked ? 'requested' : 'following'], true); + return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true); case ACCOUNT_FOLLOW_FAIL: return state.setIn([action.id, action.locked ? 'requested' : 'following'], false); case ACCOUNT_UNFOLLOW_REQUEST: diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index ef99ad552..bf0545c48 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -33,12 +33,13 @@ const initialState = ImmutableMap({ notifications: ImmutableMap({ alerts: ImmutableMap({ - follow: true, + follow: false, follow_request: false, - favourite: true, - reblog: true, - mention: true, - poll: true, + favourite: false, + reblog: false, + mention: false, + poll: false, + status: false, }), quickFilter: ImmutableMap({ @@ -54,6 +55,7 @@ const initialState = ImmutableMap({ reblog: true, mention: true, poll: true, + status: true, }), sounds: ImmutableMap({ @@ -63,6 +65,7 @@ const initialState = ImmutableMap({ reblog: true, mention: true, poll: true, + status: true, }), }), diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 145219fa7..c0bbe5633 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -254,127 +254,6 @@ text-align: center; } -.column-settings__outer { - background: lighten($ui-base-color, 8%); - padding: 15px; -} - -.column-settings__section { - color: $darker-text-color; - cursor: default; - display: block; - font-weight: 500; - margin-bottom: 10px; -} - -.column-settings__hashtags { - .column-settings__row { - margin-bottom: 15px; - } - - .column-select { - &__control { - @include search-input(); - - &::placeholder { - color: lighten($darker-text-color, 4%); - } - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - - &:focus { - background: lighten($ui-base-color, 4%); - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } - } - - &__placeholder { - color: $dark-text-color; - padding-left: 2px; - font-size: 12px; - } - - &__value-container { - padding-left: 6px; - } - - &__multi-value { - background: lighten($ui-base-color, 8%); - - &__remove { - cursor: pointer; - - &:hover, - &:active, - &:focus { - background: lighten($ui-base-color, 12%); - color: lighten($darker-text-color, 4%); - } - } - } - - &__multi-value__label, - &__input { - color: $darker-text-color; - } - - &__clear-indicator, - &__dropdown-indicator { - cursor: pointer; - transition: none; - color: $dark-text-color; - - &:hover, - &:active, - &:focus { - color: lighten($dark-text-color, 4%); - } - } - - &__indicator-separator { - background-color: lighten($ui-base-color, 8%); - } - - &__menu { - @include search-popout(); - padding: 0; - background: $ui-secondary-color; - } - - &__menu-list { - padding: 6px; - } - - &__option { - color: $inverted-text-color; - border-radius: 4px; - font-size: 14px; - - &--is-focused, - &--is-selected { - background: darken($ui-secondary-color, 10%); - } - } - } -} - -.column-settings__row { - .text-btn { - margin-bottom: 15px; - } -} - .relationship-tag { color: $primary-text-color; margin-bottom: 4px; diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 6b657660a..be32ae52e 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -463,6 +463,15 @@ flex: 1; } +.column-header__issue-btn { + color: $warning-red; + + &:hover { + color: $error-red; + text-decoration: underline; + } +} + .column-header__icon { display: inline-block; margin-right: 5px; @@ -560,3 +569,150 @@ margin: 0 5px; } } + +.column-settings__outer { + background: lighten($ui-base-color, 8%); + padding: 15px; +} + +.column-settings__section { + color: $darker-text-color; + cursor: default; + display: block; + font-weight: 500; + margin-bottom: 10px; +} + +.column-settings__row--with-margin { + margin-bottom: 15px; +} + +.column-settings__hashtags { + .column-settings__row { + margin-bottom: 15px; + } + + .column-select { + &__control { + @include search-input(); + + &::placeholder { + color: lighten($darker-text-color, 4%); + } + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus, + &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($ui-base-color, 4%); + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } + } + + &__placeholder { + color: $dark-text-color; + padding-left: 2px; + font-size: 12px; + } + + &__value-container { + padding-left: 6px; + } + + &__multi-value { + background: lighten($ui-base-color, 8%); + + &__remove { + cursor: pointer; + + &:hover, + &:active, + &:focus { + background: lighten($ui-base-color, 12%); + color: lighten($darker-text-color, 4%); + } + } + } + + &__multi-value__label, + &__input { + color: $darker-text-color; + } + + &__clear-indicator, + &__dropdown-indicator { + cursor: pointer; + transition: none; + color: $dark-text-color; + + &:hover, + &:active, + &:focus { + color: lighten($dark-text-color, 4%); + } + } + + &__indicator-separator { + background-color: lighten($ui-base-color, 8%); + } + + &__menu { + @include search-popout(); + padding: 0; + background: $ui-secondary-color; + } + + &__menu-list { + padding: 6px; + } + + &__option { + color: $inverted-text-color; + border-radius: 4px; + font-size: 14px; + + &--is-focused, + &--is-selected { + background: darken($ui-secondary-color, 10%); + } + } + } +} + +.column-settings__row { + .text-btn { + margin-bottom: 15px; + } +} + +.notifications-permission-banner { + padding: 30px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h2 { + font-size: 16px; + font-weight: 500; + margin-bottom: 15px; + text-align: center; + } + + p { + color: $darker-text-color; + margin-bottom: 15px; + text-align: center; + } +} diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 04266c497..56d658d97 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -708,6 +708,17 @@ line-height: 14px; 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 { diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index d0be730ac..85f216887 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -785,6 +785,22 @@ } } } + + select { + appearance: none; + box-sizing: border-box; + font-size: 14px; + color: $inverted-text-color; + display: inline-block; + width: auto; + outline: 0; + font-family: inherit; + background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>") no-repeat right 8px center / auto 16px; + border: 1px solid darken($simple-background-color, 14%); + border-radius: 4px; + padding: 6px 10px; + padding-right: 30px; + } } .confirmation-modal__container, diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index e5a5cc246..163b5220c 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -385,3 +385,8 @@ .directory__tag > div { box-shadow: none; } + +.mute-modal select { + border: 1px solid lighten($ui-base-color, 8%); + background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px; +} diff --git a/app/javascript/flavours/glitch/util/config.js b/app/javascript/flavours/glitch/util/config.js new file mode 100644 index 000000000..c3e2b73ae --- /dev/null +++ b/app/javascript/flavours/glitch/util/config.js @@ -0,0 +1,10 @@ +import ready from './ready'; + +export let assetHost = ''; + +ready(() => { + const cdnHost = document.querySelector('meta[name=cdn-host]'); + if (cdnHost) { + assetHost = cdnHost.content || ''; + } +}); diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index 233ec25e3..43ff4661c 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -1,11 +1,10 @@ import { autoPlayGif, useSystemEmojiFont } from 'flavours/glitch/util/initial_state'; import unicodeMapping from './emoji_unicode_mapping_light'; +import { assetHost } from 'flavours/glitch/util/config'; import Trie from 'substring-trie'; const trie = new Trie(Object.keys(unicodeMapping)); -const assetHost = process.env.CDN_HOST || ''; - // Convert to file names from emojis. (For different variation selector emojis) const emojiFilenames = (emojis) => { return emojis.map(v => unicodeMapping[v].filename); diff --git a/app/javascript/flavours/glitch/util/main.js b/app/javascript/flavours/glitch/util/main.js index 1fdb9ff2b..6577b70c2 100644 --- a/app/javascript/flavours/glitch/util/main.js +++ b/app/javascript/flavours/glitch/util/main.js @@ -1,4 +1,5 @@ import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications'; +import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -22,6 +23,7 @@ function main() { const props = JSON.parse(mountNode.getAttribute('data-props')); ReactDOM.render(<Mastodon {...props} />, mountNode); + store.dispatch(setupBrowserNotifications()); if (process.env.NODE_ENV === 'production') { // avoid offline in dev mode because it's harder to debug require('offline-plugin/runtime').install(); diff --git a/app/javascript/flavours/glitch/util/notifications.js b/app/javascript/flavours/glitch/util/notifications.js new file mode 100644 index 000000000..ab119c2e3 --- /dev/null +++ b/app/javascript/flavours/glitch/util/notifications.js @@ -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)); + } +}; |