diff options
Diffstat (limited to 'app')
112 files changed, 1211 insertions, 353 deletions
diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb index 983483881..863cc7351 100644 --- a/app/controllers/settings/two_factor_authentications_controller.rb +++ b/app/controllers/settings/two_factor_authentications_controller.rb @@ -18,7 +18,7 @@ module Settings end def destroy - if current_user.validate_and_consume_otp!(confirmation_params[:code]) + if acceptable_code? current_user.otp_required_for_login = false current_user.save! redirect_to settings_two_factor_authentication_path @@ -38,5 +38,10 @@ module Settings def verify_otp_required redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login? end + + def acceptable_code? + current_user.validate_and_consume_otp!(confirmation_params[:code]) || + current_user.invalidate_otp_backup_code!(confirmation_params[:code]) + end end end diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index a1c3c3521..70027cca9 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -2,7 +2,7 @@ module InstanceHelper def site_title - Setting.site_title.to_s + Setting.site_title.presence || site_hostname end def site_hostname diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index fa41e59e1..35b37600f 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -162,20 +162,23 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { return ( <div className='autosuggest-textarea'> - <Textarea - inputRef={this.setTextarea} - className='autosuggest-textarea__textarea' - disabled={disabled} - placeholder={placeholder} - autoFocus={autoFocus} - value={value} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={onKeyUp} - onBlur={this.onBlur} - onPaste={this.onPaste} - style={style} - /> + <label> + <span style={{ display: 'none' }}>{placeholder}</span> + <Textarea + inputRef={this.setTextarea} + className='autosuggest-textarea__textarea' + disabled={disabled} + placeholder={placeholder} + autoFocus={autoFocus} + value={value} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyUp={onKeyUp} + onBlur={this.onBlur} + onPaste={this.onPaste} + style={style} + /> + </label> <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> {suggestions.map((suggestion, i) => ( diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js index 589215ce8..50c3bf11f 100644 --- a/app/javascript/mastodon/components/column_back_button.js +++ b/app/javascript/mastodon/components/column_back_button.js @@ -19,10 +19,10 @@ export default class ColumnBackButton extends React.PureComponent { render () { return ( - <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'> + <button onClick={this.handleClick} className='column-back-button'> <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </div> + </button> ); } diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index 9945fc209..e0042b055 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -8,6 +8,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container'; const messages = defineMessages({ + show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, + hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, + moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, + moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, }); @@ -19,11 +23,13 @@ export default class ColumnHeader extends React.PureComponent { }; static propTypes = { + intl: PropTypes.object.isRequired, title: PropTypes.node.isRequired, icon: PropTypes.string.isRequired, active: PropTypes.bool, localSettings : ImmutablePropTypes.map, multiColumn: PropTypes.bool, + focusable: PropTypes.bool, showBackButton: PropTypes.bool, notifCleaning: PropTypes.bool, // true only for the notification column notifCleaningActive: PropTypes.bool, @@ -36,6 +42,10 @@ export default class ColumnHeader extends React.PureComponent { intl: PropTypes.object.isRequired, }; + static defaultProps = { + focusable: true, + } + state = { collapsed: true, animating: false, @@ -82,10 +92,9 @@ export default class ColumnHeader extends React.PureComponent { } render () { - const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, notifCleaningActive } = this.props; + const { intl, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props; const { collapsed, animating, animatingNCD } = this.state; - let title = this.props.title; const wrapperClassName = classNames('column-header__wrapper', { @@ -132,8 +141,8 @@ export default class ColumnHeader extends React.PureComponent { moveButtons = ( <div key='move-buttons' className='column-header__setting-arrows'> - <button className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> - <button className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> + <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> + <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> </div> ); } else if (multiColumn) { @@ -159,12 +168,12 @@ export default class ColumnHeader extends React.PureComponent { } if (children || multiColumn) { - collapseButton = <button className={collapsibleButtonClassName} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; + collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; } return ( <div className={wrapperClassName}> - <div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}> + <h1 tabIndex={focusable && '0'} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}> <i className={`fa fa-fw fa-${icon} column-header__icon`} /> {title} <div className='column-header__buttons'> @@ -181,7 +190,7 @@ export default class ColumnHeader extends React.PureComponent { ) : null} {collapseButton} </div> - </div> + </h1> { notifCleaning ? ( <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}> @@ -191,7 +200,7 @@ export default class ColumnHeader extends React.PureComponent { </div> ) : null} - <div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}> + <div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}> <div className='column-header__collapsible-inner'> {(!collapsed || animating) && collapsedContent} </div> diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 98323b069..28631f463 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -1,4 +1,5 @@ import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import PropTypes from 'prop-types'; @@ -9,16 +10,23 @@ export default class DropdownMenu extends React.PureComponent { }; static propTypes = { + isUserTouching: PropTypes.func, + isModalOpen: PropTypes.bool.isRequired, + onModalOpen: PropTypes.func, + onModalClose: PropTypes.func, icon: PropTypes.string.isRequired, items: PropTypes.array.isRequired, size: PropTypes.number.isRequired, direction: PropTypes.string, + status: ImmutablePropTypes.map, ariaLabel: PropTypes.string, disabled: PropTypes.bool, }; static defaultProps = { ariaLabel: 'Menu', + isModalOpen: false, + isUserTouching: () => false, }; state = { @@ -34,6 +42,10 @@ export default class DropdownMenu extends React.PureComponent { const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; + if (this.props.isModalOpen) { + this.props.onModalClose(); + } + // Don't call e.preventDefault() when the item uses 'href' property. // ex. "Edit profile" on the account action bar @@ -48,10 +60,32 @@ export default class DropdownMenu extends React.PureComponent { this.dropdown.hide(); } - handleShow = () => this.setState({ expanded: true }) + handleShow = () => { + if (this.props.isUserTouching()) { + this.props.onModalOpen({ + status: this.props.status, + actions: this.props.items, + onClick: this.handleClick, + }); + } else { + this.setState({ expanded: true }); + } + } handleHide = () => this.setState({ expanded: false }) + handleToggle = (e) => { + if (e.key === 'Enter') { + if (this.props.isUserTouching()) { + this.handleShow(); + } else { + this.setState({ expanded: !this.state.expanded }); + } + } else if (e.key === 'Escape') { + this.setState({ expanded: false }); + } + } + renderItem = (item, i) => { if (item === null) { return <li key={`sep-${i}`} className='dropdown__sep' />; @@ -61,7 +95,7 @@ export default class DropdownMenu extends React.PureComponent { return ( <li className='dropdown__content-list-item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'> + <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'> {text} </a> </li> @@ -71,6 +105,7 @@ export default class DropdownMenu extends React.PureComponent { render () { const { icon, items, size, direction, ariaLabel, disabled } = this.props; const { expanded } = this.state; + const isUserTouching = this.props.isUserTouching(); const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right'; const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }; const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`; @@ -84,20 +119,26 @@ export default class DropdownMenu extends React.PureComponent { } const dropdownItems = expanded && ( - <ul className='dropdown__content-list'> + <ul role='group' className='dropdown__content-list' onClick={this.handleHide}> {items.map(this.renderItem)} </ul> ); + // No need to render the actual dropdown if we use the modal. If we + // don't render anything <Dropdow /> breaks, so we just put an empty div. + const dropdownContent = !isUserTouching ? ( + <DropdownContent className={directionClass} > + {dropdownItems} + </DropdownContent> + ) : <div />; + return ( - <Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}> - <DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}> + <Dropdown ref={this.setRef} active={isUserTouching ? false : expanded} onShow={this.handleShow} onHide={this.handleHide}> + <DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-expanded={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}> <i className={iconClassname} aria-hidden /> </DropdownTrigger> - <DropdownContent className={directionClass}> - {dropdownItems} - </DropdownContent> + {dropdownContent} </Dropdown> ); } diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 748283853..8c5b5e0b9 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -12,6 +12,8 @@ export default class IconButton extends React.PureComponent { onClick: PropTypes.func, size: PropTypes.number, active: PropTypes.bool, + pressed: PropTypes.bool, + expanded: PropTypes.bool, style: PropTypes.object, activeStyle: PropTypes.object, disabled: PropTypes.bool, @@ -19,6 +21,7 @@ export default class IconButton extends React.PureComponent { animate: PropTypes.bool, flip: PropTypes.bool, overlay: PropTypes.bool, + tabIndex: PropTypes.string, }; static defaultProps = { @@ -27,6 +30,7 @@ export default class IconButton extends React.PureComponent { disabled: false, animate: false, overlay: false, + tabIndex: '0', }; handleClick = (e) => { @@ -74,10 +78,13 @@ export default class IconButton extends React.PureComponent { {({ rotate }) => <button aria-label={this.props.title} + aria-pressed={this.props.pressed} + aria-expanded={this.props.expanded} title={this.props.title} className={classes.join(' ')} onClick={this.handleClick} style={style} + tabIndex={this.props.tabIndex} > <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> </button> diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index d95e7c75d..fa6ea72d5 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -215,10 +215,10 @@ export default class MediaGallery extends React.PureComponent { } children = ( - <div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}> + <button className='media-spoiler' onClick={this.handleOpen}> <span className='media-spoiler__warning'>{warning}</span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </div> + </button> ); } else { const size = media.take(4).size; diff --git a/app/javascript/mastodon/components/setting_text.js b/app/javascript/mastodon/components/setting_text.js index dd975bc99..a6dde4c0f 100644 --- a/app/javascript/mastodon/components/setting_text.js +++ b/app/javascript/mastodon/components/setting_text.js @@ -19,12 +19,15 @@ export default class SettingText extends React.PureComponent { const { settings, settingKey, label } = this.props; return ( - <input - className='setting-text' - value={settings.getIn(settingKey)} - onChange={this.handleChange} - placeholder={label} - /> + <label> + <span style={{ display: 'none' }}>{label}</span> + <input + className='setting-text' + value={settings.getIn(settingKey)} + onChange={this.handleChange} + placeholder={label} + /> + </label> ); } diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 6605457f7..ac82e536f 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -44,6 +44,8 @@ export default class Status extends ImmutablePureComponent { autoPlayGif: PropTypes.bool, muted: PropTypes.bool, intersectionObserverWrapper: PropTypes.object, + index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; state = { @@ -62,6 +64,7 @@ export default class Status extends ImmutablePureComponent { 'boostModal', 'autoPlayGif', 'muted', + 'listLength', ] updateOnStates = ['isExpanded'] @@ -70,8 +73,8 @@ export default class Status extends ImmutablePureComponent { if (!nextState.isIntersecting && nextState.isHidden) { // It's only if we're not intersecting (i.e. offscreen) and isHidden is true // that either "isIntersecting" or "isHidden" matter, and then they're - // the only things that matter. - return this.state.isIntersecting || !this.state.isHidden; + // the only things that matter (and updated ARIA attributes). + return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength; } else if (nextState.isIntersecting && !this.state.isIntersecting) { // If we're going from a non-intersecting state to an intersecting state, // (i.e. offscreen to onscreen), then we definitely need to re-render @@ -110,17 +113,12 @@ export default class Status extends ImmutablePureComponent { this.height = getRectFromEntry(entry).height; } - // Edge 15 doesn't support isIntersecting, but we can infer it - // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/ - // https://github.com/WICG/IntersectionObserver/issues/211 - const isIntersecting = (typeof entry.isIntersecting === 'boolean') ? - entry.isIntersecting : entry.intersectionRect.height > 0; this.setState((prevState) => { - if (prevState.isIntersecting && !isIntersecting) { + if (prevState.isIntersecting && !entry.isIntersecting) { scheduleIdleTask(this.hideIfNotIntersecting); } return { - isIntersecting: isIntersecting, + isIntersecting: entry.isIntersecting, isHidden: false, }; }); @@ -177,7 +175,7 @@ export default class Status extends ImmutablePureComponent { // Exclude intersectionObserverWrapper from `other` variable // because intersection is managed in here. - const { status, account, intersectionObserverWrapper, ...other } = this.props; + const { status, account, intersectionObserverWrapper, index, listLength, wrapped, ...other } = this.props; const { isExpanded, isIntersecting, isHidden } = this.state; if (status === null) { @@ -186,10 +184,10 @@ export default class Status extends ImmutablePureComponent { if (!isIntersecting && isHidden) { return ( - <div ref={this.handleRef} data-id={status.get('id')} style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}> + <article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}> {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.get('content')} - </div> + </article> ); } @@ -203,14 +201,14 @@ export default class Status extends ImmutablePureComponent { const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; return ( - <div className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} > + <article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'> <div className='status__prepend'> <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} /> </div> <Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} /> - </div> + </article> ); } @@ -239,7 +237,7 @@ export default class Status extends ImmutablePureComponent { } return ( - <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}> + <article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex={wrapped ? null : '0'} ref={this.handleRef}> <div className='status__info'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> @@ -257,7 +255,7 @@ export default class Status extends ImmutablePureComponent { {media} <StatusActionBar {...this.props} /> - </div> + </article> ); } diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 3e947b4c5..81c2a4e23 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -5,7 +5,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import IconButton from './icon_button'; -import DropdownMenu from './dropdown_menu'; +import DropdownMenuContainer from '../containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -154,12 +154,12 @@ export default class StatusActionBar extends ImmutablePureComponent { return ( <div className='status__action-bar'> <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> - <IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> - <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> + <IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> + <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> {shareButton} <div className='status__action-bar-dropdown'> - <DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' /> + <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' /> </div> </div> ); diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index ad925edef..5f02e3261 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -149,7 +149,7 @@ export default class StatusContent extends React.PureComponent { } return ( - <div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> + <div className={classNames} ref={this.setRef} tabIndex='0' aria-label={status.get('search_index')} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> <span dangerouslySetInnerHTML={spoilerContent} /> {' '} @@ -158,13 +158,15 @@ export default class StatusContent extends React.PureComponent { {mentionsPlaceholder} - <div className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} /> + <div tabIndex={!hidden && 0} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} /> </div> ); } else if (this.props.onClick) { return ( <div ref={this.setRef} + tabIndex='0' + aria-label={status.get('search_index')} className={classNames} style={directionStyle} onMouseDown={this.handleMouseDown} @@ -175,6 +177,8 @@ export default class StatusContent extends React.PureComponent { } else { return ( <div + tabIndex='0' + aria-label={status.get('search_index')} ref={this.setRef} className='status__content' style={directionStyle} diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 3dd207dbc..639c8b4e7 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -6,7 +6,7 @@ import StatusContainer from '../../glitch/components/status/container'; import LoadMore from './load_more'; import ImmutablePureComponent from 'react-immutable-pure-component'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; -import { debounce } from 'lodash'; +import { throttle } from 'lodash'; export default class StatusList extends ImmutablePureComponent { @@ -30,13 +30,13 @@ export default class StatusList extends ImmutablePureComponent { intersectionObserverWrapper = new IntersectionObserverWrapper(); - handleScroll = debounce(() => { + handleScroll = throttle(() => { if (this.node) { const { scrollTop, scrollHeight, clientHeight } = this.node; const offset = scrollHeight - scrollTop - clientHeight; this._oldScrollPosition = scrollHeight - scrollTop; - if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) { + if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) { this.props.onScrollToBottom(); } else if (scrollTop < 100 && this.props.onScrollToTop) { this.props.onScrollToTop(); @@ -44,7 +44,7 @@ export default class StatusList extends ImmutablePureComponent { this.props.onScroll(); } } - }, 200, { + }, 150, { trailing: true, }); @@ -104,6 +104,32 @@ export default class StatusList extends ImmutablePureComponent { this.props.onScrollToBottom(); } + handleKeyDown = (e) => { + if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) { + const article = (() => { + switch (e.key) { + case 'PageDown': + return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling; + case 'PageUp': + return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling; + case 'End': + return this.node.querySelector('[role="feed"] > article:last-of-type'); + case 'Home': + return this.node.querySelector('[role="feed"] > article:first-of-type'); + default: + return null; + } + })(); + + + if (article) { + e.preventDefault(); + article.focus(); + article.scrollIntoView(); + } + } + } + render () { const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; @@ -113,11 +139,11 @@ export default class StatusList extends ImmutablePureComponent { if (isLoading || statusIds.size > 0 || !emptyMessage) { scrollableArea = ( <div className='scrollable' ref={this.setRef}> - <div className='status-list'> + <div role='feed' className='status-list' onKeyDown={this.handleKeyDown}> {prepend} - {statusIds.map((statusId) => { - return <StatusContainer key={statusId} id={statusId} intersectionObserverWrapper={this.intersectionObserverWrapper} />; + {statusIds.map((statusId, index) => { + return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />; })} {loadMore} diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js new file mode 100644 index 000000000..151f25390 --- /dev/null +++ b/app/javascript/mastodon/containers/dropdown_menu_container.js @@ -0,0 +1,16 @@ +import { openModal, closeModal } from '../actions/modal'; +import { connect } from 'react-redux'; +import DropdownMenu from '../components/dropdown_menu'; +import { isUserTouching } from '../is_mobile'; + +const mapStateToProps = state => ({ + isModalOpen: state.get('modal').modalType === 'ACTIONS', +}); + +const mapDispatchToProps = dispatch => ({ + isUserTouching, + onModalOpen: props => dispatch(openModal('ACTIONS', props)), + onModalClose: () => dispatch(closeModal()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index 9b58cacf5..5695c86dd 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -3,6 +3,8 @@ import Trie from 'substring-trie'; const trie = new Trie(Object.keys(unicodeMapping)); +const excluded = ['™', '©', '®']; + function emojify(str) { // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) // and replacing valid unicode strings @@ -19,7 +21,7 @@ function emojify(str) { insideTag = true; } else if (!insideTag && (match = trie.search(str.substring(i)))) { const unicodeStr = match; - if (unicodeStr in unicodeMapping) { + if (unicodeStr in unicodeMapping && excluded.indexOf(unicodeStr) === -1) { const [filename, shortCode] = unicodeMapping[unicodeStr]; const alt = unicodeStr; const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index b8df724c6..c12c0889e 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -1,7 +1,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import DropdownMenu from '../../../components/dropdown_menu'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import Link from 'react-router-dom/Link'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; @@ -15,6 +15,7 @@ const messages = defineMessages({ mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' }, + share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, media: { id: 'account.media', defaultMessage: 'Media' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, @@ -36,6 +37,12 @@ export default class ActionBar extends React.PureComponent { intl: PropTypes.object.isRequired, }; + handleShare = () => { + navigator.share({ + url: this.props.account.get('url'), + }); + } + render () { const { account, me, intl } = this.props; @@ -43,6 +50,9 @@ export default class ActionBar extends React.PureComponent { let extraInfo = ''; menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); + if ('share' in navigator) { + menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); + } menu.push(null); menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` }); menu.push(null); @@ -96,7 +106,7 @@ export default class ActionBar extends React.PureComponent { <div className='account__action-bar'> <div className='account__action-bar-dropdown'> - <DropdownMenu items={menu} icon='bars' size={24} direction='right' /> + <DropdownMenuContainer items={menu} icon='bars' size={24} direction='right' /> </div> <div className='account__action-bar-links'> diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 1133e8a4e..9d7bc82c0 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -55,9 +55,10 @@ class Avatar extends ImmutablePureComponent { return ( <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}> {({ radius }) => - <a // eslint-disable-line jsx-a11y/anchor-has-content + <a href={account.get('url')} className='account__header__avatar' + role='presentation' target='_blank' rel='noopener' style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }} @@ -65,7 +66,9 @@ class Avatar extends ImmutablePureComponent { onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver} onBlur={this.handleMouseOut} - /> + > + <span style={{ display: 'none' }}>{account.get('acct')}</span> + </a> } </Motion> ); diff --git a/app/javascript/mastodon/features/compose/components/character_counter.js b/app/javascript/mastodon/features/compose/components/character_counter.js index 6c488b661..0ecfc9141 100644 --- a/app/javascript/mastodon/features/compose/components/character_counter.js +++ b/app/javascript/mastodon/features/compose/components/character_counter.js @@ -13,12 +13,12 @@ export default class CharacterCounter extends React.PureComponent { if (diff < 0) { return <span className='character-counter character-counter--over'>{diff}</span>; } + return <span className='character-counter'>{diff}</span>; } render () { const diff = this.props.max - length(this.props.text); - return this.checkRemainingText(diff); } diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 67906594f..0027783b4 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -19,6 +19,7 @@ import WarningContainer from '../containers/warning_container'; import { isMobile } from '../../../is_mobile'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; +import { countableText } from '../util/counter'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, @@ -150,9 +151,9 @@ export default class ComposeForm extends ImmutablePureComponent { const { intl, onPaste, showSearch } = this.props; const disabled = this.props.is_submitting; const maybeEye = this.props.advanced_options.get('do_not_federate') ? ' 👁️' : ''; - const text = [this.props.spoiler_text, this.props.text, maybeEye].join(''); + const text = [this.props.spoiler_text, countableText(this.props.text), maybeEye].join(''); - let publishText = ''; + let publishText = ''; if (this.props.privacy === 'private' || this.props.privacy === 'direct') { publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; @@ -164,7 +165,10 @@ export default class ComposeForm extends ImmutablePureComponent { <div className='compose-form'> <Collapsable isVisible={this.props.spoiler} fullHeight={50}> <div className='spoiler-input'> - <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' /> + <label> + <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> + <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' /> + </label> </div> </Collapsable> @@ -206,7 +210,7 @@ export default class ComposeForm extends ImmutablePureComponent { <div className='compose-form__publish'> <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> - <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !==0 && text.trim().length === 0)} block /></div> + <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div> </div> </div> </div> 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 acc584f20..9d05b7a34 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -65,6 +65,22 @@ export default class EmojiPickerDropdown extends React.PureComponent { this.setState({ active: false }); } + onToggle = (e) => { + if (!this.state.loading && (!e.key || e.key === 'Enter')) { + if (this.state.active) { + this.onHideDropdown(); + } else { + this.onShowDropdown(); + } + } + } + + onEmojiPickerKeyDown = (e) => { + if (e.key === 'Escape') { + this.onHideDropdown(); + } + } + render () { const { intl } = this.props; @@ -104,10 +120,11 @@ export default class EmojiPickerDropdown extends React.PureComponent { }; const { active, loading } = this.state; + const title = intl.formatMessage(messages.emoji); return ( - <Dropdown ref={this.setRef} className='emoji-picker__dropdown' onShow={this.onShowDropdown} onHide={this.onHideDropdown}> - <DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)}> + <Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}> + <DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} > <img className={`emojione ${active && loading ? 'pulse-loading' : ''}`} alt='🙂' @@ -118,7 +135,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { <DropdownContent className='dropdown__left'> { this.state.active && !this.state.loading && - (<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} categories={categories} search />) + (<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} onKeyDown={this.onEmojiPickerKeyDown} categories={categories} search />) } </DropdownContent> </Dropdown> diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js index b0bc0958e..5000ea2f1 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.js +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js @@ -18,6 +18,7 @@ export default class NavigationBar extends ImmutablePureComponent { return ( <div className='navigation-bar'> <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <Avatar src={this.props.account.get('avatar')} staticSrc={this.props.account.get('avatar_static')} size={40} /> </Permalink> diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 9524f7501..da3c0a0ab 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -24,6 +24,10 @@ const iconStyle = { export default class PrivacyDropdown extends React.PureComponent { static propTypes = { + isUserTouching: PropTypes.func, + isModalOpen: PropTypes.bool.isRequired, + onModalOpen: PropTypes.func, + onModalClose: PropTypes.func, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -34,22 +38,55 @@ export default class PrivacyDropdown extends React.PureComponent { }; handleToggle = () => { - this.setState({ open: !this.state.open }); + if (this.props.isUserTouching()) { + if (this.state.open) { + this.props.onModalClose(); + } else { + this.props.onModalOpen({ + actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })), + onClick: this.handleModalActionClick, + }); + } + } else { + this.setState({ open: !this.state.open }); + } } - handleClick = (e) => { - const value = e.currentTarget.getAttribute('data-index'); + handleModalActionClick = (e) => { e.preventDefault(); - this.setState({ open: false }); + const { value } = this.options[e.currentTarget.getAttribute('data-index')]; + this.props.onModalClose(); this.props.onChange(value); } + handleClick = (e) => { + if (e.key === 'Escape') { + this.setState({ open: false }); + } else if (!e.key || e.key === 'Enter') { + const value = e.currentTarget.getAttribute('data-index'); + e.preventDefault(); + this.setState({ open: false }); + this.props.onChange(value); + } + } + onGlobalClick = (e) => { if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { this.setState({ open: false }); } } + componentWillMount () { + const { intl: { formatMessage } } = this.props; + + this.options = [ + { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, + { icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, + { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, + { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, + ]; + } + componentDidMount () { window.addEventListener('click', this.onGlobalClick); window.addEventListener('touchstart', this.onGlobalClick); @@ -68,25 +105,18 @@ export default class PrivacyDropdown extends React.PureComponent { const { value, intl } = this.props; const { open } = this.state; - const options = [ - { icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) }, - { icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) }, - { icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) }, - { icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) }, - ]; - - const valueOption = options.find(item => item.value === value); + const valueOption = this.options.find(item => item.value === value); return ( <div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}> - <div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div> + <div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} expanded={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div> <div className='privacy-dropdown__dropdown'> - {open && options.map(item => - <div role='button' tabIndex='0' key={item.value} data-index={item.value} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}> + {open && this.options.map(item => + <div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}> <div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div> <div className='privacy-dropdown__option__content'> - <strong>{item.shortText}</strong> - {item.longText} + <strong>{item.text}</strong> + {item.meta} </div> </div> )} diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index cdc7952c0..85ef767ab 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -52,15 +52,18 @@ export default class Search extends React.PureComponent { return ( <div className='search'> - <input - className='search__input' - type='text' - placeholder={intl.formatMessage(messages.placeholder)} - value={value} - onChange={this.handleChange} - onKeyUp={this.handleKeyDown} - onFocus={this.handleFocus} - /> + <label> + <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> + <input + className='search__input' + type='text' + placeholder={intl.formatMessage(messages.placeholder)} + value={value} + onChange={this.handleChange} + onKeyUp={this.handleKeyDown} + onFocus={this.handleFocus} + /> + </label> <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> <i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index badd6cfc5..70b28a2ba 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -57,16 +57,19 @@ export default class UploadButton extends ImmutablePureComponent { return ( <div className='compose-form__upload-button'> <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> - <input - key={resetFileKey} - ref={this.setRef} - type='file' - multiple={false} - accept={acceptContentTypes.toArray().join(',')} - onChange={this.handleChange} - disabled={disabled} - style={{ display: 'none' }} - /> + <label> + <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span> + <input + key={resetFileKey} + ref={this.setRef} + type='file' + multiple={false} + accept={acceptContentTypes.toArray().join(',')} + onChange={this.handleChange} + disabled={disabled} + style={{ display: 'none' }} + /> + </label> </div> ); } diff --git a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js index 9c05e054e..0ddf531d3 100644 --- a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js @@ -1,8 +1,11 @@ import { connect } from 'react-redux'; import PrivacyDropdown from '../components/privacy_dropdown'; import { changeComposeVisibility } from '../../../actions/compose'; +import { openModal, closeModal } from '../../../actions/modal'; +import { isUserTouching } from '../../../is_mobile'; const mapStateToProps = state => ({ + isModalOpen: state.get('modal').modalType === 'ACTIONS', value: state.getIn(['compose', 'privacy']), }); @@ -12,6 +15,10 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeComposeVisibility(value)); }, + isUserTouching, + onModalOpen: props => dispatch(openModal('ACTIONS', props)), + onModalClose: () => dispatch(closeModal()), + }); export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown); diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js index 63c0e8ae4..8624849f3 100644 --- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js @@ -15,6 +15,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ visible: state.getIn(['compose', 'media_attachments']).size > 0, active: state.getIn(['compose', 'sensitive']), + disabled: state.getIn(['compose', 'spoiler']), }); const mapDispatchToProps = dispatch => ({ @@ -30,12 +31,13 @@ class SensitiveButton extends React.PureComponent { static propTypes = { visible: PropTypes.bool, active: PropTypes.bool, + disabled: PropTypes.bool, onClick: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; render () { - const { visible, active, onClick, intl } = this.props; + const { visible, active, disabled, onClick, intl } = this.props; return ( <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> @@ -53,6 +55,7 @@ class SensitiveButton extends React.PureComponent { onClick={onClick} size={18} active={active} + disabled={disabled} style={{ lineHeight: null, height: null }} inverted /> diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 66b0746c5..f0bce1e40 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -76,23 +76,23 @@ export default class Compose extends React.PureComponent { if (multiColumn) { const { columns } = this.props; header = ( - <div className='drawer__header'> - <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link> + <nav className='drawer__header'> + <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link> {!columns.some(column => column.get('id') === 'HOME') && ( - <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' aria-label={intl.formatMessage(messages.home_timeline)} /></Link> + <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link> )} {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( - <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' aria-label={intl.formatMessage(messages.notifications)} /></Link> + <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link> )} {!columns.some(column => column.get('id') === 'COMMUNITY') && ( - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link> + <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link> )} {!columns.some(column => column.get('id') === 'PUBLIC') && ( - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link> + <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link> )} - <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a> - <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a> - </div> + <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)} aria-label={intl.formatMessage(messages.settings)}><i role='img' className='fa fa-fw fa-cogs' /></a> + <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a> + </nav> ); } diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js new file mode 100644 index 000000000..f0fea1a0e --- /dev/null +++ b/app/javascript/mastodon/features/compose/util/counter.js @@ -0,0 +1,7 @@ +const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; + +export function countableText(inputText) { + return inputText + .replace(/https?:\/\/\S+/g, urlPlaceholder) + .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2'); +}; diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 31cac5bc7..88a29d4d3 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -36,40 +36,48 @@ export default class ColumnSettings extends React.PureComponent { <ClearColumnButton onClick={onClear} /> </div> - <span 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} 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 role='group' aria-labelledby='notifications-follow'> + <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} 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> </div> - <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> + <div role='group' aria-labelledby='notifications-favourite'> + <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} 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 className='column-settings__row'> + <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> </div> - <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> + <div role='group' aria-labelledby='notifications-mention'> + <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} 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 className='column-settings__row'> + <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> </div> - <span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> + <div role='group' aria-labelledby='notifications-reblog'> + <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} 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 className='column-settings__row'> + <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> </div> </div> ); diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index be1ff91d6..a20e7ca51 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -18,13 +18,19 @@ export default class SettingToggle extends React.PureComponent { this.props.onChange(this.props.settingKey, target.checked); } + onKeyDown = e => { + if (e.key === ' ') { + this.props.onChange(this.props.settingKey, !e.target.checked); + } + } + render () { 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} /> + <Toggle id={id} checked={settings.getIn(settingKey)} 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/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 1eff04e97..a2885adda 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import IconButton from '../../../components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import DropdownMenu from '../../../components/dropdown_menu'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ @@ -13,6 +13,7 @@ const messages = defineMessages({ cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, + share: { id: 'status.share', defaultMessage: 'Share' }, }); @injectIntl @@ -58,6 +59,13 @@ export default class ActionBar extends React.PureComponent { this.props.onReport(this.props.status); } + handleShare = () => { + navigator.share({ + text: this.props.status.get('search_index'), + url: this.props.status.get('url'), + }); + } + render () { const { status, me, intl } = this.props; @@ -71,6 +79,10 @@ export default class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); } + const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( + <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div> + ); + let reblogIcon = 'retweet'; //if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; // else if (status.get('visibility') === 'private') reblogIcon = 'lock'; @@ -82,9 +94,10 @@ export default class ActionBar extends React.PureComponent { <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> + {shareButton} <div className='detailed-status__action-bar-dropdown'> - <DropdownMenu size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' /> + <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' /> </div> </div> ); diff --git a/app/javascript/mastodon/features/ui/components/actions_modal.js b/app/javascript/mastodon/features/ui/components/actions_modal.js new file mode 100644 index 000000000..cc0620d1c --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/actions_modal.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import StatusContent from '../../../components/status_content'; +import Avatar from '../../../components/avatar'; +import RelativeTimestamp from '../../../components/relative_timestamp'; +import DisplayName from '../../../components/display_name'; +import IconButton from '../../../components/icon_button'; + +export default class ActionsModal extends ImmutablePureComponent { + + static propTypes = { + actions: PropTypes.array, + onClick: PropTypes.func, + }; + + renderAction = (action, i) => { + if (action === null) { + return <li key={`sep-${i}`} className='dropdown__sep' />; + } + + const { icon = null, text, meta = null, active = false, href = '#' } = action; + + return ( + <li key={`${text}-${i}`}> + <a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={active && 'active'}> + {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' />} + <div> + <div>{text}</div> + <div>{meta}</div> + </div> + </a> + </li> + ); + } + + render () { + const status = this.props.status && ( + <div className='status light'> + <div className='boost-modal__status-header'> + <div className='boost-modal__status-time'> + <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> + <RelativeTimestamp timestamp={this.props.status.get('created_at')} /> + </a> + </div> + + <a href={this.props.status.getIn(['account', 'url'])} className='status__display-name'> + <div className='status__avatar'> + <Avatar src={this.props.status.getIn(['account', 'avatar'])} staticSrc={this.props.status.getIn(['account', 'avatar_static'])} size={48} /> + </div> + + <DisplayName account={this.props.status.get('account')} /> + </a> + </div> + + <StatusContent status={this.props.status} /> + </div> + ); + + return ( + <div className='modal-root__modal actions-modal'> + {status} + + <ul> + {this.props.actions.map(this.renderAction)} + </ul> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.js index ce1dca171..aea102aac 100644 --- a/app/javascript/mastodon/features/ui/components/column.js +++ b/app/javascript/mastodon/features/ui/components/column.js @@ -3,6 +3,7 @@ import ColumnHeader from './column_header'; import PropTypes from 'prop-types'; import { debounce } from 'lodash'; import scrollTop from '../../../scroll'; +import { isMobile } from '../../../is_mobile'; export default class Column extends React.PureComponent { @@ -37,13 +38,12 @@ export default class Column extends React.PureComponent { render () { const { heading, icon, children, active, hideHeadingOnMobile } = this.props; - let columnHeaderId = null; - let header = ''; + const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth))); - if (heading) { - columnHeaderId = heading.replace(/ /g, '-'); - header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} hideOnMobile={hideHeadingOnMobile} columnHeaderId={columnHeaderId} />; - } + const columnHeaderId = showHeading && heading.replace(/ /g, '-'); + const header = showHeading && ( + <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} /> + ); return ( <div ref={this.setRef} diff --git a/app/javascript/mastodon/features/ui/components/column_header.js b/app/javascript/mastodon/features/ui/components/column_header.js index dc601d6e1..af195ea9c 100644 --- a/app/javascript/mastodon/features/ui/components/column_header.js +++ b/app/javascript/mastodon/features/ui/components/column_header.js @@ -8,7 +8,6 @@ export default class ColumnHeader extends React.PureComponent { type: PropTypes.string, active: PropTypes.bool, onClick: PropTypes.func, - hideOnMobile: PropTypes.bool, columnHeaderId: PropTypes.string, }; @@ -17,7 +16,7 @@ export default class ColumnHeader extends React.PureComponent { } render () { - const { type, active, hideOnMobile, columnHeaderId } = this.props; + const { type, active, columnHeaderId } = this.props; let icon = ''; @@ -26,7 +25,7 @@ export default class ColumnHeader extends React.PureComponent { } return ( - <div role='button heading' tabIndex='0' className={`column-header ${active ? 'active' : ''} ${hideOnMobile ? 'hidden-on-mobile' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> + <div role='heading' tabIndex='0' className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> {icon} {type} </div> diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.js index cbc926581..06004e830 100644 --- a/app/javascript/mastodon/features/ui/components/column_link.js +++ b/app/javascript/mastodon/features/ui/components/column_link.js @@ -2,24 +2,24 @@ import React from 'react'; import PropTypes from 'prop-types'; import Link from 'react-router-dom/Link'; -const ColumnLink = ({ icon, text, to, onClick, href, method, hideOnMobile }) => { +const ColumnLink = ({ icon, text, to, onClick, href, method }) => { if (href) { return ( - <a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}> + <a href={href} className='column-link' data-method={method}> <i className={`fa fa-fw fa-${icon} column-link__icon`} /> {text} </a> ); } else if (to) { return ( - <Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}> + <Link to={to} className='column-link'> <i className={`fa fa-fw fa-${icon} column-link__icon`} /> {text} </Link> ); } else { return ( - <a onClick={onClick} role='button' tabIndex='0' className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}> + <a onClick={onClick} role='button' tabIndex='0' data-method={method}> <i className={`fa fa-fw fa-${icon} column-link__icon`} /> {text} </a> @@ -34,7 +34,6 @@ ColumnLink.propTypes = { onClick: PropTypes.func, href: PropTypes.string, method: PropTypes.string, - hideOnMobile: PropTypes.bool, }; export default ColumnLink; diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js index 7ecfaf77a..1c4058926 100644 --- a/app/javascript/mastodon/features/ui/components/column_loading.js +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -6,7 +6,7 @@ import ColumnHeader from '../../../components/column_header'; const ColumnLoading = ({ title = '', icon = ' ' }) => ( <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} /> + <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> <div className='scrollable' /> </Column> ); diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 7de66ce3f..63bd1b021 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -56,6 +56,15 @@ export default class ColumnsArea extends ImmutablePureComponent { handleSwipe = (index) => { this.pendingIndex = index; + + const nextLinkTranslationId = links[index].props['data-preview-title-id']; + const currentLinkSelector = '.tabs-bar__link.active'; + const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`; + + // HACK: Remove the active class from the current link and set it to the next one + // React-router does this for us, but too late, feeling laggy. + document.querySelector(currentLinkSelector).classList.remove('active'); + document.querySelector(nextLinkSelector).classList.add('active'); } handleAnimationEnd = () => { diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index dcc9becd3..828419d5a 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -10,6 +10,8 @@ import ImageLoader from './image_loader'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, + previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, + next: { id: 'lightbox.next', defaultMessage: 'Next' }, }); @injectIntl @@ -66,16 +68,10 @@ export default class MediaModal extends ImmutablePureComponent { const index = this.getIndex(); - let leftNav, rightNav, content; + const leftNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>; + const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>; - leftNav = rightNav = content = ''; - - if (media.size > 1) { - leftNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; - 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>; - } - - content = media.map((image) => { + const content = media.map((image) => { const width = image.getIn(['meta', 'original', 'width']) || null; const height = image.getIn(['meta', 'original', 'height']) || null; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 3da3f6391..d316ff433 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -5,6 +5,7 @@ import spring from 'react-motion/lib/spring'; import BundleContainer from '../containers/bundle_container'; import BundleModalError from './bundle_modal_error'; import ModalLoading from './modal_loading'; +import ActionsModal from '../components/actions_modal'; import { MediaModal, OnboardingModal, @@ -23,6 +24,7 @@ const MODAL_COMPONENTS = { 'CONFIRM': ConfirmationModal, 'REPORT': ReportModal, 'SETTINGS': SettingsModal, + 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), }; export default class ModalRoot extends React.PureComponent { @@ -44,10 +46,34 @@ export default class ModalRoot extends React.PureComponent { window.addEventListener('keyup', this.handleKeyUp, false); } + componentWillReceiveProps (nextProps) { + if (!!nextProps.type && !this.props.type) { + this.activeElement = document.activeElement; + + this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); + } + } + + componentDidUpdate (prevProps) { + if (!this.props.type && !!prevProps.type) { + this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); + this.activeElement.focus(); + this.activeElement = null; + } + } + componentWillUnmount () { window.removeEventListener('keyup', this.handleKeyUp); } + getSiblings = () => { + return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); + } + + setRef = ref => { + this.node = ref; + } + willEnter () { return { opacity: 0, scale: 0.98 }; } @@ -86,11 +112,11 @@ export default class ModalRoot extends React.PureComponent { willLeave={this.willLeave} > {interpolatedStyles => - <div className='modal-root'> + <div className='modal-root' ref={this.setRef}> {interpolatedStyles.map(({ key, data: { type, props }, style }) => ( <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})` }}> + <div role='dialog' 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}> {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} </BundleContainer> diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index baec86d0d..af9e6bf45 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -1,16 +1,19 @@ import React from 'react'; +import PropTypes from 'prop-types'; import NavLink from 'react-router-dom/NavLink'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import { debounce } from 'lodash'; +import { isUserTouching } from '../../../is_mobile'; export const links = [ - <NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, - <NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, - <NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, + <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, + <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, + <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, - <NavLink className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, - <NavLink className='tabs-bar__link secondary' activeClassName='active' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, + <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, + <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, - <NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='tabs_bar.federated_timeline' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>, + <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>, ]; export function getIndex (path) { @@ -21,13 +24,60 @@ export function getLink (index) { return links[index].props.to; } +@injectIntl export default class TabsBar extends React.Component { + static contextTypes = { + router: PropTypes.object.isRequired, + } + + static propTypes = { + intl: PropTypes.object.isRequired, + } + + setRef = ref => { + this.node = ref; + } + + handleClick = (e) => { + // Only apply optimization for touch devices, which we assume are slower + // We thus avoid the 250ms delay for non-touch devices and the lag for touch devices + if (isUserTouching()) { + e.preventDefault(); + e.persist(); + + requestAnimationFrame(() => { + const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link')); + const currentTab = tabs.find(tab => tab.classList.contains('active')); + const nextTab = tabs.find(tab => tab.contains(e.target)); + const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)]; + + + if (currentTab !== nextTab) { + if (currentTab) { + currentTab.classList.remove('active'); + } + + const listener = debounce(() => { + nextTab.removeEventListener('transitionend', listener); + this.context.router.history.push(to); + }, 50); + + nextTab.addEventListener('transitionend', listener); + nextTab.classList.add('active'); + } + }); + } + + } + render () { + const { intl: { formatMessage } } = this.props; + return ( - <div className='tabs-bar'> - {React.Children.toArray(links)} - </div> + <nav className='tabs-bar' ref={this.setRef}> + {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} + </nav> ); } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index eb499c836..f7a6eb319 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -52,6 +52,10 @@ const mapStateToProps = state => ({ @connect(mapStateToProps) export default class UI extends React.PureComponent { + static contextTypes = { + router: PropTypes.object.isRequired, + } + static propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, @@ -129,6 +133,14 @@ export default class UI extends React.PureComponent { this.setState({ draggingOver: false }); } + handleServiceWorkerPostMessage = ({ data }) => { + if (data.type === 'navigate') { + this.context.router.history.push(data.path); + } else { + console.warn('Unknown message type:', data.type); // eslint-disable-line no-console + } + } + componentWillMount () { window.addEventListener('resize', this.handleResize, { passive: true }); document.addEventListener('dragenter', this.handleDragEnter, false); @@ -137,6 +149,10 @@ export default class UI extends React.PureComponent { document.addEventListener('dragleave', this.handleDragLeave, false); document.addEventListener('dragend', this.handleDragEnd, false); + if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); + } + this.props.dispatch(refreshHomeTimeline()); this.props.dispatch(refreshNotifications()); } diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index b7c521ac3..9267519dd 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -110,9 +110,9 @@ export function SettingsModal () { // IF MASTODON EVER CHANGES DETAILED STATUSES TO REQUIRE THEM, WE'LL NEED TO UPDATE THE URLS OR SOMETHING LOL. // export function MediaGallery () { - return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery'); + return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery'); } export function VideoPlayer () { - return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player'); + return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player'); } diff --git a/app/javascript/mastodon/is_mobile.js b/app/javascript/mastodon/is_mobile.js index 014a9a8d5..129d66682 100644 --- a/app/javascript/mastodon/is_mobile.js +++ b/app/javascript/mastodon/is_mobile.js @@ -12,6 +12,15 @@ export function isMobile(width, columns) { }; const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; +let userTouching = false; + +window.addEventListener('touchstart', () => { + userTouching = true; +}, { once: true }); + +export function isUserTouching() { + return userTouching; +} export function isIOS() { return iOS; diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js index df7889118..8927b7358 100644 --- a/app/javascript/mastodon/load_polyfills.js +++ b/app/javascript/mastodon/load_polyfills.js @@ -24,6 +24,8 @@ function loadPolyfills() { // This avoids shipping them all the polyfills. const needsExtraPolyfills = !( window.IntersectionObserver && + window.IntersectionObserverEntry && + 'isIntersecting' in IntersectionObserverEntry.prototype && window.requestIdleCallback && 'object-fit' in (new Image()).style ); diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 7f27d78cd..f5cf77f92 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -13,6 +13,7 @@ "account.posts": "المشاركات", "account.report": "أبلغ عن @{name}", "account.requested": "في انتظار الموافقة", + "account.share": "Share @{name}'s profile", "account.unblock": "إلغاء الحظر عن @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "إلغاء المتابعة", @@ -34,7 +35,11 @@ "column.notifications": "الإشعارات", "column.public": "الخيط العام الموحد", "column_back_button.label": "العودة", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "التصفح", "column_subheading.settings": "الإعدادات", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "عرض الردود", "home.settings": "إعدادات العمود", "lightbox.close": "إغلاق", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "تحميل ...", "media_gallery.toggle_visible": "عرض / إخفاء", "missing_indicator.label": "تعذر العثور عليه", @@ -168,6 +175,7 @@ "status.report": "إبلِغ عن @{name}", "status.sensitive_toggle": "اضغط للعرض", "status.sensitive_warning": "محتوى حساس", + "status.share": "Share", "status.show_less": "إعرض أقلّ", "status.show_more": "أظهر المزيد", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 68aaf56b0..e6788f9eb 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -13,6 +13,7 @@ "account.posts": "Публикации", "account.report": "Report @{name}", "account.requested": "В очакване на одобрение", + "account.share": "Share @{name}'s profile", "account.unblock": "Не блокирай", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Не следвай", @@ -34,7 +35,11 @@ "column.notifications": "Известия", "column.public": "Публичен канал", "column_back_button.label": "Назад", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", "lightbox.close": "Затвори", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Зареждане...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -168,6 +175,7 @@ "status.report": "Report @{name}", "status.sensitive_toggle": "Покажи", "status.sensitive_warning": "Деликатно съдържание", + "status.share": "Share", "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 6fdcde4b4..95b3c60bf 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -13,6 +13,7 @@ "account.posts": "Publicacions", "account.report": "Informe @{name}", "account.requested": "Esperant aprovació", + "account.share": "Share @{name}'s profile", "account.unblock": "Desbloquejar @{name}", "account.unblock_domain": "Mostra {domain}", "account.unfollow": "Deixar de seguir", @@ -34,7 +35,11 @@ "column.notifications": "Notificacions", "column.public": "Línia de temps federada", "column_back_button.label": "Enrere", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navegació", "column_subheading.settings": "Configuració", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Mostrar respostes", "home.settings": "Ajustos de columna", "lightbox.close": "Tancar", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Carregant...", "media_gallery.toggle_visible": "Alternar visibilitat", "missing_indicator.label": "No trobat", @@ -168,6 +175,7 @@ "status.report": "Informar sobre @{name}", "status.sensitive_toggle": "Clic per veure", "status.sensitive_warning": "Contingut sensible", + "status.share": "Share", "status.show_less": "Mostra menys", "status.show_more": "Mostra més", "status.unmute_conversation": "Activar conversació", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index f911c7b75..67a99b765 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -13,6 +13,7 @@ "account.posts": "Beiträge", "account.report": "@{name} melden", "account.requested": "Warte auf Erlaubnis", + "account.share": "Share @{name}'s profile", "account.unblock": "@{name} entblocken", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Entfolgen", @@ -34,7 +35,11 @@ "column.notifications": "Mitteilungen", "column.public": "Gesamtes bekanntes Netz", "column_back_button.label": "Zurück", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Antworten anzeigen", "home.settings": "Spalteneinstellungen", "lightbox.close": "Schließen", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Lade…", "media_gallery.toggle_visible": "Sichtbarkeit einstellen", "missing_indicator.label": "Nicht gefunden", @@ -168,6 +175,7 @@ "status.report": "@{name} melden", "status.sensitive_toggle": "Klicke, um sie zu sehen", "status.sensitive_warning": "Heikle Inhalte", + "status.share": "Share", "status.show_less": "Weniger anzeigen", "status.show_more": "Mehr anzeigen", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index a7b8f01d0..e5d541cd6 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -54,6 +54,22 @@ { "descriptors": [ { + "defaultMessage": "Show settings", + "id": "column_header.show_settings" + }, + { + "defaultMessage": "Hide settings", + "id": "column_header.hide_settings" + }, + { + "defaultMessage": "Move column to the left", + "id": "column_header.moveLeft_settings" + }, + { + "defaultMessage": "Move column to the right", + "id": "column_header.moveRight_settings" + }, + { "defaultMessage": "Unpin", "id": "column_header.unpin" }, @@ -139,6 +155,10 @@ "id": "status.reply" }, { + "defaultMessage": "Share", + "id": "status.share" + }, + { "defaultMessage": "Reply to thread", "id": "status.replyAll" }, @@ -355,6 +375,10 @@ "id": "account.report" }, { + "defaultMessage": "Share @{name}'s profile", + "id": "account.share" + }, + { "defaultMessage": "Media", "id": "account.media" }, @@ -1007,6 +1031,10 @@ { "defaultMessage": "Report @{name}", "id": "status.report" + }, + { + "defaultMessage": "Share", + "id": "status.share" } ], "path": "app/javascript/mastodon/features/status/components/action_bar.json" @@ -1085,6 +1113,14 @@ { "defaultMessage": "Close", "id": "lightbox.close" + }, + { + "defaultMessage": "Previous", + "id": "lightbox.previous" + }, + { + "defaultMessage": "Next", + "id": "lightbox.next" } ], "path": "app/javascript/mastodon/features/ui/components/media_modal.json" diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 3a201b9c1..2ea2062d3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -13,6 +13,7 @@ "account.posts": "Posts", "account.report": "Report @{name}", "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", "account.unblock": "Unblock @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Unfollow", @@ -34,7 +35,11 @@ "column.notifications": "Notifications", "column.public": "Federated timeline", "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", "lightbox.close": "Close", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -168,6 +175,7 @@ "status.report": "Report @{name}", "status.sensitive_toggle": "Click to view", "status.sensitive_warning": "Sensitive content", + "status.share": "Share", "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 0bb5159c8..960d747ec 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -13,6 +13,7 @@ "account.posts": "Mesaĝoj", "account.report": "Report @{name}", "account.requested": "Atendas aprobon", + "account.share": "Share @{name}'s profile", "account.unblock": "Malbloki @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Malsekvi", @@ -34,7 +35,11 @@ "column.notifications": "Sciigoj", "column.public": "Fratara tempolinio", "column_back_button.label": "Reveni", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", "lightbox.close": "Fermi", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Ŝarĝanta...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -168,6 +175,7 @@ "status.report": "Report @{name}", "status.sensitive_toggle": "Alklaki por vidi", "status.sensitive_warning": "Tikla enhavo", + "status.share": "Share", "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index a39b608c6..212d16639 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -13,6 +13,7 @@ "account.posts": "Publicaciones", "account.report": "Report @{name}", "account.requested": "Esperando aprobación", + "account.share": "Share @{name}'s profile", "account.unblock": "Desbloquear", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Dejar de seguir", @@ -34,7 +35,11 @@ "column.notifications": "Notificaciones", "column.public": "Historia federada", "column_back_button.label": "Atrás", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", "lightbox.close": "Cerrar", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -168,6 +175,7 @@ "status.report": "Reportar", "status.sensitive_toggle": "Click para ver", "status.sensitive_warning": "Contenido sensible", + "status.share": "Share", "status.show_less": "Mostrar menos", "status.show_more": "Mostrar más", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 6842558d9..d2682ef12 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -13,18 +13,19 @@ "account.posts": "نوشتهها", "account.report": "گزارش @{name}", "account.requested": "در انتظار پذیرش", + "account.share": "Share @{name}'s profile", "account.unblock": "رفع انسداد @{name}", "account.unblock_domain": "رفع پنهانسازی از {domain}", "account.unfollow": "پایان پیگیری", "account.unmute": "باصدا کردن @{name}", - "account.view_full_profile": "View full profile", + "account.view_full_profile": "نمایش نمایهٔ کامل", "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", - "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", + "bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.", + "bundle_column_error.retry": "تلاش دوباره", + "bundle_column_error.title": "خطای شبکه", + "bundle_modal_error.close": "بستن", + "bundle_modal_error.message": "هنگام بازکردن این بخش خطایی رخ داد.", + "bundle_modal_error.retry": "تلاش دوباره", "column.blocks": "کاربران مسدودشده", "column.community": "نوشتههای محلی", "column.favourites": "پسندیدهها", @@ -34,8 +35,12 @@ "column.notifications": "اعلانها", "column.public": "نوشتههای همهجا", "column_back_button.label": "بازگشت", - "column_header.pin": "Pin", - "column_header.unpin": "Unpin", + "column_header.hide_settings": "نهفتن تنظیمات", + "column_header.moveLeft_settings": "انتقال ستون به چپ", + "column_header.moveRight_settings": "انتقال ستون به راست", + "column_header.pin": "ثابتکردن", + "column_header.show_settings": "نمایش تنظیمات", + "column_header.unpin": "رهاکردن", "column_subheading.navigation": "گشت و گذار", "column_subheading.settings": "تنظیمات", "compose_form.lock_disclaimer": "حساب شما {locked} نیست. هر کسی میتواند پیگیر شما شود و نوشتههای ویژهٔ پیگیران شما را ببیند.", @@ -56,8 +61,8 @@ "confirmations.domain_block.message": "آیا جدی جدی میخواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقتها مسدودکردن یا بیصداکردن چند حساب کاربری خاص کافی است و توصیه میشود.", "confirmations.mute.confirm": "بیصدا کن", "confirmations.mute.message": "آیا واقعاً میخواهید {name} را بیصدا کنید؟", - "confirmations.unfollow.confirm": "Unfollow", - "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "confirmations.unfollow.confirm": "لغو پیگیری", + "confirmations.unfollow.message": "آیا واقعاً میخواهید به پیگیری از {name} پایان دهید؟", "emoji_button.activity": "فعالیت", "emoji_button.flags": "پرچمها", "emoji_button.food": "غذا و نوشیدنی", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "نمایش پاسخها", "home.settings": "تنظیمات ستون", "lightbox.close": "بستن", + "lightbox.next": "بعدی", + "lightbox.previous": "قبلی", "loading_indicator.label": "بارگیری...", "media_gallery.toggle_visible": "تغییر پیدایی", "missing_indicator.label": "پیدا نشد", @@ -112,8 +119,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.push": "اعلانها از سمت سرور", + "notifications.column_settings.push_meta": "این دستگاه", "notifications.column_settings.reblog": "بازبوقها:", "notifications.column_settings.show": "نمایش در ستون", "notifications.column_settings.sound": "پخش صدا", @@ -152,7 +159,7 @@ "report.target": "گزارشدادن", "search.placeholder": "جستجو", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", - "standalone.public_title": "A look inside...", + "standalone.public_title": "نگاهی به کاربران این سرور...", "status.cannot_reblog": "این نوشته را نمیشود بازبوقید", "status.delete": "پاککردن", "status.favourite": "پسندیدن", @@ -168,6 +175,7 @@ "status.report": "گزارش دادن @{name}", "status.sensitive_toggle": "برای دیدن کلیک کنید", "status.sensitive_warning": "محتوای حساس", + "status.share": "همرسانی", "status.show_less": "نهفتن", "status.show_more": "نمایش", "status.unmute_conversation": "باصداکردن گفتگو", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index efc9b1053..cb9e9c2a6 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -13,6 +13,7 @@ "account.posts": "Postit", "account.report": "Report @{name}", "account.requested": "Odottaa hyväksyntää", + "account.share": "Share @{name}'s profile", "account.unblock": "Salli @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Lopeta seuraaminen", @@ -34,7 +35,11 @@ "column.notifications": "Ilmoitukset", "column.public": "Yleinen aikajana", "column_back_button.label": "Takaisin", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", "lightbox.close": "Sulje", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Ladataan...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -168,6 +175,7 @@ "status.report": "Report @{name}", "status.sensitive_toggle": "Klikkaa nähdäksesi", "status.sensitive_warning": "Arkaluontoista sisältöä", + "status.share": "Share", "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 3cc1f152a..ad9060d25 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -13,11 +13,12 @@ "account.posts": "Statuts", "account.report": "Signaler", "account.requested": "Invitation envoyée", + "account.share": "Share @{name}'s profile", "account.unblock": "Débloquer", "account.unblock_domain": "Ne plus masquer {domain}", "account.unfollow": "Ne plus suivre", "account.unmute": "Ne plus masquer", -"account.view_full_profile": "Afficher le profil complet", + "account.view_full_profile": "Afficher le profil complet", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois", "bundle_column_error.body": "Une erreur s'est produite lors du chargement de ce composant.", "bundle_column_error.retry": "Réessayer", @@ -34,7 +35,11 @@ "column.notifications": "Notifications", "column.public": "Fil public global", "column_back_button.label": "Retour", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Épingler", + "column_header.show_settings": "Show settings", "column_header.unpin": "Retirer", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Paramètres", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Afficher les réponses", "home.settings": "Paramètres de la colonne", "lightbox.close": "Fermer", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Chargement…", "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", @@ -168,6 +175,7 @@ "status.report": "Signaler @{name}", "status.sensitive_toggle": "Cliquer pour afficher", "status.sensitive_warning": "Contenu sensible", + "status.share": "Share", "status.show_less": "Replier", "status.show_more": "Déplier", "status.unmute_conversation": "Ne plus masquer la conversation", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 36be0842b..34266d8e1 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -13,6 +13,7 @@ "account.posts": "הודעות", "account.report": "לדווח על @{name}", "account.requested": "בהמתנה לאישור", + "account.share": "Share @{name}'s profile", "account.unblock": "הסרת חסימה מעל @{name}", "account.unblock_domain": "הסר חסימה מקהילת {domain}", "account.unfollow": "הפסקת מעקב", @@ -34,7 +35,11 @@ "column.notifications": "התראות", "column.public": "בפרהסיה", "column_back_button.label": "חזרה", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "ניווט", "column_subheading.settings": "אפשרויות", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "הצגת תגובות", "home.settings": "הגדרות טור", "lightbox.close": "סגירה", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "טוען...", "media_gallery.toggle_visible": "נראה\\בלתי נראה", "missing_indicator.label": "לא נמצא", @@ -168,6 +175,7 @@ "status.report": "דיווח על @{name}", "status.sensitive_toggle": "לחצו כדי לראות", "status.sensitive_warning": "תוכן רגיש", + "status.share": "Share", "status.show_less": "הראה פחות", "status.show_more": "הראה יותר", "status.unmute_conversation": "הסרת השתקת שיחה", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 363c4c490..f69b096d4 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -13,6 +13,7 @@ "account.posts": "Postovi", "account.report": "Prijavi @{name}", "account.requested": "Čeka pristanak", + "account.share": "Share @{name}'s profile", "account.unblock": "Deblokiraj @{name}", "account.unblock_domain": "Otkrij {domain}", "account.unfollow": "Prestani slijediti", @@ -34,7 +35,11 @@ "column.notifications": "Notifikacije", "column.public": "Federalni timeline", "column_back_button.label": "Natrag", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigacija", "column_subheading.settings": "Postavke", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Pokaži odgovore", "home.settings": "Postavke Stupca", "lightbox.close": "Zatvori", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Nije nađen", @@ -168,6 +175,7 @@ "status.report": "Prijavi @{name}", "status.sensitive_toggle": "Klikni da bi vidio", "status.sensitive_warning": "Osjetljiv sadržaj", + "status.share": "Share", "status.show_less": "Pokaži manje", "status.show_more": "Pokaži više", "status.unmute_conversation": "Poništi utišavanje razgovora", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index d43570f0d..4d2a50963 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -13,6 +13,7 @@ "account.posts": "Posts", "account.report": "Report @{name}", "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", "account.unblock": "Blokkolás levétele", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Követés abbahagyása", @@ -34,7 +35,11 @@ "column.notifications": "Értesítések", "column.public": "Nyilvános", "column_back_button.label": "Vissza", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", "lightbox.close": "Bezárás", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Betöltés...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -168,6 +175,7 @@ "status.report": "Report @{name}", "status.sensitive_toggle": "Katt a megtekintéshez", "status.sensitive_warning": "Érzékeny tartalom", + "status.share": "Share", "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 916f313bb..532739e3c 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -13,6 +13,7 @@ "account.posts": "Postingan", "account.report": "Laporkan @{name}", "account.requested": "Menunggu persetujuan", + "account.share": "Share @{name}'s profile", "account.unblock": "Hapus blokir @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Berhenti mengikuti", @@ -34,7 +35,11 @@ "column.notifications": "Notifikasi", "column.public": "Linimasa gabunggan", "column_back_button.label": "Kembali", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigasi", "column_subheading.settings": "Pengaturan", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Tampilkan balasan", "home.settings": "Pengaturan kolom", "lightbox.close": "Tutup", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Tunggu sebentar...", "media_gallery.toggle_visible": "Tampil/Sembunyikan", "missing_indicator.label": "Tidak ditemukan", @@ -168,6 +175,7 @@ "status.report": "Laporkan @{name}", "status.sensitive_toggle": "Klik untuk menampilkan", "status.sensitive_warning": "Konten sensitif", + "status.share": "Share", "status.show_less": "Tampilkan lebih sedikit", "status.show_more": "Tampilkan semua", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index a87cc9328..a5e363e40 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -13,6 +13,7 @@ "account.posts": "Mesaji", "account.report": "Denuncar @{name}", "account.requested": "Vartante aprobo", + "account.share": "Share @{name}'s profile", "account.unblock": "Desblokusar @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Ne plus sequar", @@ -34,7 +35,11 @@ "column.notifications": "Savigi", "column.public": "Federata tempolineo", "column_back_button.label": "Retro", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Montrar respondi", "home.settings": "Aranji di la kolumno", "lightbox.close": "Klozar", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Kargante...", "media_gallery.toggle_visible": "Chanjar videbleso", "missing_indicator.label": "Ne trovita", @@ -168,6 +175,7 @@ "status.report": "Denuncar @{name}", "status.sensitive_toggle": "Kliktar por vidar", "status.sensitive_warning": "Trubliva kontenajo", + "status.share": "Share", "status.show_less": "Montrar mine", "status.show_more": "Montrar plue", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 243ed7344..329eb82ca 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -13,6 +13,7 @@ "account.posts": "Posts", "account.report": "Segnala @{name}", "account.requested": "In attesa di approvazione", + "account.share": "Share @{name}'s profile", "account.unblock": "Sblocca @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Non seguire", @@ -34,7 +35,11 @@ "column.notifications": "Notifiche", "column.public": "Timeline federata", "column_back_button.label": "Indietro", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Mostra risposte", "home.settings": "Impostazioni colonna", "lightbox.close": "Chiudi", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Carico...", "media_gallery.toggle_visible": "Imposta visibilità", "missing_indicator.label": "Non trovato", @@ -168,6 +175,7 @@ "status.report": "Segnala @{name}", "status.sensitive_toggle": "Clicca per vedere", "status.sensitive_warning": "Materiale sensibile", + "status.share": "Share", "status.show_less": "Mostra meno", "status.show_more": "Mostra di più", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index ca36122f7..4c98086bb 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -1,7 +1,7 @@ { "account.block": "ブロック", "account.block_domain": "{domain}全体を非表示", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.disclaimer_full": "以下の情報は不正確な可能性があります。", "account.edit_profile": "プロフィールを編集", "account.follow": "フォロー", "account.followers": "フォロワー", @@ -13,11 +13,12 @@ "account.posts": "投稿", "account.report": "通報", "account.requested": "承認待ち", + "account.share": "@{name} のプロフィールを共有する", "account.unblock": "ブロック解除", "account.unblock_domain": "{domain}を表示", "account.unfollow": "フォロー解除", "account.unmute": "ミュート解除", - "account.view_full_profile": "View full profile", + "account.view_full_profile": "全ての情報を見る", "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。", "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。", "bundle_column_error.retry": "再試行", @@ -34,7 +35,11 @@ "column.notifications": "通知", "column.public": "連合タイムライン", "column_back_button.label": "戻る", + "column_header.hide_settings": "設定を隠す", + "column_header.moveLeft_settings": "カラムを左に移動する", + "column_header.moveRight_settings": "カラムを右に移動する", "column_header.pin": "ピン留めする", + "column_header.show_settings": "設定を表示", "column_header.unpin": "ピン留めを外す", "column_subheading.navigation": "ナビゲーション", "column_subheading.settings": "設定", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "返信表示", "home.settings": "カラム設定", "lightbox.close": "閉じる", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "読み込み中...", "media_gallery.toggle_visible": "表示切り替え", "missing_indicator.label": "見つかりません", @@ -149,7 +156,7 @@ "reply_indicator.cancel": "キャンセル", "report.placeholder": "コメント", "report.submit": "通報する", - "report.target": "問題のユーザー", + "report.target": "{target} を通報する", "search.placeholder": "検索", "search_results.total": "{count, number}件の結果", "standalone.public_title": "連合タイムライン", @@ -168,6 +175,7 @@ "status.report": "通報", "status.sensitive_toggle": "クリックして表示", "status.sensitive_warning": "閲覧注意", + "status.share": "共有", "status.show_less": "隠す", "status.show_more": "もっと見る", "status.unmute_conversation": "会話のミュートを解除", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 768efa37d..47d0d4087 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -13,6 +13,7 @@ "account.posts": "포스트", "account.report": "신고", "account.requested": "승인 대기 중", + "account.share": "Share @{name}'s profile", "account.unblock": "차단 해제", "account.unblock_domain": "{domain} 숨김 해제", "account.unfollow": "팔로우 해제", @@ -34,7 +35,11 @@ "column.notifications": "알림", "column.public": "연합 타임라인", "column_back_button.label": "돌아가기", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "고정하기", + "column_header.show_settings": "Show settings", "column_header.unpin": "고정 해제", "column_subheading.navigation": "내비게이션", "column_subheading.settings": "설정", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "답글 표시", "home.settings": "컬럼 설정", "lightbox.close": "닫기", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "불러오는 중...", "media_gallery.toggle_visible": "표시 전환", "missing_indicator.label": "찾을 수 없습니다", @@ -168,6 +175,7 @@ "status.report": "신고", "status.sensitive_toggle": "클릭해서 표시하기", "status.sensitive_warning": "민감한 미디어", + "status.share": "Share", "status.show_less": "숨기기", "status.show_more": "더 보기", "status.unmute_conversation": "이 대화의 뮤트 해제하기", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index fbfabc5d1..4d68c7992 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -13,11 +13,12 @@ "account.posts": "Toots", "account.report": "Rapporteer @{name}", "account.requested": "Wacht op goedkeuring", + "account.share": "Profiel van @{name} delen", "account.unblock": "Deblokkeer @{name}", "account.unblock_domain": "{domain} niet meer negeren", "account.unfollow": "Ontvolgen", "account.unmute": "@{name} niet meer negeren", - "account.view_full_profile": "Volledig profiel tonen", + "account.view_full_profile": "Volledig profiel tonen", "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.", "bundle_column_error.retry": "Opnieuw proberen", @@ -34,7 +35,11 @@ "column.notifications": "Meldingen", "column.public": "Globale tijdlijn", "column_back_button.label": "terug", + "column_header.hide_settings": "Instellingen verbergen", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Vastmaken", + "column_header.show_settings": "Instellingen tonen", "column_header.unpin": "Losmaken", "column_subheading.navigation": "Navigatie", "column_subheading.settings": "Instellingen", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Reacties tonen", "home.settings": "Kolom-instellingen", "lightbox.close": "Sluiten", + "lightbox.next": "Volgende", + "lightbox.previous": "Vorige", "loading_indicator.label": "Laden…", "media_gallery.toggle_visible": "Media wel/niet tonen", "missing_indicator.label": "Niet gevonden", @@ -147,12 +154,12 @@ "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", "privacy.unlisted.short": "Minder openbaar", "reply_indicator.cancel": "Annuleren", - "report.heading": "Rapporteren", "report.placeholder": "Extra opmerkingen", "report.submit": "Verzenden", "report.target": "Rapporteren van", "search.placeholder": "Zoeken", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", + "standalone.public_title": "Een kijkje binnenin...", "status.cannot_reblog": "Deze toot kan niet geboost worden", "status.delete": "Verwijderen", "status.favourite": "Favoriet", @@ -166,8 +173,9 @@ "status.reply": "Reageren", "status.replyAll": "Reageer op iedereen", "status.report": "Rapporteer @{name}", - "status.sensitive_toggle": "Klik om te zien", + "status.sensitive_toggle": "Klik om te bekijken", "status.sensitive_warning": "Gevoelige inhoud", + "status.share": "Delen", "status.show_less": "Minder tonen", "status.show_more": "Meer tonen", "status.unmute_conversation": "Conversatie niet meer negeren", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 8727f6147..9453e65ff 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -13,6 +13,7 @@ "account.posts": "Innlegg", "account.report": "Rapportér @{name}", "account.requested": "Venter på godkjennelse", + "account.share": "Share @{name}'s profile", "account.unblock": "Avblokker @{name}", "account.unblock_domain": "Vis {domain}", "account.unfollow": "Avfølg", @@ -34,7 +35,11 @@ "column.notifications": "Varsler", "column.public": "Felles tidslinje", "column_back_button.label": "Tilbake", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigasjon", "column_subheading.settings": "Innstillinger", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Vis svar", "home.settings": "Kolonneinnstillinger", "lightbox.close": "Lukk", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Laster...", "media_gallery.toggle_visible": "Veksle synlighet", "missing_indicator.label": "Ikke funnet", @@ -168,6 +175,7 @@ "status.report": "Rapporter @{name}", "status.sensitive_toggle": "Klikk for å vise", "status.sensitive_warning": "Følsomt innhold", + "status.share": "Share", "status.show_less": "Vis mindre", "status.show_more": "Vis mer", "status.unmute_conversation": "Ikke demp samtale", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index c39d57333..e2a5d7c59 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -1,7 +1,7 @@ { "account.block": "Blocar @{name}", "account.block_domain": "Tot amagar del domeni {domain}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incompletas.", "account.edit_profile": "Modificar lo perfil", "account.follow": "Sègre", "account.followers": "Seguidors", @@ -13,18 +13,19 @@ "account.posts": "Estatuts", "account.report": "Senhalar @{name}", "account.requested": "Invitacion mandada", + "account.share": "Partejar lo perfil a @{name}", "account.unblock": "Desblocar @{name}", "account.unblock_domain": "Desblocar {domain}", "account.unfollow": "Quitar de sègre", "account.unmute": "Quitar de rescondre @{name}", - "account.view_full_profile": "View full profile", + "account.view_full_profile": "Veire lo perfil complet", "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven", "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.", - "bundle_column_error.retry": "Tornar ensejar", + "bundle_column_error.retry": "Tornar ensajar", "bundle_column_error.title": "Error de ret", "bundle_modal_error.close": "Tampar", - "bundle_modal_error.message": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.", - "bundle_modal_error.retry": "Tornar ensejar", + "bundle_modal_error.message": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.", + "bundle_modal_error.retry": "Tornar ensajar", "column.blocks": "Personas blocadas", "column.community": "Flux public local", "column.favourites": "Favorits", @@ -34,7 +35,11 @@ "column.notifications": "Notificacions", "column.public": "Flux public global", "column_back_button.label": "Tornar", + "column_header.hide_settings": "Amagar los paramètres", + "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha", + "column_header.moveRight_settings": "Desplaçar la colomna a man esquèrra", "column_header.pin": "Penjar", + "column_header.show_settings": "Mostrar los paramètres", "column_header.unpin": "Despenjar", "column_subheading.navigation": "Navigacion", "column_subheading.settings": "Paramètres", @@ -46,35 +51,35 @@ "compose_form.publish_loud": "{publish} !", "compose_form.sensitive": "Marcar lo mèdia coma sensible", "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment", - "compose_form.spoiler_placeholder": "Avertiment", + "compose_form.spoiler_placeholder": "Escrivètz l’avertiment aquí", "confirmation_modal.cancel": "Anullar", "confirmations.block.confirm": "Blocar", "confirmations.block.message": "Sètz segur de voler blocar {name} ?", "confirmations.delete.confirm": "Suprimir", "confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?", "confirmations.domain_block.confirm": "Amagar tot lo domeni", - "confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.", + "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.", "confirmations.mute.confirm": "Metre en silenci", "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", "confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?", - "emoji_button.activity": "Activitat", + "emoji_button.activity": "Activitats", "emoji_button.flags": "Drapèus", "emoji_button.food": "Beure e manjar", "emoji_button.label": "Inserir un emoji", "emoji_button.nature": "Natura", "emoji_button.objects": "Objèctes", "emoji_button.people": "Gents", - "emoji_button.search": "Cercar...", + "emoji_button.search": "Cercar…", "emoji_button.symbols": "Simbòls", "emoji_button.travel": "Viatges & lòcs", - "empty_column.community": "Lo flux public local es void. Escribètz quicòm per lo garnir !", + "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !", "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag", - "empty_column.home": "Pel moment segètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.", + "empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.", "empty_column.home.inactivity": "Vòstra pagina d’acuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.", "empty_column.home.public_timeline": "lo flux public", "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.", - "empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.", + "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.", "follow_request.authorize": "Autorizar", "follow_request.reject": "Regetar", "getting_started.appsshort": "Apps", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Mostrar las responsas", "home.settings": "Paramètres de la colomna", "lightbox.close": "Tampar", + "lightbox.next": "Seguent", + "lightbox.previous": "Precedent", "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "missing_indicator.label": "Pas trobat", @@ -103,11 +110,11 @@ "navigation_bar.preferences": "Preferéncias", "navigation_bar.public_timeline": "Flux public global", "notification.favourite": "{name} a ajustat a sos favorits :", - "notification.follow": "{name} vos sèc.", + "notification.follow": "{name} vos sèc", "notification.mention": "{name} vos a mencionat :", "notification.reblog": "{name} a partejat vòstre estatut :", - "notifications.clear": "Levar", - "notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?", + "notifications.clear": "Escafar", + "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?", "notifications.column_settings.alert": "Notificacions localas", "notifications.column_settings.favourite": "Favorits :", "notifications.column_settings.follow": "Nòus seguidors :", @@ -119,15 +126,15 @@ "notifications.column_settings.sound": "Emetre un son", "onboarding.done": "Fach", "onboarding.next": "Seguent", - "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra intància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.", + "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.", "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.", - "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un enteragís amb vos", + "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos", "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.", "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}", "onboarding.page_one.welcome": "Benvengut a Mastodon !", "onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.", "onboarding.page_six.almost_done": "Gaireben acabat…", - "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.appetoot": "Bon Appetut!", "onboarding.page_six.apps_available": "I a d’aplicacions per mobil per iOS, Android e mai.", "onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.", "onboarding.page_six.guidelines": "guida de la comunitat", @@ -168,6 +175,7 @@ "status.report": "Senhalar @{name}", "status.sensitive_toggle": "Clicar per mostrar", "status.sensitive_warning": "Contengut sensible", + "status.share": "Partejar", "status.show_less": "Tornar plegar", "status.show_more": "Desplegar", "status.unmute_conversation": "Conversacions amb silenci levat", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index af069b6d7..c42721f64 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -13,6 +13,7 @@ "account.posts": "Posty", "account.report": "Zgłoś @{name}", "account.requested": "Oczekująca prośba", + "account.share": "Udostępnij profil @{name}", "account.unblock": "Odblokuj @{name}", "account.unblock_domain": "Odblokuj domenę {domain}", "account.unfollow": "Przestań śledzić", @@ -34,7 +35,11 @@ "column.notifications": "Powiadomienia", "column.public": "Globalna oś czasu", "column_back_button.label": "Wróć", + "column_header.hide_settings": "Ukryj ustawienia", + "column_header.moveLeft_settings": "Przesuń kolumnę w lewo", + "column_header.moveRight_settings": "Przesuń kolumnę w prawo", "column_header.pin": "Przypnij", + "column_header.show_settings": "Pokaż ustawienia", "column_header.unpin": "Cofnij przypięcie", "column_subheading.navigation": "Nawigacja", "column_subheading.settings": "Ustawienia", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Pokazuj odpowiedzi", "home.settings": "Ustawienia kolumny", "lightbox.close": "Zamknij", + "lightbox.next": "Następne", + "lightbox.previous": "Poprzednie", "loading_indicator.label": "Ładowanie...", "media_gallery.toggle_visible": "Przełącz widoczność", "missing_indicator.label": "Nie znaleziono", @@ -168,6 +175,7 @@ "status.report": "Zgłoś @{name}", "status.sensitive_toggle": "Naciśnij aby wyświetlić", "status.sensitive_warning": "Wrażliwa zawartość", + "status.share": "Udostępnij", "status.show_less": "Pokaż mniej", "status.show_more": "Pokaż więcej", "status.unmute_conversation": "Cofnij wyciezenie konwersacji", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 86da7c4e6..55d2f05de 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -13,6 +13,7 @@ "account.posts": "Posts", "account.report": "Denunciar @{name}", "account.requested": "A aguardar aprovação", + "account.share": "Share @{name}'s profile", "account.unblock": "Não bloquear @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Deixar de seguir", @@ -34,7 +35,11 @@ "column.notifications": "Notificações", "column.public": "Global", "column_back_button.label": "Voltar", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Mostrar as respostas", "home.settings": "Parâmetros da listagem", "lightbox.close": "Fechar", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", @@ -168,6 +175,7 @@ "status.report": "Denúnciar @{name}", "status.sensitive_toggle": "Clique para ver", "status.sensitive_warning": "Conteúdo sensível", + "status.share": "Share", "status.show_less": "Mostrar menos", "status.show_more": "Mostrar mais", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 86da7c4e6..55d2f05de 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -13,6 +13,7 @@ "account.posts": "Posts", "account.report": "Denunciar @{name}", "account.requested": "A aguardar aprovação", + "account.share": "Share @{name}'s profile", "account.unblock": "Não bloquear @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Deixar de seguir", @@ -34,7 +35,11 @@ "column.notifications": "Notificações", "column.public": "Global", "column_back_button.label": "Voltar", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Mostrar as respostas", "home.settings": "Parâmetros da listagem", "lightbox.close": "Fechar", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", @@ -168,6 +175,7 @@ "status.report": "Denúnciar @{name}", "status.sensitive_toggle": "Clique para ver", "status.sensitive_warning": "Conteúdo sensível", + "status.share": "Share", "status.show_less": "Mostrar menos", "status.show_more": "Mostrar mais", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 16af3fe7e..1abfb4370 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -13,6 +13,7 @@ "account.posts": "Посты", "account.report": "Пожаловаться", "account.requested": "Ожидает подтверждения", + "account.share": "Share @{name}'s profile", "account.unblock": "Разблокировать", "account.unblock_domain": "Разблокировать {domain}", "account.unfollow": "Отписаться", @@ -34,7 +35,11 @@ "column.notifications": "Уведомления", "column.public": "Глобальная лента", "column_back_button.label": "Назад", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Закрепить", + "column_header.show_settings": "Show settings", "column_header.unpin": "Открепить", "column_subheading.navigation": "Навигация", "column_subheading.settings": "Настройки", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Показывать ответы", "home.settings": "Настройки колонки", "lightbox.close": "Закрыть", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Загрузка...", "media_gallery.toggle_visible": "Показать/скрыть", "missing_indicator.label": "Не найдено", @@ -168,6 +175,7 @@ "status.report": "Пожаловаться", "status.sensitive_toggle": "Нажмите для просмотра", "status.sensitive_warning": "Чувствительный контент", + "status.share": "Share", "status.show_less": "Свернуть", "status.show_more": "Развернуть", "status.unmute_conversation": "Снять глушение с треда", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index be5c0815d..aa0929f82 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -13,6 +13,7 @@ "account.posts": "Posts", "account.report": "Report @{name}", "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", "account.unblock": "Unblock @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Unfollow", @@ -34,7 +35,11 @@ "column.notifications": "Notifications", "column.public": "Federated timeline", "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Settings", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", "lightbox.close": "Close", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -168,6 +175,7 @@ "status.report": "Report @{name}", "status.sensitive_toggle": "Click to view", "status.sensitive_warning": "Sensitive content", + "status.share": "Share", "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 9d4d5fa17..37ce8597e 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -13,6 +13,7 @@ "account.posts": "Gönderiler", "account.report": "Rapor et @{name}", "account.requested": "Onay bekleniyor", + "account.share": "Share @{name}'s profile", "account.unblock": "Engeli kaldır @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Takipten vazgeç", @@ -34,7 +35,11 @@ "column.notifications": "Bildirimler", "column.public": "Federe zaman tüneli", "column_back_button.label": "Geri", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Navigasyon", "column_subheading.settings": "Ayarlar", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Cevapları göster", "home.settings": "Kolon ayarları", "lightbox.close": "Kapat", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Yükleniyor...", "media_gallery.toggle_visible": "Görünürlüğü değiştir", "missing_indicator.label": "Bulunamadı", @@ -168,6 +175,7 @@ "status.report": "@{name}'i raporla", "status.sensitive_toggle": "Görmek için tıklayınız", "status.sensitive_warning": "Hassas içerik", + "status.share": "Share", "status.show_less": "Daha azı", "status.show_more": "Daha fazlası", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 60a551bb6..fea7bd94e 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -13,6 +13,7 @@ "account.posts": "Пости", "account.report": "Поскаржитися", "account.requested": "Очікує підтвердження", + "account.share": "Share @{name}'s profile", "account.unblock": "Розблокувати", "account.unblock_domain": "Розблокувати {domain}", "account.unfollow": "Відписатися", @@ -34,7 +35,11 @@ "column.notifications": "Сповіщення", "column.public": "Глобальна стрічка", "column_back_button.label": "Назад", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "Навігація", "column_subheading.settings": "Налаштування", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "Показувати відповіді", "home.settings": "Налаштування колонок", "lightbox.close": "Закрити", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "Завантаження...", "media_gallery.toggle_visible": "Показати/приховати", "missing_indicator.label": "Не знайдено", @@ -168,6 +175,7 @@ "status.report": "Поскаржитися", "status.sensitive_toggle": "Натисніть, щоб подивитися", "status.sensitive_warning": "Непристойний зміст", + "status.share": "Share", "status.show_less": "Згорнути", "status.show_more": "Розгорнути", "status.unmute_conversation": "Зняти глушення з діалогу", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 97f1f5e27..d0c4b3d1b 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -13,6 +13,7 @@ "account.posts": "嘟文", "account.report": "举报 @{name}", "account.requested": "等待审批", + "account.share": "Share @{name}'s profile", "account.unblock": "解除对 @{name} 的屏蔽", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "取消关注", @@ -34,7 +35,11 @@ "column.notifications": "通知", "column.public": "跨站公共时间轴", "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "导航", "column_subheading.settings": "设置", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "显示回应嘟文", "home.settings": "字段设置", "lightbox.close": "关闭", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "加载中……", "media_gallery.toggle_visible": "打开或关上", "missing_indicator.label": "找不到内容", @@ -168,6 +175,7 @@ "status.report": "举报 @{name}", "status.sensitive_toggle": "点击显示", "status.sensitive_warning": "敏感内容", + "status.share": "Share", "status.show_less": "减少显示", "status.show_more": "显示更多", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index c65c3d45c..7312aae82 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -13,6 +13,7 @@ "account.posts": "文章", "account.report": "舉報 @{name}", "account.requested": "等候審批", + "account.share": "Share @{name}'s profile", "account.unblock": "解除對 @{name} 的封鎖", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "取消關注", @@ -34,7 +35,11 @@ "column.notifications": "通知", "column.public": "跨站時間軸", "column_back_button.label": "返回", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "瀏覽", "column_subheading.settings": "設定", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "顯示回應文章", "home.settings": "欄位設定", "lightbox.close": "關閉", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "載入中...", "media_gallery.toggle_visible": "打開或關上", "missing_indicator.label": "找不到內容", @@ -168,6 +175,7 @@ "status.report": "舉報 @{name}", "status.sensitive_toggle": "點擊顯示", "status.sensitive_warning": "敏感內容", + "status.share": "Share", "status.show_less": "減少顯示", "status.show_more": "顯示更多", "status.unmute_conversation": "Unmute conversation", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 12e840b16..1c2e35272 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -13,6 +13,7 @@ "account.posts": "貼文", "account.report": "檢舉 @{name}", "account.requested": "正在等待許可", + "account.share": "Share @{name}'s profile", "account.unblock": "取消封鎖 @{name}", "account.unblock_domain": "不再隱藏 {domain}", "account.unfollow": "取消關注", @@ -34,7 +35,11 @@ "column.notifications": "通知", "column.public": "聯盟時間軸", "column_back_button.label": "上一頁", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.navigation": "瀏覽", "column_subheading.settings": "設定", @@ -89,6 +94,8 @@ "home.column_settings.show_replies": "顯示回應", "home.settings": "欄位設定", "lightbox.close": "關閉", + "lightbox.next": "Next", + "lightbox.previous": "Previous", "loading_indicator.label": "讀取中...", "media_gallery.toggle_visible": "切換可見性", "missing_indicator.label": "找不到", @@ -168,6 +175,7 @@ "status.report": "通報 @{name}", "status.sensitive_toggle": "點來看", "status.sensitive_warning": "敏感內容", + "status.share": "Share", "status.show_less": "看少點", "status.show_more": "看更多", "status.unmute_conversation": "不消音對話", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 7c98854a2..07207c93b 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -96,7 +96,7 @@ function appendMedia(state, media) { map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); - if (prevSize === 0 && state.get('default_sensitive')) { + if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { map.set('sensitive', true); } }); @@ -165,14 +165,22 @@ export default function compose(state = initialState, action) { state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option]))) .set('idempotencyKey', uuid()); case COMPOSE_SENSITIVITY_CHANGE: - return state - .set('sensitive', !state.get('sensitive')) - .set('idempotencyKey', uuid()); + return state.withMutations(map => { + if (!state.get('spoiler')) { + map.set('sensitive', !state.get('sensitive')); + } + + map.set('idempotencyKey', uuid()); + }); case COMPOSE_SPOILERNESS_CHANGE: return state.withMutations(map => { map.set('spoiler_text', ''); map.set('spoiler', !state.get('spoiler')); map.set('idempotencyKey', uuid()); + + if (!state.get('sensitive') && state.get('media_attachments').size >= 1) { + map.set('sensitive', true); + } }); case COMPOSE_SPOILER_TEXT_CHANGE: return state diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 4a8a57767..acb85f626 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -1,3 +1,45 @@ +const MAX_NOTIFICATIONS = 5; +const GROUP_TAG = 'tag'; + +// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker +const formatGroupTitle = (message, count) => message.replace('%{count}', count); + +const notify = options => + self.registration.getNotifications().then(notifications => { + if (notifications.length === MAX_NOTIFICATIONS) { + // Reached the maximum number of notifications, proceed with grouping + const group = { + title: formatGroupTitle(options.data.message, notifications.length + 1), + body: notifications + .sort((n1, n2) => n1.timestamp < n2.timestamp) + .map(notification => notification.title).join('\n'), + badge: '/badge.png', + icon: '/android-chrome-192x192.png', + tag: GROUP_TAG, + data: { + url: (new URL('/web/notifications', self.location)).href, + count: notifications.length + 1, + message: options.data.message, + }, + }; + + notifications.forEach(notification => notification.close()); + + return self.registration.showNotification(group.title, group); + } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { + // Already grouped, proceed with appending the notification to the group + const group = cloneNotification(notifications[0]); + + group.title = formatGroupTitle(group.data.message, group.data.count + 1); + group.body = `${options.title}\n${group.body}`; + group.data = { ...group.data, count: group.data.count + 1 }; + + return self.registration.showNotification(group.title, group); + } + + return self.registration.showNotification(options.title, options); + }); + const handlePush = (event) => { const options = event.data.json(); @@ -17,7 +59,7 @@ const handlePush = (event) => { options.actions = options.data.actions; } - event.waitUntil(self.registration.showNotification(options.title, options)); + event.waitUntil(notify(options)); }; const cloneNotification = (notification) => { @@ -50,22 +92,37 @@ const makeRequest = (notification, action) => credentials: 'include', }); +const findBestClient = clients => { + const focusedClient = clients.find(client => client.focused); + const visibleClient = clients.find(client => client.visibilityState === 'visible'); + + return focusedClient || visibleClient || clients[0]; +}; + const openUrl = url => self.clients.matchAll({ type: 'window' }).then(clientList => { - if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate - const webClients = clientList - .filter(client => /\/web\//.test(client.url)) - .sort(client => client !== 'visible'); + if (clientList.length !== 0) { + const webClients = clientList.filter(client => /\/web\//.test(client.url)); - const visibleClient = clientList.find(client => client.visibilityState === 'visible'); - const focusedClient = clientList.find(client => client.focused); + if (webClients.length !== 0) { + const client = findBestClient(webClients); - const client = webClients[0] || visibleClient || focusedClient || clientList[0]; + const { pathname } = new URL(url); - return client.navigate(url).then(client => client.focus()); - } else { - return self.clients.openWindow(url); + if (pathname.startsWith('/web/')) { + return client.focus().then(client => client.postMessage({ + type: 'navigate', + path: pathname.slice('/web/'.length - 1), + })); + } + } else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate + const client = findBestClient(clientList); + + return client.navigate(url).then(client => client.focus()); + } } + + return self.clients.openWindow(url); }); const removeActionFromNotification = (notification, action) => { diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss index deab66ff2..66da44086 100644 --- a/app/javascript/styles/about.scss +++ b/app/javascript/styles/about.scss @@ -121,7 +121,7 @@ .information-board { background: darken($ui-base-color, 4%); - padding: 40px 0; + padding: 20px 0; .panel { position: absolute; @@ -147,10 +147,15 @@ white-space: nowrap; overflow: hidden; + a, span { font-weight: 400; color: lighten($ui-base-color, 34%); } + + a { + text-decoration: none; + } } } @@ -162,13 +167,14 @@ .information-board-sections { display: flex; justify-content: space-between; + flex-wrap: wrap; } .section { flex: 1 0 0; font: 16px/28px 'mastodon-font-sans-serif', sans-serif; text-align: right; - padding: 0 15px; + padding: 10px 15px; span, strong { @@ -190,14 +196,6 @@ color: $primary-text-color; } } - - @media screen and (max-width: 500px) { - flex-direction: column; - - .section { - text-align: left; - } - } } .owner { @@ -317,6 +315,17 @@ } } + p, + li { + font: inherit; + font-weight: inherit; + margin-bottom: 0; + } + + hr { + border-color: rgba($ui-base-lighter-color, .6); + } + .header { line-height: 30px; overflow: hidden; @@ -553,6 +562,7 @@ } #mastodon-timeline { + display: flex; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; font-family: 'mastodon-font-sans-serif', sans-serif; @@ -567,11 +577,20 @@ overflow: hidden; box-shadow: 0 0 6px rgba($black, 0.1); + .column-header { + color: inherit; + font-family: inherit; + font-size: 16px; + line-height: inherit; + font-weight: inherit; + margin: 0; + padding: 15px; + } + .column { padding: 0; border-radius: 4px; overflow: hidden; - height: 100%; } .scrollable { @@ -652,21 +671,17 @@ } } - @media screen and (max-width: 800px) { + @media screen and (max-width: 840px) { .container { padding: 0 20px; } - .information-board { - padding-bottom: 20px; - } - .information-board .container { padding-right: 20px; .panel { position: static; - margin-top: 30px; + margin-top: 20px; width: 100%; border-radius: 4px; @@ -694,6 +709,10 @@ @media screen and (max-width: 675px) { .header-wrapper { padding-top: 0; + + &.compact .hero .heading { + padding-bottom: 20px; + } } .header .container, @@ -707,14 +726,13 @@ } .header { - padding-top: 0; .hero { margin-top: 30px; padding: 0; .heading { - padding-bottom: 20px; + padding: 0 20px 20px; } } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index fe74bae84..fa604df5c 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -149,12 +149,16 @@ color: $ui-base-lighter-color; } + &.disabled { + color: $ui-primary-color; + } + &.active { color: $ui-highlight-color; - } - &.disabled { - color: $ui-primary-color; + &.disabled { + color: lighten($ui-highlight-color, 13%); + } } } @@ -215,16 +219,18 @@ } .dropdown--active::after { - content: ""; - display: block; - position: absolute; - width: 0; - height: 0; - border-style: solid; - border-width: 0 4.5px 7.8px; - border-color: transparent transparent $ui-secondary-color; - bottom: 8px; - right: 104px; + @media screen and (min-width: 1025px) { + content: ""; + display: block; + position: absolute; + width: 0; + height: 0; + border-style: solid; + border-width: 0 4.5px 7.8px; + border-color: transparent transparent $ui-secondary-color; + bottom: 8px; + right: 104px; + } } .invisible { @@ -1837,6 +1843,8 @@ cursor: pointer; flex: 0 0 auto; font-size: 16px; + border: 0; + text-align: start; padding: 15px; z-index: 3; @@ -1999,12 +2007,6 @@ &:hover { background: lighten($ui-base-color, 11%); } - - &.hidden-on-mobile { - @include single-column('screen and (max-width: 1024px)') { - display: none; - } - } } .column-link__icon { @@ -2388,12 +2390,6 @@ button.icon-button.active i.fa-retweet { } } - &.hidden-on-mobile { - @include single-column('screen and (max-width: 1024px)') { - display: none; - } - } - &:focus, &:active { outline: 0; @@ -2672,6 +2668,8 @@ button.icon-button.active i.fa-retweet { cursor: pointer; display: flex; flex-direction: column; + border: 0; + width: 100%; height: 100%; justify-content: center; position: relative; @@ -2754,6 +2752,7 @@ button.icon-button.active i.fa-retweet { align-items: center; background: rgba($base-overlay-background, 0.5); box-sizing: border-box; + border: 0; color: $primary-text-color; cursor: pointer; display: flex; @@ -3848,7 +3847,8 @@ button.icon-button.active i.fa-retweet { .boost-modal, .confirmation-modal, -.report-modal { +.report-modal, +.actions-modal { background: lighten($ui-secondary-color, 8%); color: $ui-base-color; border-radius: 8px; @@ -3873,6 +3873,15 @@ button.icon-button.active i.fa-retweet { } } +.actions-modal { + .status { + background: $white; + border-bottom-color: $ui-secondary-color; + padding-top: 10px; + padding-bottom: 10px; + } +} + .boost-modal__container { overflow-x: scroll; padding: 10px; @@ -3914,7 +3923,7 @@ button.icon-button.active i.fa-retweet { } .confirmation-modal { - max-width: 280px; + max-width: 85vw; @media screen and (min-width: 480px) { max-width: 380px; @@ -3939,6 +3948,47 @@ button.icon-button.active i.fa-retweet { } } +.actions-modal { + .status { + overflow-y: auto; + max-height: 300px; + } + + max-height: 80vh; + max-width: 80vw; + + ul { + overflow-y: auto; + flex-shrink: 0; + + li:empty { + margin: 0; + } + + li:not(:empty) { + a { + color: $ui-base-color; + display: flex; + padding: 10px; + align-items: center; + text-decoration: none; + + &.active { + &, + button { + background: $ui-highlight-color; + color: $primary-text-color; + } + } + + button:first-child { + margin-right: 10px; + } + } + } + } +} + .confirmation-modal__action-bar { .confirmation-modal__cancel-button { background-color: transparent; diff --git a/app/lib/emoji.rb b/app/lib/emoji.rb index e444b6893..45b7f53de 100644 --- a/app/lib/emoji.rb +++ b/app/lib/emoji.rb @@ -6,7 +6,7 @@ class Emoji include Singleton def initialize - data = Oj.load(File.open(File.join(Rails.root, 'lib', 'assets', 'emoji.json'))) + data = Oj.load(File.open(Rails.root.join('lib', 'assets', 'emoji.json'))) @map = {} @@ -32,7 +32,7 @@ class Emoji def codepoint_to_unicode(codepoint) if codepoint.include?('-') - codepoint.split('-').map(&:hex).pack('U') + codepoint.split('-').map(&:hex).pack('U*') else [codepoint.hex].pack('U') end diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb index 34d84a34f..b2489711d 100644 --- a/app/lib/exceptions.rb +++ b/app/lib/exceptions.rb @@ -8,11 +8,11 @@ module Mastodon class UnexpectedResponseError < Error def initialize(response = nil) - @response = response - end - - def to_s - "#{@response.uri} returned code #{@response.code}" + if response.respond_to? :uri + super("#{response.uri} returned code #{response.code}") + else + super + end end end end diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb index 6d6ae2fb3..cc7509fdc 100644 --- a/app/lib/language_detector.rb +++ b/app/lib/language_detector.rb @@ -33,9 +33,7 @@ class LanguageDetector def simplified_text text.dup.tap do |new_text| - URI.extract(new_text).each do |url| - new_text.gsub!(url, '') - end + new_text.gsub!(FetchLinkCardService::URL_PATTERN, '') new_text.gsub!(Account::MENTION_RE, '') new_text.gsub!(Tag::HASHTAG_RE, '') new_text.gsub!(/\s+/, ' ') diff --git a/app/models/account.rb b/app/models/account.rb index 46cc84746..e217733f5 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -44,7 +44,7 @@ # class Account < ApplicationRecord - MENTION_RE = /(?:^|[^\/[:word:]])@([a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i + MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i include AccountAvatar include AccountFinderConcern diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 86df9b591..e76f61278 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -53,6 +53,7 @@ class Web::PushSubscription < ApplicationRecord url: url, actions: actions, access_token: access_token, + message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker } ), endpoint: endpoint, @@ -117,7 +118,7 @@ class Web::PushSubscription < ApplicationRecord when :mention then [ { title: translate('push_notifications.mention.action_favourite'), - icon: full_asset_url('emoji/2764.png', skip_pipeline: true), + icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/favourite", @@ -130,11 +131,11 @@ class Web::PushSubscription < ApplicationRecord can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden? if should_hide - actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('emoji/1f441.png'), todo: 'expand', action: 'expand') + actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), todo: 'expand', action: 'expand') end if can_boost - actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" } + actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" } end actions @@ -160,6 +161,7 @@ class Web::PushSubscription < ApplicationRecord content: translate('push_notifications.subscribed.body'), actions: [], url: web_url('notifications'), + message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker } ), endpoint: endpoint, diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index c266494f0..b0c663d02 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -18,7 +18,7 @@ class AccountSearchService < BaseService return [] if query_blank_or_hashtag? || limit < 1 if resolving_non_matching_remote_account? - [ResolveRemoteAccountService.new.call("#{query_username}@#{query_domain}")] + [ResolveRemoteAccountService.new.call("#{query_username}@#{query_domain}")].compact else search_results_and_exact_match.compact.uniq.slice(0, limit) end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index b462154ae..ab810c628 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -90,7 +90,7 @@ class BatchedRemoveStatusService < BaseService key = FeedManager.instance.key(:home, follower_id) originals = statuses.reject(&:reblog?) - reblogs = statuses.reject { |s| !s.reblog? } + reblogs = statuses.select(&:reblog?) # Quickly remove all originals redis.pipelined do diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb index cd791e2f3..2ce5d1ee9 100644 --- a/app/validators/status_length_validator.rb +++ b/app/validators/status_length_validator.rb @@ -5,6 +5,27 @@ class StatusLengthValidator < ActiveModel::Validator def validate(status) return unless status.local? && !status.reblog? - status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if [status.text, status.spoiler_text].join.mb_chars.grapheme_length > MAX_CHARS + status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if too_long?(status) + end + + private + + def too_long?(status) + countable_length(status) > MAX_CHARS + end + + def countable_length(status) + total_text(status).mb_chars.grapheme_length + end + + def total_text(status) + [status.spoiler_text, countable_text(status)].join + end + + def countable_text(status) + status.text.dup.tap do |new_text| + new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23) + new_text.gsub!(Account::MENTION_RE, '@\2') + end end end diff --git a/app/views/about/_contact.html.haml b/app/views/about/_contact.html.haml index 822639962..cf21ad5a3 100644 --- a/app/views/about/_contact.html.haml +++ b/app/views/about/_contact.html.haml @@ -2,7 +2,10 @@ .panel-header = succeed ':' do = t 'about.contact' - %span{ title: contact.site_contact_email.presence }= contact.site_contact_email.presence + - if contact.site_contact_email.present? + = mail_to contact.site_contact_email, nil, title: contact.site_contact_email + - else + %span= t 'about.contact_unavailable' .panel-body - if contact.contact_account .owner diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml index eeeb0088f..f1c6e6b9d 100644 --- a/app/views/about/_registration.html.haml +++ b/app/views/about/_registration.html.haml @@ -14,15 +14,13 @@ required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :password, - autocomplete: 'off', placeholder: t('simple_form.labels.defaults.password'), required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } + input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } = f.input :password_confirmation, - autocomplete: 'off', placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } .actions = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative' diff --git a/app/views/admin_mailer/new_report.text.erb b/app/views/admin_mailer/new_report.text.erb index 6fa744bc3..671ae5ca7 100644 --- a/app/views/admin_mailer/new_report.text.erb +++ b/app/views/admin_mailer/new_report.text.erb @@ -1,4 +1,4 @@ -<%= display_name(@me) %>, +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> <%= raw t('admin_mailer.new_report.body', target: @report.target_account.acct, reporter: @report.account.acct) %> diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml index 5e2b4fbd6..5ef3de976 100644 --- a/app/views/auth/passwords/edit.html.haml +++ b/app/views/auth/passwords/edit.html.haml @@ -5,8 +5,8 @@ = render 'shared/error_messages', object: resource = f.input :reset_password_token, as: :hidden - = f.input :password, autofocus: true, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password') } - = f.input :password_confirmation, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password') } + = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } .actions = f.button :button, t('auth.set_new_password'), type: :submit diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 84207862a..7ac578bb1 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -19,10 +19,10 @@ %td %samp= session.ip %td - - if request.session['auth_id'] == session.session_id + - if current_session.session_id == session.session_id = t 'sessions.current_session' - else %time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at) %td - - if request.session['auth_id'] != session.session_id + - if current_session.session_id != session.session_id = table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index fbc8d017b..f016a4883 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -5,9 +5,9 @@ = render 'shared/error_messages', object: resource = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password') } - = f.input :password_confirmation, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password') } - = f.input :current_password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password') } + = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' } .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index af7ee2b28..d0529a20c 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -11,8 +11,8 @@ = "@#{site_hostname}" = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } - = f.input :password_confirmation, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } .actions = f.button :button, t('auth.register'), type: :submit diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index f613100c1..e589377bf 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -3,7 +3,7 @@ = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } + = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } .actions = f.button :button, t('auth.login'), type: :submit diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml index 0321e1ec7..cb5e32f3e 100644 --- a/app/views/auth/sessions/two_factor.html.haml +++ b/app/views/auth/sessions/two_factor.html.haml @@ -3,7 +3,7 @@ = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), - input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt') }, required: true, autofocus: true, autocomplete: 'off', + input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true, hint: t('simple_form.hints.sessions.otp') .actions diff --git a/app/views/notification_mailer/digest.text.erb b/app/views/notification_mailer/digest.text.erb index b63352978..e0d1f9b8b 100644 --- a/app/views/notification_mailer/digest.text.erb +++ b/app/views/notification_mailer/digest.text.erb @@ -1,4 +1,4 @@ -<%= display_name(@me) %>, +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> <%= raw t('notification_mailer.digest.body', since: l(@since), instance: root_url) %> <% @notifications.each do |notification| %> diff --git a/app/views/notification_mailer/favourite.text.erb b/app/views/notification_mailer/favourite.text.erb index 795045307..2581b4909 100644 --- a/app/views/notification_mailer/favourite.text.erb +++ b/app/views/notification_mailer/favourite.text.erb @@ -1,4 +1,4 @@ -<%= display_name(@me) %>, +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> <%= raw t('notification_mailer.favourite.body', name: @account.acct) %> diff --git a/app/views/notification_mailer/follow.text.erb b/app/views/notification_mailer/follow.text.erb index af41a3080..cbe46f552 100644 --- a/app/views/notification_mailer/follow.text.erb +++ b/app/views/notification_mailer/follow.text.erb @@ -1,4 +1,4 @@ -<%= display_name(@me) %>, +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> <%= raw t('notification_mailer.follow.body', name: @account.acct) %> diff --git a/app/views/notification_mailer/follow_request.text.erb b/app/views/notification_mailer/follow_request.text.erb index 49087a575..a018394b8 100644 --- a/app/views/notification_mailer/follow_request.text.erb +++ b/app/views/notification_mailer/follow_request.text.erb @@ -1,4 +1,4 @@ -<%= display_name(@me) %>, +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> <%= raw t('notification_mailer.follow_request.body', name: @account.acct) %> diff --git a/app/views/notification_mailer/mention.text.erb b/app/views/notification_mailer/mention.text.erb index b38c5a4d0..03f53813b 100644 --- a/app/views/notification_mailer/mention.text.erb +++ b/app/views/notification_mailer/mention.text.erb @@ -1,4 +1,4 @@ -<%= display_name(@me) %>, +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> <%= raw t('notification_mailer.mention.body', name: @status.account.acct) %> diff --git a/app/views/notification_mailer/reblog.text.erb b/app/views/notification_mailer/reblog.text.erb index fd85437a7..8fc841bf6 100644 --- a/app/views/notification_mailer/reblog.text.erb +++ b/app/views/notification_mailer/reblog.text.erb @@ -1,4 +1,4 @@ -<%= display_name(@me) %>, +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> <%= raw t('notification_mailer.reblog.body', name: @account.acct) %> diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml index d49a7bd0c..b246f83a1 100644 --- a/app/views/settings/deletes/show.html.haml +++ b/app/views/settings/deletes/show.html.haml @@ -10,7 +10,7 @@ %p.hint= t('deletes.description_html') - = f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password') }, hint: t('deletes.confirm_password') + = f.input :password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, hint: t('deletes.confirm_password') .actions = f.button :button, t('deletes.proceed'), type: :submit, class: 'negative' diff --git a/app/views/settings/two_factor_authentication/confirmations/new.html.haml b/app/views/settings/two_factor_authentication/confirmations/new.html.haml index b7eb0c23d..fd4a3e768 100644 --- a/app/views/settings/two_factor_authentication/confirmations/new.html.haml +++ b/app/views/settings/two_factor_authentication/confirmations/new.html.haml @@ -11,7 +11,7 @@ %p.hint= t('two_factor_authentication.manual_instructions') %samp.qr-alternative__code= current_user.otp_secret.scan(/.{4}/).join(' ') - = f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt') + = f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' } .actions = f.button :button, t('two_factor_authentication.enable'), type: :submit diff --git a/app/views/settings/two_factor_authentications/show.html.haml b/app/views/settings/two_factor_authentications/show.html.haml index 8ba42a101..67a64a046 100644 --- a/app/views/settings/two_factor_authentications/show.html.haml +++ b/app/views/settings/two_factor_authentications/show.html.haml @@ -10,7 +10,7 @@ %hr/ = simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f| - = f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt') + = f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' } .actions = f.button :button, t('two_factor_authentication.disable'), type: :submit diff --git a/app/views/user_mailer/confirmation_instructions.fa.html.erb b/app/views/user_mailer/confirmation_instructions.fa.html.erb index cccdaa2c5..3e77e043b 100644 --- a/app/views/user_mailer/confirmation_instructions.fa.html.erb +++ b/app/views/user_mailer/confirmation_instructions.fa.html.erb @@ -9,4 +9,4 @@ <p dir="rtl">با احترام,<p> -<p dir="rtl">گردانندگان سرور <%= @instance %></p> +<p dir="rtl">گردانندگان سرور <%= @instance %></p> \ No newline at end of file diff --git a/app/views/user_mailer/confirmation_instructions.fa.text.erb b/app/views/user_mailer/confirmation_instructions.fa.text.erb index 904bd5bfe..76727b3be 100644 --- a/app/views/user_mailer/confirmation_instructions.fa.text.erb +++ b/app/views/user_mailer/confirmation_instructions.fa.text.erb @@ -9,4 +9,4 @@ با احترام، -گردانندگان سرور <%= @instance %> +گردانندگان سرور <%= @instance %> \ No newline at end of file diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 035a59048..88645cf33 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -16,6 +16,8 @@ class Pubsubhubbub::DeliveryWorker @subscription = Subscription.find(subscription_id) @payload = payload process_delivery unless blocked_domain? + rescue => e + raise e.class, "Delivery failed for #{subscription&.callback_url}: #{e.message}" end private diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb index ce467d18b..ea246128d 100644 --- a/app/workers/pubsubhubbub/distribution_worker.rb +++ b/app/workers/pubsubhubbub/distribution_worker.rb @@ -14,7 +14,7 @@ class Pubsubhubbub::DistributionWorker @subscriptions = active_subscriptions.to_a distribute_public!(stream_entries.reject(&:hidden?)) - distribute_hidden!(stream_entries.reject { |s| !s.hidden? }) + distribute_hidden!(stream_entries.select(&:hidden?)) end private @@ -35,7 +35,7 @@ class Pubsubhubbub::DistributionWorker @payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries)) @domains = @account.followers.domains - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url, s.domain) }) do |subscription| + Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.select { |s| allowed_to_receive?(s.callback_url, s.domain) }) do |subscription| [subscription.id, @payload] end end diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb index 6865e7136..7560c2671 100644 --- a/app/workers/pubsubhubbub/subscribe_worker.rb +++ b/app/workers/pubsubhubbub/subscribe_worker.rb @@ -3,7 +3,7 @@ class Pubsubhubbub::SubscribeWorker include Sidekiq::Worker - sidekiq_options queue: 'push', retry: 10, unique: :until_executed + sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false sidekiq_retry_in do |count| case count @@ -18,9 +18,17 @@ class Pubsubhubbub::SubscribeWorker end end + sidekiq_retries_exhausted do |msg, _e| + account = Account.find(msg['args'].first) + logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing" + ::UnsubscribeService.new.call(account) + end + def perform(account_id) account = Account.find(account_id) logger.debug "PuSH re-subscribing to #{account.acct}" ::SubscribeService.new.call(account) + rescue => e + raise e.class, "Subscribe failed for #{account&.acct}: #{e.message}" end end diff --git a/app/workers/web_push_notification_worker.rb b/app/workers/web_push_notification_worker.rb index da4043ddb..eacea04c3 100644 --- a/app/workers/web_push_notification_worker.rb +++ b/app/workers/web_push_notification_worker.rb @@ -7,16 +7,19 @@ class WebPushNotificationWorker def perform(session_activation_id, notification_id) session_activation = SessionActivation.find(session_activation_id) - notification = Notification.find(notification_id) + notification = Notification.find(notification_id) - begin - session_activation.web_push_subscription.push(notification) - rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription => e - # Subscription expiration is not currently implemented in any browser - session_activation.web_push_subscription.destroy! - session_activation.update!(web_push_subscription: nil) + return if session_activation.web_push_subscription.nil? || notification.activity.nil? - raise e - end + session_activation.web_push_subscription.push(notification) + rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription + # Subscription expiration is not currently implemented in any browser + + session_activation.web_push_subscription.destroy! + session_activation.update!(web_push_subscription: nil) + + true + rescue ActiveRecord::RecordNotFound + true end end |