diff options
Diffstat (limited to 'app/javascript')
62 files changed, 671 insertions, 144 deletions
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index bce836b45..4b8e9e50d 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -2,8 +2,6 @@ import api from '../api'; import { updateTimeline } from './timelines'; -import * as emojione from 'emojione'; - export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; @@ -74,10 +72,12 @@ export function mentionCompose(account, router) { export function submitCompose() { return function (dispatch, getState) { - let status = emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')); + const status = getState().getIn(['compose', 'text'], ''); + if (!status || !status.length) { return; } + dispatch(submitComposeRequest()); if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) { status = status + ' 👁️'; diff --git a/app/javascript/mastodon/actions/push_notifications.js b/app/javascript/mastodon/actions/push_notifications.js new file mode 100644 index 000000000..55661d2b0 --- /dev/null +++ b/app/javascript/mastodon/actions/push_notifications.js @@ -0,0 +1,52 @@ +import axios from 'axios'; + +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'); + + axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { + data: { + alerts, + }, + }); + }; +} diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js index 4c62fa7b3..b38a4b8ff 100644 --- a/app/javascript/mastodon/components/extended_video_player.js +++ b/app/javascript/mastodon/components/extended_video_player.js @@ -5,6 +5,8 @@ export default class ExtendedVideoPlayer extends React.PureComponent { static propTypes = { src: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, time: PropTypes.number, controls: PropTypes.bool.isRequired, muted: PropTypes.bool.isRequired, @@ -30,7 +32,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent { render () { return ( - <div className='extended-video-player'> + <div className='extended-video-player' style={{ width: this.props.width, height: this.props.height }}> <video ref={this.setRef} src={this.props.src} diff --git a/app/javascript/mastodon/components/load_more.js b/app/javascript/mastodon/components/load_more.js index 2996d4dc8..e2fe1fed7 100644 --- a/app/javascript/mastodon/components/load_more.js +++ b/app/javascript/mastodon/components/load_more.js @@ -6,11 +6,18 @@ export default class LoadMore extends React.PureComponent { static propTypes = { onClick: PropTypes.func, + visible: PropTypes.bool, + } + + static defaultProps = { + visible: true, } render() { + const { visible } = this.props; + return ( - <button className='load-more' onClick={this.props.onClick}> + <button className='load-more' disabled={!visible} style={{ opacity: visible ? 1 : 0 }} onClick={this.props.onClick}> <FormattedMessage id='status.load_more' defaultMessage='Load more' /> </button> ); diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 94b348f25..e7b38a07a 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -101,13 +101,9 @@ export default class StatusList extends ImmutablePureComponent { render () { const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; - let loadMore = null; + const loadMore = <LoadMore visible={!isLoading && statusIds.size > 0 && hasMore} onClick={this.handleLoadMore} />; let scrollableArea = null; - if (!isLoading && statusIds.size > 0 && hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - if (isLoading || statusIds.size > 0 || !emptyMessage) { scrollableArea = ( <div className='scrollable' ref={this.setRef}> diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index 7043d5f3a..1de41f572 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -1,49 +1,28 @@ -import emojione from 'emojione'; +import { unicodeToFilename } from './emojione_light'; import Trie from 'substring-trie'; -const mappedUnicode = emojione.mapUnicodeToShort(); -const trie = new Trie(Object.keys(emojione.jsEscapeMap)); +const trie = new Trie(Object.keys(unicodeToFilename)); function emojify(str) { // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) - // and replacing valid shortnames like :smile: and :wink: as well as unicode strings + // and replacing valid unicode strings // that _aren't_ within tags with an <img> version. - // The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster. + // The goal is to be the same as an emojione.regUnicode replacement, but faster. let i = -1; let insideTag = false; - let insideShortname = false; - let shortnameStartIndex = -1; let match; while (++i < str.length) { const char = str.charAt(i); - if (insideShortname && char === ':') { - const shortname = str.substring(shortnameStartIndex, i + 1); - if (shortname in emojione.emojioneList) { - const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; - const alt = emojione.convert(unicode.toUpperCase()); - const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`; - str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1); - i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string - } else { - i--; // stray colon, try again - } - insideShortname = false; - } else if (insideTag && char === '>') { + if (insideTag && char === '>') { insideTag = false; } else if (char === '<') { insideTag = true; - insideShortname = false; - } else if (!insideTag && char === ':') { - insideShortname = true; - shortnameStartIndex = i; } else if (!insideTag && (match = trie.search(str.substring(i)))) { const unicodeStr = match; - if (unicodeStr in emojione.jsEscapeMap) { - const unicode = emojione.jsEscapeMap[unicodeStr]; - const short = mappedUnicode[unicode]; - const filename = emojione.emojioneList[short].fname; - const alt = emojione.convert(unicode.toUpperCase()); - const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`; + if (unicodeStr in unicodeToFilename) { + const filename = unicodeToFilename[unicodeStr]; + const alt = unicodeStr; + const replacement = `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${filename}.svg" />`; str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string } diff --git a/app/javascript/mastodon/emojione_light.js b/app/javascript/mastodon/emojione_light.js new file mode 100644 index 000000000..c75e10a98 --- /dev/null +++ b/app/javascript/mastodon/emojione_light.js @@ -0,0 +1,11 @@ +// @preval +// Force tree shaking on emojione by exposing just a subset of its functionality + +const emojione = require('emojione'); + +const mappedUnicode = emojione.mapUnicodeToShort(); + +module.exports.unicodeToFilename = Object.keys(emojione.jsEscapeMap) + .map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]]) + .map(([unicodeStr, shortCode]) => ({ [unicodeStr]: emojione.emojioneList[shortCode].fname })) + .reduce((x, y) => Object.assign(x, y), { }); diff --git a/app/javascript/mastodon/extra_polyfills.js b/app/javascript/mastodon/extra_polyfills.js index 546b693b1..3acc55abd 100644 --- a/app/javascript/mastodon/extra_polyfills.js +++ b/app/javascript/mastodon/extra_polyfills.js @@ -1,2 +1,5 @@ import 'intersection-observer'; import 'requestidlecallback'; +import objectFitImages from 'object-fit-images'; + +objectFitImages(); diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index de5b09834..7273edf48 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -140,7 +140,8 @@ export default class ComposeForm extends ImmutablePureComponent { handleEmojiPick = (data) => { const position = this.autosuggestTextarea.textarea.selectionStart; - this._restoreCaret = position + data.shortname.length + 1; + const emojiChar = String.fromCodePoint(parseInt(data.unicode, 16)); + this._restoreCaret = position + emojiChar.length + 1; this.props.onPickEmoji(position, data); } diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index 83c66a5d5..acc584f20 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -109,11 +109,12 @@ export default class EmojiPickerDropdown extends React.PureComponent { <Dropdown ref={this.setRef} className='emoji-picker__dropdown' onShow={this.onShowDropdown} onHide={this.onHideDropdown}> <DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)}> <img - draggable='false' className={`emojione ${active && loading ? 'pulse-loading' : ''}`} - alt='🙂' src='/emoji/1f602.svg' + alt='🙂' + src='/emoji/1f602.svg' /> </DropdownTrigger> + <DropdownContent className='dropdown__left'> { this.state.active && !this.state.loading && diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 8cef6a1e4..d9ad9bc1f 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -2,11 +2,11 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites'; import Column from '../ui/components/column'; +import ColumnHeader from '../../components/column_header'; +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import StatusList from '../../components/status_list'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -16,8 +16,6 @@ const messages = defineMessages({ const mapStateToProps = state => ({ statusIds: state.getIn(['status_lists', 'favourites', 'items']), - loaded: state.getIn(['status_lists', 'favourites', 'loaded']), - me: state.getIn(['meta', 'me']), }); @connect(mapStateToProps) @@ -27,34 +25,64 @@ export default class Favourites extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list.isRequired, - loaded: PropTypes.bool, intl: PropTypes.object.isRequired, - me: PropTypes.number.isRequired, + columnId: PropTypes.string, + multiColumn: PropTypes.bool, }; componentWillMount () { this.props.dispatch(fetchFavouritedStatuses()); } + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('FAVOURITES', {})); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + handleScrollToBottom = () => { this.props.dispatch(expandFavouritedStatuses()); } render () { - const { loaded, intl } = this.props; - - if (!loaded) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } + const { intl, statusIds, columnId, multiColumn } = this.props; + const pinned = !!columnId; return ( - <Column icon='star' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <StatusList {...this.props} scrollKey='favourited_statuses' onScrollToBottom={this.handleScrollToBottom} /> + <Column ref={this.setRef}> + <ColumnHeader + icon='star' + title={intl.formatMessage(messages.heading)} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + /> + + <StatusList + trackScroll={!pinned} + statusIds={statusIds} + scrollKey={`favourited_statuses-${columnId}`} + onScrollToBottom={this.handleScrollToBottom} + /> </Column> ); } diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 260594894..31cac5bc7 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -9,18 +9,27 @@ export default class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, + pushSettings: ImmutablePropTypes.map.isRequired, onChange: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, }; + onPushChange = (key, checked) => { + this.props.onChange(['push', ...key], checked); + } + render () { - const { settings, onChange, onClear } = this.props; + const { settings, pushSettings, onChange, onClear } = this.props; const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; + const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); + const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; + const pushMeta = showPushSettings && <FormattedMessage id='notifications.column_settings.push_meta' defaultMessage='This device' />; + return ( <div> <div className='column-settings__row'> @@ -30,7 +39,8 @@ export default class ColumnSettings extends React.PureComponent { <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} /> + <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} /> + {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} /> </div> @@ -38,7 +48,8 @@ export default class ColumnSettings extends React.PureComponent { <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> + <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> + {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> </div> @@ -46,7 +57,8 @@ export default class ColumnSettings extends React.PureComponent { <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} /> + <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} /> + {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} /> </div> @@ -54,7 +66,8 @@ export default class ColumnSettings extends React.PureComponent { <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> <div className='column-settings__row'> - <SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> + <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> + {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> </div> diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index 510820358..be1ff91d6 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -10,6 +10,7 @@ export default class SettingToggle extends React.PureComponent { settings: ImmutablePropTypes.map.isRequired, settingKey: PropTypes.array.isRequired, label: PropTypes.node.isRequired, + meta: PropTypes.node, onChange: PropTypes.func.isRequired, } @@ -18,13 +19,14 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingKey, label } = this.props; + const { prefix, settings, settingKey, label, meta } = this.props; const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-'); return ( <div className='setting-toggle'> <Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} /> <label htmlFor={id} className='setting-toggle__label'>{label}</label> + {meta && <span className='setting-meta__label'>{meta}</span>} </div> ); } diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index b139d4615..d4ead7881 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -3,6 +3,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import ColumnSettings from '../components/column_settings'; import { changeSetting, saveSettings } from '../../../actions/settings'; import { clearNotifications } from '../../../actions/notifications'; +import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from '../../../actions/push_notifications'; import { openModal } from '../../../actions/modal'; const messages = defineMessages({ @@ -12,16 +13,22 @@ const messages = defineMessages({ const mapStateToProps = state => ({ settings: state.getIn(['settings', 'notifications']), + pushSettings: state.get('push_notifications'), }); const mapDispatchToProps = (dispatch, { intl }) => ({ onChange (key, checked) { - dispatch(changeSetting(['notifications', ...key], checked)); + if (key[0] === 'push') { + dispatch(changePushNotifications(key.slice(1), checked)); + } else { + dispatch(changeSetting(['notifications', ...key], checked)); + } }, onSave () { dispatch(saveSettings()); + dispatch(savePushNotificationSettings()); }, onClear () { diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index cbc185a7d..515c377b9 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -9,7 +9,7 @@ import { links, getIndex, getLink } from './tabs_bar'; import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components'; +import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; const componentMap = { 'COMPOSE': Compose, @@ -18,6 +18,7 @@ const componentMap = { 'PUBLIC': PublicTimeline, 'COMMUNITY': CommunityTimeline, 'HASHTAG': HashtagTimeline, + 'FAVOURITES': FavouritedStatuses, }; export default class ColumnsArea extends ImmutablePureComponent { @@ -32,12 +33,33 @@ export default class ColumnsArea extends ImmutablePureComponent { children: PropTypes.node, }; + state = { + shouldAnimate: false, + } + + componentWillReceiveProps() { + this.setState({ shouldAnimate: false }); + } + + componentDidMount() { + this.lastIndex = getIndex(this.context.router.history.location.pathname); + this.setState({ shouldAnimate: true }); + } + + componentDidUpdate() { + this.lastIndex = getIndex(this.context.router.history.location.pathname); + this.setState({ shouldAnimate: true }); + } + handleSwipe = (index) => { - window.requestAnimationFrame(() => { - window.requestAnimationFrame(() => { - this.context.router.history.push(getLink(index)); - }); - }); + this.pendingIndex = index; + } + + handleAnimationEnd = () => { + if (typeof this.pendingIndex === 'number') { + this.context.router.history.push(getLink(this.pendingIndex)); + this.pendingIndex = null; + } } renderView = (link, index) => { @@ -66,12 +88,14 @@ export default class ColumnsArea extends ImmutablePureComponent { render () { const { columns, children, singleColumn } = this.props; + const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); + this.pendingIndex = null; if (singleColumn) { return columnIndex !== -1 ? ( - <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} animateTransitions={false} style={{ height: '100%' }}> + <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> {links.map(this.renderView)} </ReactSwipeableViews> ) : <div className='columns-area'>{children}</div>; diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index d869fffa6..dcc9becd3 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -65,8 +65,6 @@ export default class MediaModal extends ImmutablePureComponent { const { media, intl, onClose } = this.props; const index = this.getIndex(); - const attachment = media.get(index); - const url = attachment.get('url'); let leftNav, rightNav, content; @@ -77,16 +75,18 @@ export default class MediaModal extends ImmutablePureComponent { rightNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; } - if (attachment.get('type') === 'image') { - content = media.map((image) => { - const width = image.getIn(['meta', 'original', 'width']) || null; - const height = image.getIn(['meta', 'original', 'height']) || null; + content = media.map((image) => { + const width = image.getIn(['meta', 'original', 'width']) || null; + const height = image.getIn(['meta', 'original', 'height']) || null; + if (image.get('type') === 'image') { return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} key={image.get('preview_url')} />; - }).toArray(); - } else if (attachment.get('type') === 'gifv') { - content = <ExtendedVideoPlayer src={url} muted controls={false} />; - } + } else if (image.get('type') === 'gifv') { + return <ExtendedVideoPlayer src={image.get('url')} muted controls={false} width={width} height={height} key={image.get('preview_url')} />; + } + + return null; + }).toArray(); return ( <div className='modal-root__modal media-modal'> diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index de4f44ce6..84461d9b5 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -56,12 +56,6 @@ export default class ModalRoot extends React.PureComponent { return { opacity: spring(0), scale: spring(0.98) }; } - renderModal = (SpecificComponent) => { - const { props, onClose } = this.props; - - return <SpecificComponent {...props} onClose={onClose} />; - } - renderLoading = () => { return <ModalLoading />; } @@ -97,7 +91,9 @@ export default class ModalRoot extends React.PureComponent { <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} /> <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> - <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>{this.renderModal}</BundleContainer> + <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}> + {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} + </BundleContainer> </div> </div> ))} diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js index bc5468595..df7889118 100644 --- a/app/javascript/mastodon/load_polyfills.js +++ b/app/javascript/mastodon/load_polyfills.js @@ -20,11 +20,12 @@ function loadPolyfills() { ); // Latest version of Firefox and Safari do not have IntersectionObserver. - // Edge does not have requestIdleCallback. + // Edge does not have requestIdleCallback and object-fit CSS property. // This avoids shipping them all the polyfills. const needsExtraPolyfills = !( window.IntersectionObserver && - window.requestIdleCallback + window.requestIdleCallback && + 'object-fit' in (new Image()).style ); return Promise.all([ diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 6992e7e0f..7b890ce64 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "المُفَضَّلة :", "notifications.column_settings.follow": "متابعُون جُدُد :", "notifications.column_settings.mention": "الإشارات :", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "الترقيّات:", "notifications.column_settings.show": "إعرِضها في عمود", "notifications.column_settings.sound": "أصدر صوتا", @@ -147,6 +149,7 @@ "report.target": "إبلاغ", "search.placeholder": "ابحث", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "تعذرت ترقية هذا المنشور", "status.delete": "إحذف", "status.favourite": "أضف إلى المفضلة", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 7a56e1446..0cf6bf3ac 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Предпочитани:", "notifications.column_settings.follow": "Нови последователи:", "notifications.column_settings.mention": "Споменавания:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Споделяния:", "notifications.column_settings.show": "Покажи в колона", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Търсене", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Изтриване", "status.favourite": "Предпочитани", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index b2673915a..1e44d6fa5 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorits:", "notifications.column_settings.follow": "Nous seguidors:", "notifications.column_settings.mention": "Mencions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Mostrar en la columna", "notifications.column_settings.sound": "Reproduïr so", @@ -147,6 +149,7 @@ "report.target": "Informes", "search.placeholder": "Cercar", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Aquesta publicació no pot ser retootejada", "status.delete": "Esborrar", "status.favourite": "Favorit", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 4b62403c3..f73011e73 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorisierungen:", "notifications.column_settings.follow": "Neue Folgende:", "notifications.column_settings.mention": "Erwähnungen:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.show": "In der Spalte anzeigen", "notifications.column_settings.sound": "Ton abspielen", @@ -147,6 +149,7 @@ "report.target": "Melden", "search.placeholder": "Suche", "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Löschen", "status.favourite": "Favorisieren", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 36d82ec1a..368f68193 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -890,6 +890,14 @@ "id": "notifications.column_settings.sound" }, { + "defaultMessage": "Push notifications", + "id": "notifications.column_settings.push" + }, + { + "defaultMessage": "This device", + "id": "notifications.column_settings.push_meta" + }, + { "defaultMessage": "New followers:", "id": "notifications.column_settings.follow" }, @@ -967,6 +975,15 @@ { "descriptors": [ { + "defaultMessage": "A look inside...", + "id": "standalone.public_title" + } + ], + "path": "app/javascript/mastodon/features/standalone/public_timeline/index.json" + }, + { + "descriptors": [ + { "defaultMessage": "Delete", "id": "status.delete" }, diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index d2e5f90ea..1d553d514 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -114,6 +114,8 @@ "notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.follow": "New followers:", "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -170,6 +172,7 @@ "settings.media_fullwidth": "Full-width media previews", "settings.preferences": "User preferences", "settings.wide_view": "Wide view (Desktop mode only)", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.collapse": "Collapse", "status.delete": "Delete", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 2648a6840..4f9e26c25 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoroj:", "notifications.column_settings.follow": "Novaj sekvantoj:", "notifications.column_settings.mention": "Mencioj:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Diskonigoj:", "notifications.column_settings.show": "Montri en kolono", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Serĉi", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Forigi", "status.favourite": "Favori", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index c42930380..64ba78716 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.mention": "Menciones:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Buscar", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Borrar", "status.favourite": "Favorito", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index c9f1888b5..306937cc2 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "پسندیدهها:", "notifications.column_settings.follow": "پیگیران تازه:", "notifications.column_settings.mention": "نامبردنها:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "بازبوقها:", "notifications.column_settings.show": "نمایش در ستون", "notifications.column_settings.sound": "پخش صدا", @@ -147,6 +149,7 @@ "report.target": "گزارشدادن", "search.placeholder": "جستجو", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "این نوشته را نمیشود بازبوقید", "status.delete": "پاککردن", "status.favourite": "پسندیدن", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index b836d2f5d..1b17fb155 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Tykkäyksiä:", "notifications.column_settings.follow": "Uusia seuraajia:", "notifications.column_settings.mention": "Mainintoja:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Buusteja:", "notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Hae", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Poista", "status.favourite": "Tykkää", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index eaa01638c..b6605295b 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -29,7 +29,7 @@ "column.favourites": "Favoris", "column.follow_requests": "Demandes de suivi", "column.home": "Accueil", - "column.mutes": "Comptes silencés", + "column.mutes": "Comptes masqués", "column.notifications": "Notifications", "column.public": "Fil public global", "column_back_button.label": "Retour", @@ -52,9 +52,9 @@ "confirmations.delete.confirm": "Supprimer", "confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?", "confirmations.domain_block.confirm": "Masquer le domaine entier", - "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou silenciations ciblés sont suffisants et préférables.", - "confirmations.mute.confirm": "Silencer", - "confirmations.mute.message": "Confirmez vous la silenciation {name} ?", + "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.", + "confirmations.mute.confirm": "Masquer", + "confirmations.mute.message": "Confirmez vous le masquage de {name} ?", "emoji_button.activity": "Activités", "emoji_button.flags": "Drapeaux", "emoji_button.food": "Boire et manger", @@ -96,7 +96,7 @@ "navigation_bar.follow_requests": "Demandes de suivi", "navigation_bar.info": "Plus d’informations", "navigation_bar.logout": "Déconnexion", - "navigation_bar.mutes": "Comptes silencés", + "navigation_bar.mutes": "Comptes masqués", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public global", "notification.favourite": "{name} a ajouté à ses favoris :", @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoris :", "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Émettre un son", @@ -147,6 +149,7 @@ "report.target": "Signalement", "search.placeholder": "Rechercher", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Cette publication ne peut être boostée", "status.delete": "Effacer", "status.favourite": "Ajouter aux favoris", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 98c7ea021..8b63bd26b 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "מחובבים:", "notifications.column_settings.follow": "עוקבים חדשים:", "notifications.column_settings.mention": "פניות:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "הדהודים:", "notifications.column_settings.show": "הצגה בטור", "notifications.column_settings.sound": "שמע מופעל", @@ -147,6 +149,7 @@ "report.target": "דיווח", "search.placeholder": "חיפוש", "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "לא ניתן להדהד הודעה זו", "status.delete": "מחיקה", "status.favourite": "חיבוב", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index fdf5c11c0..165e3088f 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoriti:", "notifications.column_settings.follow": "Novi sljedbenici:", "notifications.column_settings.mention": "Spominjanja:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Prikaži u stupcu", "notifications.column_settings.sound": "Sviraj zvuk", @@ -147,6 +149,7 @@ "report.target": "Prijavljivanje", "search.placeholder": "Traži", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Ovaj post ne može biti podignut", "status.delete": "Obriši", "status.favourite": "Označi omiljenim", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index baf762c8d..71dcce505 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.follow": "New followers:", "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Keresés", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Törlés", "status.favourite": "Kedvenc", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 6f6d688e9..0c21877d8 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorit:", "notifications.column_settings.follow": "Pengikut baru:", "notifications.column_settings.mention": "Balasan:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boost:", "notifications.column_settings.show": "Tampilkan dalam kolom", "notifications.column_settings.sound": "Mainkan suara", @@ -147,6 +149,7 @@ "report.target": "Melaporkan", "search.placeholder": "Pencarian", "search_results.total": "{count} {count, plural, one {hasil} other {hasil}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Hapus", "status.favourite": "Difavoritkan", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 25e0adc8a..788d09f34 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorati:", "notifications.column_settings.follow": "Nova sequanti:", "notifications.column_settings.mention": "Mencioni:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Repeti:", "notifications.column_settings.show": "Montrar en kolumno", "notifications.column_settings.sound": "Plear sono", @@ -147,6 +149,7 @@ "report.target": "Denuncante", "search.placeholder": "Serchez", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Efacar", "status.favourite": "Favorizar", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 4881b0f08..9176bfaaf 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Apprezzati:", "notifications.column_settings.follow": "Nuovi seguaci:", "notifications.column_settings.mention": "Menzioni:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Post condivisi:", "notifications.column_settings.show": "Mostra in colonna", "notifications.column_settings.sound": "Riproduci suono", @@ -147,6 +149,7 @@ "report.target": "Invio la segnalazione", "search.placeholder": "Cerca", "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Elimina", "status.favourite": "Apprezzato", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index f62072852..a686cdc03 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "お気に入り", "notifications.column_settings.follow": "新しいフォロワー", "notifications.column_settings.mention": "返信", + "notifications.column_settings.push": "プッシュ通知", + "notifications.column_settings.push_meta": "このデバイス", "notifications.column_settings.reblog": "ブースト", "notifications.column_settings.show": "カラムに表示", "notifications.column_settings.sound": "通知音を再生", @@ -147,6 +149,7 @@ "report.target": "問題のユーザー", "search.placeholder": "検索", "search_results.total": "{count, number}件の結果", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "この投稿はブーストできません", "status.delete": "削除", "status.favourite": "お気に入り", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 5e1aaac85..0b47cc990 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "즐겨찾기", "notifications.column_settings.follow": "새 팔로워", "notifications.column_settings.mention": "답글", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "부스트", "notifications.column_settings.show": "컬럼에 표시", "notifications.column_settings.sound": "효과음 재생", @@ -147,6 +149,7 @@ "report.target": "문제가 된 사용자", "search.placeholder": "검색", "search_results.total": "{count, number}건의 결과", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다", "status.delete": "삭제", "status.favourite": "즐겨찾기", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 479d157f3..cf6a8bd31 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorieten:", "notifications.column_settings.follow": "Nieuwe volgers:", "notifications.column_settings.mention": "Vermeldingen:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "In kolom tonen", "notifications.column_settings.sound": "Geluid afspelen", @@ -147,6 +149,7 @@ "report.target": "Rapporteren van", "search.placeholder": "Zoeken", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Deze toot kan niet geboost worden", "status.delete": "Verwijderen", "status.favourite": "Favoriet", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 4bbf14938..1f4082d7b 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Likt:", "notifications.column_settings.follow": "Nye følgere:", "notifications.column_settings.mention": "Nevnt:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Fremhevet:", "notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.sound": "Spill lyd", @@ -147,6 +149,7 @@ "report.target": "Rapporterer", "search.placeholder": "Søk", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Denne posten kan ikke fremheves", "status.delete": "Slett", "status.favourite": "Lik", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 2c119ef41..dc6dd5e32 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favorits :", "notifications.column_settings.follow": "Nòus seguidors :", "notifications.column_settings.mention": "Mencions :", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partatges :", "notifications.column_settings.show": "Mostrar dins la colomna", "notifications.column_settings.sound": "Emetre un son", @@ -147,6 +149,7 @@ "report.target": "Senhalar {target}", "search.placeholder": "Recercar", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", "status.delete": "Escafar", "status.favourite": "Apondre als favorits", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index ac63ec40f..233d61995 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -3,10 +3,10 @@ "account.block_domain": "Blokuj wszystko z {domain}", "account.disclaimer": "Ten użytkownik pochodzi z innej instancji. Ta liczba może być większa.", "account.edit_profile": "Edytuj profil", - "account.follow": "Obserwuj", - "account.followers": "Obserwujący", - "account.follows": "Obserwacje", - "account.follows_you": "Obserwuje cię", + "account.follow": "Śledź", + "account.followers": "Śledzący", + "account.follows": "Śledzeni", + "account.follows_you": "Śledzi Cię", "account.media": "Media", "account.mention": "Wspomnij o @{name}", "account.mute": "Wycisz @{name}", @@ -15,7 +15,7 @@ "account.requested": "Oczekująca prośba", "account.unblock": "Odblokuj @{name}", "account.unblock_domain": "Odblokuj domenę {domain}", - "account.unfollow": "Przestań obserwować", + "account.unfollow": "Przestań śledzić", "account.unmute": "Cofnij wyciszenie @{name}", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", @@ -27,7 +27,7 @@ "column.blocks": "Zablokowani użytkownicy", "column.community": "Lokalna oś czasu", "column.favourites": "Ulubione", - "column.follow_requests": "Prośby o obserwację", + "column.follow_requests": "Prośby o śledzenie", "column.home": "Strona główna", "column.mutes": "Wyciszeni użytkownicy", "column.notifications": "Powiadomienia", @@ -37,9 +37,9 @@ "column_header.unpin": "Cofnij przypięcie", "column_subheading.navigation": "Nawigacja", "column_subheading.settings": "Ustawienia", - "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto cię obserwuje, może wyświetlać twoje posty przeznaczone tylko dla obserwujących.", + "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje posty przeznaczone tylko dla śledzących.", "compose_form.lock_disclaimer.lock": "zablokowane", - "compose_form.placeholder": "Co ci chodzi po głowie?", + "compose_form.placeholder": "Co Ci chodzi po głowie?", "compose_form.privacy_disclaimer": "Twój post zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność postów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, post może być widoczny dla niewłaściwych osób.", "compose_form.publish": "Wyślij", "compose_form.publish_loud": "{publish}!", @@ -67,7 +67,7 @@ "emoji_button.travel": "Podróże i miejsca", "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby odbić piłeczkę!", "empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!", - "empty_column.home": "Nie obserwujesz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć ciekawych ludzi.", + "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.", "empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.", "empty_column.home.public_timeline": "publiczna oś czasu", "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.", @@ -93,32 +93,34 @@ "navigation_bar.community_timeline": "Lokalna oś czasu", "navigation_bar.edit_profile": "Edytuj profil", "navigation_bar.favourites": "Ulubione", - "navigation_bar.follow_requests": "Prośby o obserwację", + "navigation_bar.follow_requests": "Prośby o śledzenie", "navigation_bar.info": "Szczegółowe informacje", "navigation_bar.logout": "Wyloguj", "navigation_bar.mutes": "Wyciszeni użytkownicy", "navigation_bar.preferences": "Preferencje", "navigation_bar.public_timeline": "Oś czasu federacji", - "notification.favourite": "{name} dodał twój status do ulubionych", - "notification.follow": "{name} zaczął cię obserwować", + "notification.favourite": "{name} dodał Twój status do ulubionych", + "notification.follow": "{name} zaczął Cię śledzić", "notification.mention": "{name} wspomniał o tobie", - "notification.reblog": "{name} podbił twój status", + "notification.reblog": "{name} podbił Twój status", "notifications.clear": "Wyczyść powiadomienia", "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?", "notifications.column_settings.alert": "Powiadomienia na pulpicie", "notifications.column_settings.favourite": "Ulubione:", - "notifications.column_settings.follow": "Nowi obserwujący:", + "notifications.column_settings.follow": "Nowi śledzący:", "notifications.column_settings.mention": "Wspomniali:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Podbili:", "notifications.column_settings.show": "Pokaż w kolumnie", "notifications.column_settings.sound": "Odtwarzaj dźwięk", "onboarding.done": "Gotowe", "onboarding.next": "Dalej", - "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy obserwowanych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.", + "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.", "onboarding.page_four.home": "Główna oś czasu wyświetla publiczne wpisy.", "onboarding.page_four.notifications": "Kolumna powiadomień wyświetla, gdy ktoś dokonuje interakcji z tobą.", "onboarding.page_one.federation": "Mastodon jest siecią niezależnych serwerów połączonych w jeden portal społecznościowy. Nazywamy te serwery instancjami.", - "onboarding.page_one.handle": "Jesteś na domenie {domain}, więc twój pełny adres to {handle}", + "onboarding.page_one.handle": "Jesteś na domenie {domain}, więc Twój pełny adres to {handle}", "onboarding.page_one.welcome": "Witamy w Mastodon!", "onboarding.page_six.admin": "Administratorem tej instancji jest {admin}.", "onboarding.page_six.almost_done": "Prawie gotowe...", @@ -135,8 +137,8 @@ "privacy.change": "Dostosuj widoczność postów", "privacy.direct.long": "Widoczne tylko dla oznaczonych", "privacy.direct.short": "Bezpośrednio", - "privacy.private.long": "Widoczne tylko dla obserwujących", - "privacy.private.short": "Tylko obserwujący", + "privacy.private.long": "Widoczne tylko dla śledzących", + "privacy.private.short": "Tylko śledzący", "privacy.public.long": "Widoczne na publicznych osiach czasu", "privacy.public.short": "Publiczne", "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", @@ -147,6 +149,7 @@ "report.target": "Zgłaszanie {target}", "search.placeholder": "Szukaj", "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Ten post nie może zostać podbity", "status.delete": "Usuń", "status.favourite": "Ulubione", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index b199a39ce..cf2b911f2 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", @@ -147,6 +149,7 @@ "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Eliminar", "status.favourite": "Adicionar aos favoritos", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index b199a39ce..cf2b911f2 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", @@ -147,6 +149,7 @@ "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Eliminar", "status.favourite": "Adicionar aos favoritos", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index f9f48a48d..942a13ede 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Нравится:", "notifications.column_settings.follow": "Новые подписчики:", "notifications.column_settings.mention": "Упоминания:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Продвижения:", "notifications.column_settings.show": "Показывать в колонке", "notifications.column_settings.sound": "Проигрывать звук", @@ -147,6 +149,7 @@ "report.target": "Жалуемся на", "search.placeholder": "Поиск", "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Этот статус не может быть продвинут", "status.delete": "Удалить", "status.favourite": "Нравится", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 8a39beacb..e9e96c14f 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.follow": "New followers:", "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "Search", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "This post cannot be boosted", "status.delete": "Delete", "status.favourite": "Favourite", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 203e4a09e..adfa79cd9 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Favoriler:", "notifications.column_settings.follow": "Yeni takipçiler:", "notifications.column_settings.mention": "Bahsedilenler:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Boost’lar:", "notifications.column_settings.show": "Bildirimlerde göster", "notifications.column_settings.sound": "Ses çal", @@ -147,6 +149,7 @@ "report.target": "Raporlama", "search.placeholder": "Ara", "search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Bu gönderi boost edilemez", "status.delete": "Sil", "status.favourite": "Favorilere ekle", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index c0f4a8dbb..435067281 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "Вподобане:", "notifications.column_settings.follow": "Нові підписники:", "notifications.column_settings.mention": "Сповіщення:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "Передмухи:", "notifications.column_settings.show": "Показати в колонці", "notifications.column_settings.sound": "Відтворювати звук", @@ -147,6 +149,7 @@ "report.target": "Скаржимося на", "search.placeholder": "Пошук", "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "Цей допис не може бути передмухнутий", "status.delete": "Видалити", "status.favourite": "Подобається", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 998e1c8da..0f2c1fcec 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "你的嘟文被赞:", "notifications.column_settings.follow": "关注你:", "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "你的嘟文被转嘟:", "notifications.column_settings.show": "在通知栏显示", "notifications.column_settings.sound": "播放音效", @@ -147,6 +149,7 @@ "report.target": "Reporting", "search.placeholder": "搜索", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "没法转嘟这条嘟文啦……", "status.delete": "删除", "status.favourite": "赞", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 1079d5429..c0b4cfce9 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "喜歡你的文章:", "notifications.column_settings.follow": "關注你:", "notifications.column_settings.mention": "提及你:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "轉推你的文章:", "notifications.column_settings.show": "在通知欄顯示", "notifications.column_settings.sound": "播放音效", @@ -147,6 +149,7 @@ "report.target": "舉報", "search.placeholder": "搜尋", "search_results.total": "{count, number} 項結果", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "這篇文章無法被轉推", "status.delete": "刪除", "status.favourite": "喜歡", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 6240b8879..772cc691c 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -109,6 +109,8 @@ "notifications.column_settings.favourite": "最愛:", "notifications.column_settings.follow": "新的關注者:", "notifications.column_settings.mention": "提到:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push_meta": "This device", "notifications.column_settings.reblog": "轉推:", "notifications.column_settings.show": "顯示在欄位中", "notifications.column_settings.sound": "播放音效", @@ -147,6 +149,7 @@ "report.target": "通報中", "search.placeholder": "搜尋", "search_results.total": "{count, number} 項結果", + "standalone.public_title": "A look inside...", "status.cannot_reblog": "此貼文無法轉推", "status.delete": "刪除", "status.favourite": "喜愛", diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index 90c2c5da2..b237e9aee 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,12 +1,6 @@ -const perf = require('./performance'); +import ready from './ready'; -function onDomContentLoaded(callback) { - if (document.readyState !== 'loading') { - callback(); - } else { - document.addEventListener('DOMContentLoaded', callback); - } -} +const perf = require('./performance'); function main() { perf.start('main()'); @@ -24,11 +18,19 @@ function main() { } } - onDomContentLoaded(() => { + ready(() => { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); ReactDOM.render(<Mastodon {...props} />, mountNode); + if (process.env.NODE_ENV === 'production') { + // avoid offline in dev mode because it's harder to debug + const OfflinePluginRuntime = require('offline-plugin/runtime'); + const WebPushSubscription = require('./web_push_subscription'); + + OfflinePluginRuntime.install(); + WebPushSubscription.register(); + } perf.stop('main()'); // remember the initial URL diff --git a/app/javascript/mastodon/ready.js b/app/javascript/mastodon/ready.js new file mode 100644 index 000000000..dd543910b --- /dev/null +++ b/app/javascript/mastodon/ready.js @@ -0,0 +1,7 @@ +export default function ready(loaded) { + if (['interactive', 'complete'].includes(document.readyState)) { + loaded(); + } else { + document.addEventListener('DOMContentLoaded', loaded); + } +} diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 6ac7b4b4a..0c5dbccab 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -126,7 +126,7 @@ const insertSuggestion = (state, position, token, completion) => { }; const insertEmoji = (state, position, emojiData) => { - const emoji = emojiData.shortname; + const emoji = String.fromCodePoint(parseInt(emojiData.unicode, 16)); return state.withMutations(map => { map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`); diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 35f30f601..42b66d15f 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -11,6 +11,7 @@ import statuses from './statuses'; import relationships from './relationships'; import settings from './settings'; import local_settings from '../../glitch/reducers/local_settings'; +import push_notifications from './push_notifications'; import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; @@ -33,7 +34,11 @@ const reducers = { statuses, relationships, settings, +<<<<<<< HEAD local_settings, +======= + push_notifications, +>>>>>>> upstream cards, reports, contexts, diff --git a/app/javascript/mastodon/reducers/push_notifications.js b/app/javascript/mastodon/reducers/push_notifications.js new file mode 100644 index 000000000..31a40d246 --- /dev/null +++ b/app/javascript/mastodon/reducers/push_notifications.js @@ -0,0 +1,51 @@ +import { STORE_HYDRATE } from '../actions/store'; +import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from '../actions/push_notifications'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + subscription: null, + alerts: new Immutable.Map({ + follow: false, + favourite: false, + reblog: false, + mention: false, + }), + isSubscribed: false, + browserSupport: false, +}); + +export default function push_subscriptions(state = initialState, action) { + switch(action.type) { + case STORE_HYDRATE: { + const push_subscription = action.state.get('push_subscription'); + + if (push_subscription) { + return state + .set('subscription', new Immutable.Map({ + id: push_subscription.get('id'), + endpoint: push_subscription.get('endpoint'), + })) + .set('alerts', push_subscription.get('alerts') || initialState.get('alerts')) + .set('isSubscribed', true); + } + + return state; + } + case SET_SUBSCRIPTION: + return state + .set('subscription', new Immutable.Map({ + id: action.subscription.id, + endpoint: action.subscription.endpoint, + })) + .set('alerts', new Immutable.Map(action.subscription.alerts)) + .set('isSubscribed', true); + case SET_BROWSER_SUPPORT: + return state.set('browserSupport', action.value); + case CLEAR_SUBSCRIPTION: + return initialState; + case ALERTS_CHANGE: + return state.setIn(action.key, action.value); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js new file mode 100644 index 000000000..364b67066 --- /dev/null +++ b/app/javascript/mastodon/service_worker/entry.js @@ -0,0 +1 @@ +import './web_push_notifications'; diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js new file mode 100644 index 000000000..1708aa9f7 --- /dev/null +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -0,0 +1,86 @@ +const handlePush = (event) => { + const options = event.data.json(); + + options.body = options.data.nsfw || options.data.content; + options.image = options.image || undefined; // Null results in a network request (404) + options.timestamp = options.timestamp && new Date(options.timestamp); + + const expandAction = options.data.actions.find(action => action.todo === 'expand'); + + if (expandAction) { + options.actions = [expandAction]; + options.hiddenActions = options.data.actions.filter(action => action !== expandAction); + + options.data.hiddenImage = options.image; + options.image = undefined; + } else { + options.actions = options.data.actions; + } + + event.waitUntil(self.registration.showNotification(options.title, options)); +}; + +const cloneNotification = (notification) => { + const clone = { }; + + for(var k in notification) { + clone[k] = notification[k]; + } + + return clone; +}; + +const expandNotification = (notification) => { + const nextNotification = cloneNotification(notification); + + nextNotification.body = notification.data.content; + nextNotification.image = notification.data.hiddenImage; + nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); + + return self.registration.showNotification(nextNotification.title, nextNotification); +}; + +const makeRequest = (notification, action) => + fetch(action.action, { + headers: { + 'Authorization': `Bearer ${notification.data.access_token}`, + 'Content-Type': 'application/json', + }, + method: action.method, + credentials: 'include', + }); + +const removeActionFromNotification = (notification, action) => { + const actions = notification.actions.filter(act => act.action !== action.action); + + const nextNotification = cloneNotification(notification); + + nextNotification.actions = actions; + + return self.registration.showNotification(nextNotification.title, nextNotification); +}; + +const handleNotificationClick = (event) => { + const reactToNotificationClick = new Promise((resolve, reject) => { + if (event.action) { + const action = event.notification.data.actions.find(({ action }) => action === event.action); + + if (action.todo === 'expand') { + resolve(expandNotification(event.notification)); + } else if (action.todo === 'request') { + resolve(makeRequest(event.notification, action) + .then(() => removeActionFromNotification(event.notification, action))); + } else { + reject(`Unknown action: ${action.todo}`); + } + } else { + event.notification.close(); + resolve(self.clients.openWindow(event.notification.data.url)); + } + }); + + event.waitUntil(reactToNotificationClick); +}; + +self.addEventListener('push', handlePush); +self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/mastodon/web_push_subscription.js b/app/javascript/mastodon/web_push_subscription.js new file mode 100644 index 000000000..391d3bcec --- /dev/null +++ b/app/javascript/mastodon/web_push_subscription.js @@ -0,0 +1,109 @@ +import axios from 'axios'; +import { store } from './containers/mastodon'; +import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications'; + +// 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) => + axios.post('/api/web/push_subscriptions', { + data: subscription, + }).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)); + + if (supportsPushNotifications) { + if (!getApplicationServerKey()) { + // eslint-disable-next-line no-console + 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)); + } + }) + .catch(error => { + if (error.code === 20 && error.name === 'AbortError') { + // eslint-disable-next-line no-console + 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') { + // eslint-disable-next-line no-console + console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); + } + + // Clear alerts and hide UI settings + store.dispatch(clearSubscription()); + + try { + getRegistration() + .then(getPushSubscription) + .then(unsubscribe); + } catch (e) { + + } + }); + } else { + // eslint-disable-next-line no-console + console.warn('Your browser does not support Web Push Notifications.'); + } +} diff --git a/app/javascript/packs/about.js b/app/javascript/packs/about.js new file mode 100644 index 000000000..7b8ab5e5d --- /dev/null +++ b/app/javascript/packs/about.js @@ -0,0 +1,24 @@ +import TimelineContainer from '../mastodon/containers/timeline_container'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import loadPolyfills from '../mastodon/load_polyfills'; +import ready from '../mastodon/ready'; + +require.context('../images/', true); + +function loaded() { + const mountNode = document.getElementById('mastodon-timeline'); + + if (mountNode !== null) { + const props = JSON.parse(mountNode.getAttribute('data-props')); + ReactDOM.render(<TimelineContainer {...props} />, mountNode); + } +} + +function main() { + ready(loaded); +} + +loadPolyfills().then(main).catch(error => { + console.error(error); +}); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 06cc1b53a..4865f3ec0 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -5,9 +5,7 @@ import emojify from '../mastodon/emoji'; import { getLocale } from '../mastodon/locales'; import loadPolyfills from '../mastodon/load_polyfills'; import { processBio } from '../glitch/util/bio_metadata'; -import TimelineContainer from '../mastodon/containers/timeline_container'; -import React from 'react'; -import ReactDOM from 'react-dom'; +import ready from '../mastodon/ready'; require.context('../images/', true); @@ -40,21 +38,10 @@ function loaded() { const datetime = new Date(content.getAttribute('datetime')); content.textContent = relativeFormat.format(datetime);; }); - - const mountNode = document.getElementById('mastodon-timeline'); - - if (mountNode !== null) { - const props = JSON.parse(mountNode.getAttribute('data-props')); - ReactDOM.render(<TimelineContainer {...props} />, mountNode); - } } function main() { - if (['interactive', 'complete'].includes(document.readyState)) { - loaded(); - } else { - document.addEventListener('DOMContentLoaded', loaded); - } + ready(loaded); delegate(document, '.video-player video', 'click', ({ target }) => { if (target.paused) { diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 9602d31fa..f12c8fbd1 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1554,6 +1554,9 @@ } .react-swipeable-view-container > * { + display: flex; + align-items: center; + justify-content: center; height: 100%; } @@ -2007,6 +2010,7 @@ width: 100%; margin: 0; color: $ui-base-color; + background: $simple-background-color; padding: 10px; font-family: inherit; font-size: 14px; @@ -2029,7 +2033,6 @@ .autosuggest-textarea__textarea { min-height: 100px; - background: $simple-background-color; border-radius: 4px 4px 0 0; padding-bottom: 0; padding-right: 10px + 22px; @@ -2620,7 +2623,8 @@ button.icon-button.active i.fa-retweet { line-height: 24px; } -.setting-toggle__label { +.setting-toggle__label, +.setting-meta__label { color: $ui-primary-color; display: inline-block; margin-bottom: 14px; @@ -2628,6 +2632,11 @@ button.icon-button.active i.fa-retweet { vertical-align: middle; } +.setting-meta__label { + color: $ui-primary-color; + float: right; +} + .empty-column-indicator, .error-column { color: lighten($ui-base-color, 20%); @@ -2968,6 +2977,7 @@ button.icon-button.active i.fa-retweet { margin-left: 2px; width: 24px; outline: 0; + cursor: pointer; &:active, &:focus { @@ -3297,6 +3307,7 @@ button.icon-button.active i.fa-retweet { max-height: 80vh; position: relative; + .extended-video-player, img, canvas, video { @@ -3306,6 +3317,13 @@ button.icon-button.active i.fa-retweet { height: auto; } + .extended-video-player, + video { + display: flex; + width: 80vw; + height: 80vh; + } + img, canvas { display: block; diff --git a/app/javascript/styles/rtl.scss b/app/javascript/styles/rtl.scss index a91d0d72a..4966fbc21 100644 --- a/app/javascript/styles/rtl.scss +++ b/app/javascript/styles/rtl.scss @@ -45,6 +45,10 @@ body.rtl { margin-right: 8px; } + .setting-meta__label { + float: left; + } + .status__avatar { left: auto; right: 10px; |