Merge tag 'v1.5.0rc2' into kosmos

This commit is contained in:
Basti 2017-07-29 10:10:15 +02:00
commit 3704f3ccb3
129 changed files with 1104 additions and 1293 deletions

View File

@ -4,7 +4,6 @@ public/system
public/assets public/assets
public/packs public/packs
node_modules node_modules
storybook
neo4j neo4j
vendor/bundle vendor/bundle
.DS_Store .DS_Store

View File

@ -112,7 +112,7 @@ rules:
jsx-a11y/iframe-has-title: warn jsx-a11y/iframe-has-title: warn
jsx-a11y/img-has-alt: warn jsx-a11y/img-has-alt: warn
jsx-a11y/img-redundant-alt: warn jsx-a11y/img-redundant-alt: warn
jsx-a11y/label-has-for: warn jsx-a11y/label-has-for: off
jsx-a11y/mouse-events-have-key-events: warn jsx-a11y/mouse-events-have-key-events: warn
jsx-a11y/no-access-key: warn jsx-a11y/no-access-key: warn
jsx-a11y/no-distracting-elements: warn jsx-a11y/no-distracting-elements: warn
@ -121,6 +121,6 @@ rules:
jsx-a11y/onclick-has-focus: warn jsx-a11y/onclick-has-focus: warn
jsx-a11y/onclick-has-role: warn jsx-a11y/onclick-has-role: warn
jsx-a11y/role-has-required-aria-props: warn jsx-a11y/role-has-required-aria-props: warn
jsx-a11y/role-supports-aria-props: warn jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn jsx-a11y/tabindex-no-positive: warn

1
.gitignore vendored
View File

@ -21,7 +21,6 @@ public/system
public/assets public/assets
public/packs public/packs
public/packs-test public/packs-test
public/sw.js
.env .env
.env.production .env.production
node_modules/ node_modules/

View File

@ -14,7 +14,6 @@ node_modules/
public/assets/ public/assets/
public/system/ public/system/
spec/ spec/
storybook/
tmp/ tmp/
.vagrant/ .vagrant/
vendor/bundle/ vendor/bundle/

View File

@ -2,4 +2,3 @@ node_modules/
.cache/ .cache/
docs/ docs/
spec/ spec/
storybook/

View File

@ -2,7 +2,7 @@
module InstanceHelper module InstanceHelper
def site_title def site_title
Setting.site_title.to_s Setting.site_title.presence || site_hostname
end end
def site_hostname def site_hostname

View File

@ -162,20 +162,23 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
return ( return (
<div className='autosuggest-textarea'> <div className='autosuggest-textarea'>
<Textarea <label>
inputRef={this.setTextarea} <span style={{ display: 'none' }}>{placeholder}</span>
className='autosuggest-textarea__textarea' <Textarea
disabled={disabled} inputRef={this.setTextarea}
placeholder={placeholder} className='autosuggest-textarea__textarea'
autoFocus={autoFocus} disabled={disabled}
value={value} placeholder={placeholder}
onChange={this.onChange} autoFocus={autoFocus}
onKeyDown={this.onKeyDown} value={value}
onKeyUp={onKeyUp} onChange={this.onChange}
onBlur={this.onBlur} onKeyDown={this.onKeyDown}
onPaste={this.onPaste} onKeyUp={onKeyUp}
style={style} onBlur={this.onBlur}
/> onPaste={this.onPaste}
style={style}
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map((suggestion, i) => ( {suggestions.map((suggestion, i) => (

View File

@ -6,6 +6,8 @@ import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide 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' },
}); });
@injectIntl @injectIntl
@ -101,8 +103,8 @@ export default class ColumnHeader extends React.PureComponent {
moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <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 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 className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></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> </div>
); );
} else if (multiColumn) { } else if (multiColumn) {
@ -133,7 +135,7 @@ export default class ColumnHeader extends React.PureComponent {
return ( return (
<div className={wrapperClassName}> <div className={wrapperClassName}>
<div role='heading' tabIndex={focusable && '0'} className={buttonClassName} aria-label={title} 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`} /> <i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title} {title}
@ -141,7 +143,7 @@ export default class ColumnHeader extends React.PureComponent {
{backButton} {backButton}
{collapseButton} {collapseButton}
</div> </div>
</div> </h1>
<div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}> <div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'> <div className='column-header__collapsible-inner'>

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -9,16 +10,23 @@ export default class DropdownMenu extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
direction: PropTypes.string, direction: PropTypes.string,
status: ImmutablePropTypes.map,
ariaLabel: PropTypes.string, ariaLabel: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
ariaLabel: 'Menu', ariaLabel: 'Menu',
isModalOpen: false,
isUserTouching: () => false,
}; };
state = { state = {
@ -34,6 +42,10 @@ export default class DropdownMenu extends React.PureComponent {
const i = Number(e.currentTarget.getAttribute('data-index')); const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i]; 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. // Don't call e.preventDefault() when the item uses 'href' property.
// ex. "Edit profile" on the account action bar // ex. "Edit profile" on the account action bar
@ -48,10 +60,32 @@ export default class DropdownMenu extends React.PureComponent {
this.dropdown.hide(); 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 }) 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) => { renderItem = (item, i) => {
if (item === null) { if (item === null) {
return <li key={`sep-${i}`} className='dropdown__sep' />; return <li key={`sep-${i}`} className='dropdown__sep' />;
@ -61,7 +95,7 @@ export default class DropdownMenu extends React.PureComponent {
return ( return (
<li className='dropdown__content-list-item' key={`${text}-${i}`}> <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} {text}
</a> </a>
</li> </li>
@ -71,6 +105,7 @@ export default class DropdownMenu extends React.PureComponent {
render () { render () {
const { icon, items, size, direction, ariaLabel, disabled } = this.props; const { icon, items, size, direction, ariaLabel, disabled } = this.props;
const { expanded } = this.state; const { expanded } = this.state;
const isUserTouching = this.props.isUserTouching();
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right'; const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }; const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`; const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
@ -84,20 +119,26 @@ export default class DropdownMenu extends React.PureComponent {
} }
const dropdownItems = expanded && ( const dropdownItems = expanded && (
<ul className='dropdown__content-list'> <ul role='group' className='dropdown__content-list' onClick={this.handleHide}>
{items.map(this.renderItem)} {items.map(this.renderItem)}
</ul> </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 ( return (
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}> <Dropdown ref={this.setRef} active={isUserTouching ? false : expanded} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}> <DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-pressed={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden /> <i className={iconClassname} aria-hidden />
</DropdownTrigger> </DropdownTrigger>
<DropdownContent className={directionClass}> {dropdownContent}
{dropdownItems}
</DropdownContent>
</Dropdown> </Dropdown>
); );
} }

View File

@ -12,12 +12,14 @@ export default class IconButton extends React.PureComponent {
onClick: PropTypes.func, onClick: PropTypes.func,
size: PropTypes.number, size: PropTypes.number,
active: PropTypes.bool, active: PropTypes.bool,
pressed: PropTypes.bool,
style: PropTypes.object, style: PropTypes.object,
activeStyle: PropTypes.object, activeStyle: PropTypes.object,
disabled: PropTypes.bool, disabled: PropTypes.bool,
inverted: PropTypes.bool, inverted: PropTypes.bool,
animate: PropTypes.bool, animate: PropTypes.bool,
overlay: PropTypes.bool, overlay: PropTypes.bool,
tabIndex: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@ -26,6 +28,7 @@ export default class IconButton extends React.PureComponent {
disabled: false, disabled: false,
animate: false, animate: false,
overlay: false, overlay: false,
tabIndex: '0',
}; };
handleClick = (e) => { handleClick = (e) => {
@ -73,10 +76,12 @@ export default class IconButton extends React.PureComponent {
{({ rotate }) => {({ rotate }) =>
<button <button
aria-label={this.props.title} aria-label={this.props.title}
aria-pressed={this.props.pressed}
title={this.props.title} title={this.props.title}
className={classes.join(' ')} className={classes.join(' ')}
onClick={this.handleClick} onClick={this.handleClick}
style={style} style={style}
tabIndex={this.props.tabIndex}
> >
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
</button> </button>

View File

@ -19,12 +19,15 @@ export default class SettingText extends React.PureComponent {
const { settings, settingKey, label } = this.props; const { settings, settingKey, label } = this.props;
return ( return (
<input <label>
className='setting-text' <span style={{ display: 'none' }}>{label}</span>
value={settings.getIn(settingKey)} <input
onChange={this.handleChange} className='setting-text'
placeholder={label} value={settings.getIn(settingKey)}
/> onChange={this.handleChange}
placeholder={label}
/>
</label>
); );
} }

View File

@ -41,6 +41,8 @@ export default class Status extends ImmutablePureComponent {
autoPlayGif: PropTypes.bool, autoPlayGif: PropTypes.bool,
muted: PropTypes.bool, muted: PropTypes.bool,
intersectionObserverWrapper: PropTypes.object, intersectionObserverWrapper: PropTypes.object,
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}; };
state = { state = {
@ -59,6 +61,7 @@ export default class Status extends ImmutablePureComponent {
'boostModal', 'boostModal',
'autoPlayGif', 'autoPlayGif',
'muted', 'muted',
'listLength',
] ]
updateOnStates = ['isExpanded'] updateOnStates = ['isExpanded']
@ -67,8 +70,8 @@ export default class Status extends ImmutablePureComponent {
if (!nextState.isIntersecting && nextState.isHidden) { if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true // 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 // that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter. // the only things that matter (and updated ARIA attributes).
return this.state.isIntersecting || !this.state.isHidden; return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) { } else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state, // 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 // (i.e. offscreen to onscreen), then we definitely need to re-render
@ -107,17 +110,12 @@ export default class Status extends ImmutablePureComponent {
this.height = getRectFromEntry(entry).height; 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) => { this.setState((prevState) => {
if (prevState.isIntersecting && !isIntersecting) { if (prevState.isIntersecting && !entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting); scheduleIdleTask(this.hideIfNotIntersecting);
} }
return { return {
isIntersecting: isIntersecting, isIntersecting: entry.isIntersecting,
isHidden: false, isHidden: false,
}; };
}); });
@ -174,7 +172,7 @@ export default class Status extends ImmutablePureComponent {
// Exclude intersectionObserverWrapper from `other` variable // Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here. // because intersection is managed in here.
const { status, account, intersectionObserverWrapper, ...other } = this.props; const { status, account, intersectionObserverWrapper, index, listLength, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state; const { isExpanded, isIntersecting, isHidden } = this.state;
if (status === null) { if (status === null) {
@ -183,10 +181,10 @@ export default class Status extends ImmutablePureComponent {
if (!isIntersecting && isHidden) { if (!isIntersecting && isHidden) {
return ( 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.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')} {status.get('content')}
</div> </article>
); );
} }
@ -200,14 +198,14 @@ export default class Status extends ImmutablePureComponent {
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
return ( 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'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> <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> }} /> <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> </div>
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} /> <Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
</div> </article>
); );
} }
@ -236,7 +234,7 @@ export default class Status extends ImmutablePureComponent {
} }
return ( 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='0' ref={this.handleRef}>
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@ -254,7 +252,7 @@ export default class Status extends ImmutablePureComponent {
{media} {media}
<StatusActionBar {...this.props} /> <StatusActionBar {...this.props} />
</div> </article>
); );
} }

View File

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IconButton from './icon_button'; import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -156,7 +156,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
{shareButton} {shareButton}
<div className='status__action-bar-dropdown'> <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>
</div> </div>
); );

View File

@ -104,6 +104,32 @@ export default class StatusList extends ImmutablePureComponent {
this.props.onScrollToBottom(); this.props.onScrollToBottom();
} }
handleKeyDown = (e) => {
if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].nextElementSibling;
case 'PageUp':
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].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 () { render () {
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; 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) { if (isLoading || statusIds.size > 0 || !emptyMessage) {
scrollableArea = ( scrollableArea = (
<div className='scrollable' ref={this.setRef}> <div className='scrollable' ref={this.setRef}>
<div className='status-list'> <div role='feed' className='status-list' onKeyDown={this.handleKeyDown}>
{prepend} {prepend}
{statusIds.map((statusId) => { {statusIds.map((statusId, index) => {
return <StatusContainer key={statusId} id={statusId} intersectionObserverWrapper={this.intersectionObserverWrapper} />; return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
})} })}
{loadMore} {loadMore}

View File

@ -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);

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; 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 Link from 'react-router-dom/Link';
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
@ -15,6 +15,7 @@ const messages = defineMessages({
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
report: { id: 'account.report', defaultMessage: 'Report @{name}' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
media: { id: 'account.media', defaultMessage: 'Media' }, media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
@ -36,6 +37,12 @@ export default class ActionBar extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
handleShare = () => {
navigator.share({
url: this.props.account.get('url'),
});
}
render () { render () {
const { account, me, intl } = this.props; const { account, me, intl } = this.props;
@ -43,6 +50,9 @@ export default class ActionBar extends React.PureComponent {
let extraInfo = ''; let extraInfo = '';
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); 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(null);
menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` }); menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` });
menu.push(null); menu.push(null);
@ -96,7 +106,7 @@ export default class ActionBar extends React.PureComponent {
<div className='account__action-bar'> <div className='account__action-bar'>
<div className='account__action-bar-dropdown'> <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>
<div className='account__action-bar-links'> <div className='account__action-bar-links'>

View File

@ -52,9 +52,10 @@ class Avatar extends ImmutablePureComponent {
return ( return (
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
{({ radius }) => {({ radius }) =>
<a // eslint-disable-line jsx-a11y/anchor-has-content <a
href={account.get('url')} href={account.get('url')}
className='account__header__avatar' className='account__header__avatar'
role='presentation'
target='_blank' target='_blank'
rel='noopener' rel='noopener'
style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }} style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
@ -62,7 +63,9 @@ class Avatar extends ImmutablePureComponent {
onMouseOut={this.handleMouseOut} onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver} onFocus={this.handleMouseOver}
onBlur={this.handleMouseOut} onBlur={this.handleMouseOut}
/> >
<span style={{ display: 'none' }}>{account.get('acct')}</span>
</a>
} }
</Motion> </Motion>
); );

View File

@ -159,7 +159,10 @@ export default class ComposeForm extends ImmutablePureComponent {
<div className='compose-form'> <div className='compose-form'>
<Collapsable isVisible={this.props.spoiler} fullHeight={50}> <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
<div className='spoiler-input'> <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> </div>
</Collapsable> </Collapsable>

View File

@ -65,6 +65,22 @@ export default class EmojiPickerDropdown extends React.PureComponent {
this.setState({ active: false }); 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 () { render () {
const { intl } = this.props; const { intl } = this.props;
@ -104,10 +120,11 @@ export default class EmojiPickerDropdown extends React.PureComponent {
}; };
const { active, loading } = this.state; const { active, loading } = this.state;
const title = intl.formatMessage(messages.emoji);
return ( return (
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' onShow={this.onShowDropdown} onHide={this.onHideDropdown}> <Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)}> <DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-pressed={active} role='button' onKeyDown={this.onToggle} tabIndex={0} >
<img <img
className={`emojione ${active && loading ? 'pulse-loading' : ''}`} className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
alt='🙂' alt='🙂'
@ -118,7 +135,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
<DropdownContent className='dropdown__left'> <DropdownContent className='dropdown__left'>
{ {
this.state.active && !this.state.loading && 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> </DropdownContent>
</Dropdown> </Dropdown>

View File

@ -18,6 +18,7 @@ export default class NavigationBar extends ImmutablePureComponent {
return ( return (
<div className='navigation-bar'> <div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> <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')} animate size={40} /> <Avatar src={this.props.account.get('avatar')} animate size={40} />
</Permalink> </Permalink>

View File

@ -24,6 +24,10 @@ const iconStyle = {
export default class PrivacyDropdown extends React.PureComponent { export default class PrivacyDropdown extends React.PureComponent {
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -34,14 +38,36 @@ export default class PrivacyDropdown extends React.PureComponent {
}; };
handleToggle = () => { 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 });
}
}
handleModalActionClick = (e) => {
e.preventDefault();
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
this.props.onModalClose();
this.props.onChange(value);
} }
handleClick = (e) => { handleClick = (e) => {
const value = e.currentTarget.getAttribute('data-index'); if (e.key === 'Escape') {
e.preventDefault(); this.setState({ open: false });
this.setState({ open: false }); } else if (!e.key || e.key === 'Enter') {
this.props.onChange(value); const value = e.currentTarget.getAttribute('data-index');
e.preventDefault();
this.setState({ open: false });
this.props.onChange(value);
}
} }
onGlobalClick = (e) => { onGlobalClick = (e) => {
@ -50,6 +76,17 @@ export default class PrivacyDropdown extends React.PureComponent {
} }
} }
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 () { componentDidMount () {
window.addEventListener('click', this.onGlobalClick); window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick); window.addEventListener('touchstart', this.onGlobalClick);
@ -68,25 +105,18 @@ export default class PrivacyDropdown extends React.PureComponent {
const { value, intl } = this.props; const { value, intl } = this.props;
const { open } = this.state; const { open } = this.state;
const options = [ const valueOption = this.options.find(item => item.value === value);
{ 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);
return ( return (
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}> <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} pressed={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__dropdown'> <div className='privacy-dropdown__dropdown'>
{open && options.map(item => {open && this.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' : ''}`}> <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__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
<div className='privacy-dropdown__option__content'> <div className='privacy-dropdown__option__content'>
<strong>{item.shortText}</strong> <strong>{item.text}</strong>
{item.longText} {item.meta}
</div> </div>
</div> </div>
)} )}

View File

@ -52,15 +52,18 @@ export default class Search extends React.PureComponent {
return ( return (
<div className='search'> <div className='search'>
<input <label>
className='search__input' <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
type='text' <input
placeholder={intl.formatMessage(messages.placeholder)} className='search__input'
value={value} type='text'
onChange={this.handleChange} placeholder={intl.formatMessage(messages.placeholder)}
onKeyUp={this.handleKeyDown} value={value}
onFocus={this.handleFocus} onChange={this.handleChange}
/> onKeyUp={this.handleKeyDown}
onFocus={this.handleFocus}
/>
</label>
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> <i className={`fa fa-search ${hasValue ? '' : 'active'}`} />

View File

@ -57,16 +57,19 @@ export default class UploadButton extends ImmutablePureComponent {
return ( return (
<div className='compose-form__upload-button'> <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} /> <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 <label>
key={resetFileKey} <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
ref={this.setRef} <input
type='file' key={resetFileKey}
multiple={false} ref={this.setRef}
accept={acceptContentTypes.toArray().join(',')} type='file'
onChange={this.handleChange} multiple={false}
disabled={disabled} accept={acceptContentTypes.toArray().join(',')}
style={{ display: 'none' }} onChange={this.handleChange}
/> disabled={disabled}
style={{ display: 'none' }}
/>
</label>
</div> </div>
); );
} }

View File

@ -1,8 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PrivacyDropdown from '../components/privacy_dropdown'; import PrivacyDropdown from '../components/privacy_dropdown';
import { changeComposeVisibility } from '../../../actions/compose'; import { changeComposeVisibility } from '../../../actions/compose';
import { openModal, closeModal } from '../../../actions/modal';
import { isUserTouching } from '../../../is_mobile';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
value: state.getIn(['compose', 'privacy']), value: state.getIn(['compose', 'privacy']),
}); });
@ -12,6 +15,10 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeVisibility(value)); dispatch(changeComposeVisibility(value));
}, },
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown); export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);

View File

@ -65,21 +65,21 @@ export default class Compose extends React.PureComponent {
const { columns } = this.props; const { columns } = this.props;
header = ( header = (
<nav className='drawer__header'> <nav className='drawer__header'>
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link> <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') && ( {!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' /></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') && ( {!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' /></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') && ( {!columns.some(column => column.get('id') === 'COMMUNITY') && (
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' 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') && ( {!columns.some(column => column.get('id') === 'PUBLIC') && (
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' 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 href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a> <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></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> </nav>
); );
} }

View File

@ -18,13 +18,19 @@ export default class SettingToggle extends React.PureComponent {
this.props.onChange(this.props.settingKey, target.checked); this.props.onChange(this.props.settingKey, target.checked);
} }
onKeyDown = e => {
if (e.key === ' ') {
this.props.onChange(this.props.settingKey, !e.target.checked);
}
}
render () { render () {
const { prefix, settings, settingKey, label, meta } = this.props; const { prefix, settings, settingKey, label, meta } = this.props;
const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-'); const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-');
return ( return (
<div className='setting-toggle'> <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> <label htmlFor={id} className='setting-toggle__label'>{label}</label>
{meta && <span className='setting-meta__label'>{meta}</span>} {meta && <span className='setting-meta__label'>{meta}</span>}
</div> </div>

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IconButton from '../../../components/icon_button'; import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes'; 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'; import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
@ -13,6 +13,7 @@ const messages = defineMessages({
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' },
}); });
@injectIntl @injectIntl
@ -58,6 +59,13 @@ export default class ActionBar extends React.PureComponent {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
} }
handleShare = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
});
}
render () { render () {
const { status, me, intl } = this.props; 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 }); 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'; let reblogIcon = 'retweet';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock'; 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 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 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> <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'> <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>
</div> </div>
); );

View File

@ -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>
);
}
}

View File

@ -5,6 +5,7 @@ import spring from 'react-motion/lib/spring';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
import BundleModalError from './bundle_modal_error'; import BundleModalError from './bundle_modal_error';
import ModalLoading from './modal_loading'; import ModalLoading from './modal_loading';
import ActionsModal from '../components/actions_modal';
import { import {
MediaModal, MediaModal,
OnboardingModal, OnboardingModal,
@ -21,6 +22,7 @@ const MODAL_COMPONENTS = {
'BOOST': BoostModal, 'BOOST': BoostModal,
'CONFIRM': ConfirmationModal, 'CONFIRM': ConfirmationModal,
'REPORT': ReportModal, 'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
}; };
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {
@ -42,10 +44,34 @@ export default class ModalRoot extends React.PureComponent {
window.addEventListener('keyup', this.handleKeyUp, false); 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.type && !!prevProps.type) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
this.activeElement.focus();
this.activeElement = null;
}
}
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('keyup', this.handleKeyUp); window.removeEventListener('keyup', this.handleKeyUp);
} }
getSiblings = () => {
return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
}
setRef = ref => {
this.node = ref;
}
willEnter () { willEnter () {
return { opacity: 0, scale: 0.98 }; return { opacity: 0, scale: 0.98 };
} }
@ -84,11 +110,11 @@ export default class ModalRoot extends React.PureComponent {
willLeave={this.willLeave} willLeave={this.willLeave}
> >
{interpolatedStyles => {interpolatedStyles =>
<div className='modal-root'> <div className='modal-root' ref={this.setRef}>
{interpolatedStyles.map(({ key, data: { type, props }, style }) => ( {interpolatedStyles.map(({ key, data: { type, props }, style }) => (
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} /> <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}> <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
</BundleContainer> </BundleContainer>

View File

@ -49,6 +49,10 @@ const mapStateToProps = state => ({
@connect(mapStateToProps) @connect(mapStateToProps)
export default class UI extends React.PureComponent { export default class UI extends React.PureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
}
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
children: PropTypes.node, children: PropTypes.node,
@ -123,6 +127,14 @@ export default class UI extends React.PureComponent {
this.setState({ draggingOver: false }); 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 () { componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true }); window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragenter', this.handleDragEnter, false);
@ -131,6 +143,10 @@ export default class UI extends React.PureComponent {
document.addEventListener('dragleave', this.handleDragLeave, false); document.addEventListener('dragleave', this.handleDragLeave, false);
document.addEventListener('dragend', this.handleDragEnd, false); document.addEventListener('dragend', this.handleDragEnd, false);
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
}
this.props.dispatch(refreshHomeTimeline()); this.props.dispatch(refreshHomeTimeline());
this.props.dispatch(refreshNotifications()); this.props.dispatch(refreshNotifications());
} }

View File

@ -103,9 +103,9 @@ export function ReportModal () {
} }
export function MediaGallery () { export function MediaGallery () {
return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery'); return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
} }
export function VideoPlayer () { export function VideoPlayer () {
return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player'); return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
} }

View File

@ -5,6 +5,15 @@ export function isMobile(width) {
}; };
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 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() { export function isIOS() {
return iOS; return iOS;

View File

@ -13,6 +13,7 @@
"account.posts": "المشاركات", "account.posts": "المشاركات",
"account.report": "أبلغ عن @{name}", "account.report": "أبلغ عن @{name}",
"account.requested": "في انتظار الموافقة", "account.requested": "في انتظار الموافقة",
"account.share": "Share @{name}'s profile",
"account.unblock": "إلغاء الحظر عن @{name}", "account.unblock": "إلغاء الحظر عن @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "إلغاء المتابعة", "account.unfollow": "إلغاء المتابعة",
@ -35,6 +36,8 @@
"column.public": "الخيط العام الموحد", "column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة", "column_back_button.label": "العودة",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Публикации", "account.posts": "Публикации",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "В очакване на одобрение", "account.requested": "В очакване на одобрение",
"account.share": "Share @{name}'s profile",
"account.unblock": "Не блокирай", "account.unblock": "Не блокирай",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Не следвай", "account.unfollow": "Не следвай",
@ -35,6 +36,8 @@
"column.public": "Публичен канал", "column.public": "Публичен канал",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Publicacions", "account.posts": "Publicacions",
"account.report": "Informe @{name}", "account.report": "Informe @{name}",
"account.requested": "Esperant aprovació", "account.requested": "Esperant aprovació",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desbloquejar @{name}", "account.unblock": "Desbloquejar @{name}",
"account.unblock_domain": "Mostra {domain}", "account.unblock_domain": "Mostra {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
@ -35,6 +36,8 @@
"column.public": "Línia de temps federada", "column.public": "Línia de temps federada",
"column_back_button.label": "Enrere", "column_back_button.label": "Enrere",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Beiträge", "account.posts": "Beiträge",
"account.report": "@{name} melden", "account.report": "@{name} melden",
"account.requested": "Warte auf Erlaubnis", "account.requested": "Warte auf Erlaubnis",
"account.share": "Share @{name}'s profile",
"account.unblock": "@{name} entblocken", "account.unblock": "@{name} entblocken",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Entfolgen", "account.unfollow": "Entfolgen",
@ -35,6 +36,8 @@
"column.public": "Gesamtes bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -61,6 +61,14 @@
"defaultMessage": "Hide settings", "defaultMessage": "Hide settings",
"id": "column_header.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", "defaultMessage": "Unpin",
"id": "column_header.unpin" "id": "column_header.unpin"
@ -366,6 +374,10 @@
"defaultMessage": "Report @{name}", "defaultMessage": "Report @{name}",
"id": "account.report" "id": "account.report"
}, },
{
"defaultMessage": "Share @{name}'s profile",
"id": "account.share"
},
{ {
"defaultMessage": "Media", "defaultMessage": "Media",
"id": "account.media" "id": "account.media"
@ -1019,6 +1031,10 @@
{ {
"defaultMessage": "Report @{name}", "defaultMessage": "Report @{name}",
"id": "status.report" "id": "status.report"
},
{
"defaultMessage": "Share",
"id": "status.share"
} }
], ],
"path": "app/javascript/mastodon/features/status/components/action_bar.json" "path": "app/javascript/mastodon/features/status/components/action_bar.json"

View File

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Awaiting approval", "account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Unblock @{name}", "account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow", "account.unfollow": "Unfollow",
@ -35,6 +36,8 @@
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Mesaĝoj", "account.posts": "Mesaĝoj",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Atendas aprobon", "account.requested": "Atendas aprobon",
"account.share": "Share @{name}'s profile",
"account.unblock": "Malbloki @{name}", "account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Malsekvi", "account.unfollow": "Malsekvi",
@ -35,6 +36,8 @@
"column.public": "Fratara tempolinio", "column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni", "column_back_button.label": "Reveni",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Publicaciones", "account.posts": "Publicaciones",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Esperando aprobación", "account.requested": "Esperando aprobación",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desbloquear", "account.unblock": "Desbloquear",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Dejar de seguir", "account.unfollow": "Dejar de seguir",
@ -35,6 +36,8 @@
"column.public": "Historia federada", "column.public": "Historia federada",
"column_back_button.label": "Atrás", "column_back_button.label": "Atrás",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "نوشته‌ها", "account.posts": "نوشته‌ها",
"account.report": "گزارش @{name}", "account.report": "گزارش @{name}",
"account.requested": "در انتظار پذیرش", "account.requested": "در انتظار پذیرش",
"account.share": "Share @{name}'s profile",
"account.unblock": "رفع انسداد @{name}", "account.unblock": "رفع انسداد @{name}",
"account.unblock_domain": "رفع پنهان‌سازی از {domain}", "account.unblock_domain": "رفع پنهان‌سازی از {domain}",
"account.unfollow": "پایان پیگیری", "account.unfollow": "پایان پیگیری",
@ -35,6 +36,8 @@
"column.public": "نوشته‌های همه‌جا", "column.public": "نوشته‌های همه‌جا",
"column_back_button.label": "بازگشت", "column_back_button.label": "بازگشت",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Postit", "account.posts": "Postit",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Odottaa hyväksyntää", "account.requested": "Odottaa hyväksyntää",
"account.share": "Share @{name}'s profile",
"account.unblock": "Salli @{name}", "account.unblock": "Salli @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Lopeta seuraaminen", "account.unfollow": "Lopeta seuraaminen",
@ -35,6 +36,8 @@
"column.public": "Yleinen aikajana", "column.public": "Yleinen aikajana",
"column_back_button.label": "Takaisin", "column_back_button.label": "Takaisin",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Statuts", "account.posts": "Statuts",
"account.report": "Signaler", "account.report": "Signaler",
"account.requested": "Invitation envoyée", "account.requested": "Invitation envoyée",
"account.share": "Share @{name}'s profile",
"account.unblock": "Débloquer", "account.unblock": "Débloquer",
"account.unblock_domain": "Ne plus masquer {domain}", "account.unblock_domain": "Ne plus masquer {domain}",
"account.unfollow": "Ne plus suivre", "account.unfollow": "Ne plus suivre",
@ -35,6 +36,8 @@
"column.public": "Fil public global", "column.public": "Fil public global",
"column_back_button.label": "Retour", "column_back_button.label": "Retour",
"column_header.hide_settings": "Hide settings", "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.pin": "Épingler",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Retirer", "column_header.unpin": "Retirer",

View File

@ -13,6 +13,7 @@
"account.posts": "הודעות", "account.posts": "הודעות",
"account.report": "לדווח על @{name}", "account.report": "לדווח על @{name}",
"account.requested": "בהמתנה לאישור", "account.requested": "בהמתנה לאישור",
"account.share": "Share @{name}'s profile",
"account.unblock": "הסרת חסימה מעל @{name}", "account.unblock": "הסרת חסימה מעל @{name}",
"account.unblock_domain": "הסר חסימה מקהילת {domain}", "account.unblock_domain": "הסר חסימה מקהילת {domain}",
"account.unfollow": "הפסקת מעקב", "account.unfollow": "הפסקת מעקב",
@ -35,6 +36,8 @@
"column.public": "בפרהסיה", "column.public": "בפרהסיה",
"column_back_button.label": "חזרה", "column_back_button.label": "חזרה",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Postovi", "account.posts": "Postovi",
"account.report": "Prijavi @{name}", "account.report": "Prijavi @{name}",
"account.requested": "Čeka pristanak", "account.requested": "Čeka pristanak",
"account.share": "Share @{name}'s profile",
"account.unblock": "Deblokiraj @{name}", "account.unblock": "Deblokiraj @{name}",
"account.unblock_domain": "Otkrij {domain}", "account.unblock_domain": "Otkrij {domain}",
"account.unfollow": "Prestani slijediti", "account.unfollow": "Prestani slijediti",
@ -35,6 +36,8 @@
"column.public": "Federalni timeline", "column.public": "Federalni timeline",
"column_back_button.label": "Natrag", "column_back_button.label": "Natrag",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Awaiting approval", "account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Blokkolás levétele", "account.unblock": "Blokkolás levétele",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Követés abbahagyása", "account.unfollow": "Követés abbahagyása",
@ -35,6 +36,8 @@
"column.public": "Nyilvános", "column.public": "Nyilvános",
"column_back_button.label": "Vissza", "column_back_button.label": "Vissza",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Postingan", "account.posts": "Postingan",
"account.report": "Laporkan @{name}", "account.report": "Laporkan @{name}",
"account.requested": "Menunggu persetujuan", "account.requested": "Menunggu persetujuan",
"account.share": "Share @{name}'s profile",
"account.unblock": "Hapus blokir @{name}", "account.unblock": "Hapus blokir @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Berhenti mengikuti", "account.unfollow": "Berhenti mengikuti",
@ -35,6 +36,8 @@
"column.public": "Linimasa gabunggan", "column.public": "Linimasa gabunggan",
"column_back_button.label": "Kembali", "column_back_button.label": "Kembali",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Mesaji", "account.posts": "Mesaji",
"account.report": "Denuncar @{name}", "account.report": "Denuncar @{name}",
"account.requested": "Vartante aprobo", "account.requested": "Vartante aprobo",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desblokusar @{name}", "account.unblock": "Desblokusar @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Ne plus sequar", "account.unfollow": "Ne plus sequar",
@ -35,6 +36,8 @@
"column.public": "Federata tempolineo", "column.public": "Federata tempolineo",
"column_back_button.label": "Retro", "column_back_button.label": "Retro",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Segnala @{name}", "account.report": "Segnala @{name}",
"account.requested": "In attesa di approvazione", "account.requested": "In attesa di approvazione",
"account.share": "Share @{name}'s profile",
"account.unblock": "Sblocca @{name}", "account.unblock": "Sblocca @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Non seguire", "account.unfollow": "Non seguire",
@ -35,6 +36,8 @@
"column.public": "Timeline federata", "column.public": "Timeline federata",
"column_back_button.label": "Indietro", "column_back_button.label": "Indietro",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "投稿", "account.posts": "投稿",
"account.report": "通報", "account.report": "通報",
"account.requested": "承認待ち", "account.requested": "承認待ち",
"account.share": "Share @{name}'s profile",
"account.unblock": "ブロック解除", "account.unblock": "ブロック解除",
"account.unblock_domain": "{domain}を表示", "account.unblock_domain": "{domain}を表示",
"account.unfollow": "フォロー解除", "account.unfollow": "フォロー解除",
@ -35,6 +36,8 @@
"column.public": "連合タイムライン", "column.public": "連合タイムライン",
"column_back_button.label": "戻る", "column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す", "column_header.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.pin": "ピン留めする",
"column_header.show_settings": "設定を表示", "column_header.show_settings": "設定を表示",
"column_header.unpin": "ピン留めを外す", "column_header.unpin": "ピン留めを外す",

View File

@ -13,6 +13,7 @@
"account.posts": "포스트", "account.posts": "포스트",
"account.report": "신고", "account.report": "신고",
"account.requested": "승인 대기 중", "account.requested": "승인 대기 중",
"account.share": "Share @{name}'s profile",
"account.unblock": "차단 해제", "account.unblock": "차단 해제",
"account.unblock_domain": "{domain} 숨김 해제", "account.unblock_domain": "{domain} 숨김 해제",
"account.unfollow": "팔로우 해제", "account.unfollow": "팔로우 해제",
@ -35,6 +36,8 @@
"column.public": "연합 타임라인", "column.public": "연합 타임라인",
"column_back_button.label": "돌아가기", "column_back_button.label": "돌아가기",
"column_header.hide_settings": "Hide settings", "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.pin": "고정하기",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "고정 해제", "column_header.unpin": "고정 해제",

View File

@ -13,6 +13,7 @@
"account.posts": "Toots", "account.posts": "Toots",
"account.report": "Rapporteer @{name}", "account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring", "account.requested": "Wacht op goedkeuring",
"account.share": "Share @{name}'s profile",
"account.unblock": "Deblokkeer @{name}", "account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "{domain} niet meer negeren", "account.unblock_domain": "{domain} niet meer negeren",
"account.unfollow": "Ontvolgen", "account.unfollow": "Ontvolgen",
@ -35,6 +36,8 @@
"column.public": "Globale tijdlijn", "column.public": "Globale tijdlijn",
"column_back_button.label": "terug", "column_back_button.label": "terug",
"column_header.hide_settings": "Instellingen verbergen", "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.pin": "Vastmaken",
"column_header.show_settings": "Instellingen tonen", "column_header.show_settings": "Instellingen tonen",
"column_header.unpin": "Losmaken", "column_header.unpin": "Losmaken",

View File

@ -13,6 +13,7 @@
"account.posts": "Innlegg", "account.posts": "Innlegg",
"account.report": "Rapportér @{name}", "account.report": "Rapportér @{name}",
"account.requested": "Venter på godkjennelse", "account.requested": "Venter på godkjennelse",
"account.share": "Share @{name}'s profile",
"account.unblock": "Avblokker @{name}", "account.unblock": "Avblokker @{name}",
"account.unblock_domain": "Vis {domain}", "account.unblock_domain": "Vis {domain}",
"account.unfollow": "Avfølg", "account.unfollow": "Avfølg",
@ -35,6 +36,8 @@
"column.public": "Felles tidslinje", "column.public": "Felles tidslinje",
"column_back_button.label": "Tilbake", "column_back_button.label": "Tilbake",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -1,7 +1,7 @@
{ {
"account.block": "Blocar @{name}", "account.block": "Blocar @{name}",
"account.block_domain": "Tot amagar del domeni {domain}", "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.edit_profile": "Modificar lo perfil",
"account.follow": "Sègre", "account.follow": "Sègre",
"account.followers": "Seguidors", "account.followers": "Seguidors",
@ -13,18 +13,19 @@
"account.posts": "Estatuts", "account.posts": "Estatuts",
"account.report": "Senhalar @{name}", "account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada", "account.requested": "Invitacion mandada",
"account.share": "Partejar lo perfil a @{name}",
"account.unblock": "Desblocar @{name}", "account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}", "account.unblock_domain": "Desblocar {domain}",
"account.unfollow": "Quitar de sègre", "account.unfollow": "Quitar de sègre",
"account.unmute": "Quitar de rescondre @{name}", "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", "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 daqueste compausant.", "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament daqueste compausant.",
"bundle_column_error.retry": "Tornar ensejar", "bundle_column_error.retry": "Tornar ensajar",
"bundle_column_error.title": "Error de ret", "bundle_column_error.title": "Error de ret",
"bundle_modal_error.close": "Tampar", "bundle_modal_error.close": "Tampar",
"bundle_modal_error.message": "Quicòm a fach meuca pendent lo cargament daqueste compausant.", "bundle_modal_error.message": "Quicòm a fach mèuca pendent lo cargament daqueste compausant.",
"bundle_modal_error.retry": "Tornar ensejar", "bundle_modal_error.retry": "Tornar ensajar",
"column.blocks": "Personas blocadas", "column.blocks": "Personas blocadas",
"column.community": "Flux public local", "column.community": "Flux public local",
"column.favourites": "Favorits", "column.favourites": "Favorits",
@ -34,9 +35,11 @@
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.public": "Flux public global", "column.public": "Flux public global",
"column_back_button.label": "Tornar", "column_back_button.label": "Tornar",
"column_header.hide_settings": "Hide settings", "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.pin": "Penjar",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Mostrar los paramètres",
"column_header.unpin": "Despenjar", "column_header.unpin": "Despenjar",
"column_subheading.navigation": "Navigacion", "column_subheading.navigation": "Navigacion",
"column_subheading.settings": "Paramètres", "column_subheading.settings": "Paramètres",
@ -48,35 +51,35 @@
"compose_form.publish_loud": "{publish} !", "compose_form.publish_loud": "{publish} !",
"compose_form.sensitive": "Marcar lo mèdia coma sensible", "compose_form.sensitive": "Marcar lo mèdia coma sensible",
"compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment", "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
"compose_form.spoiler_placeholder": "Avertiment", "compose_form.spoiler_placeholder": "Escrivètz lavertiment aquí",
"confirmation_modal.cancel": "Anullar", "confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar", "confirmations.block.confirm": "Blocar",
"confirmations.block.message": "Sètz segur de voler blocar {name} ?", "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
"confirmations.delete.confirm": "Suprimir", "confirmations.delete.confirm": "Suprimir",
"confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?", "confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni", "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.confirm": "Metre en silenci",
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
"confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?", "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.flags": "Drapèus",
"emoji_button.food": "Beure e manjar", "emoji_button.food": "Beure e manjar",
"emoji_button.label": "Inserir un emoji", "emoji_button.label": "Inserir un emoji",
"emoji_button.nature": "Natura", "emoji_button.nature": "Natura",
"emoji_button.objects": "Objèctes", "emoji_button.objects": "Objèctes",
"emoji_button.people": "Gents", "emoji_button.people": "Gents",
"emoji_button.search": "Cercar...", "emoji_button.search": "Cercar",
"emoji_button.symbols": "Simbòls", "emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs", "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.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 dautras personas.", "empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.",
"empty_column.home.inactivity": "Vòstra pagina dacuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.", "empty_column.home.inactivity": "Vòstra pagina dacuè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.home.public_timeline": "lo flux public",
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.", "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.",
"empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas dautras 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 dautras instàncias per garnir lo flux public.",
"follow_request.authorize": "Autorizar", "follow_request.authorize": "Autorizar",
"follow_request.reject": "Regetar", "follow_request.reject": "Regetar",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
@ -105,11 +108,11 @@
"navigation_bar.preferences": "Preferéncias", "navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global", "navigation_bar.public_timeline": "Flux public global",
"notification.favourite": "{name} a ajustat a sos favorits :", "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.mention": "{name} vos a mencionat :",
"notification.reblog": "{name} a partejat vòstre estatut :", "notification.reblog": "{name} a partejat vòstre estatut :",
"notifications.clear": "Levar", "notifications.clear": "Escafar",
"notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?", "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
"notifications.column_settings.alert": "Notificacions localas", "notifications.column_settings.alert": "Notificacions localas",
"notifications.column_settings.favourite": "Favorits :", "notifications.column_settings.favourite": "Favorits :",
"notifications.column_settings.follow": "Nòus seguidors :", "notifications.column_settings.follow": "Nòus seguidors :",
@ -121,15 +124,15 @@
"notifications.column_settings.sound": "Emetre un son", "notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach", "onboarding.done": "Fach",
"onboarding.next": "Seguent", "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 dacuèlh mòstra los estatuts del mond que seguètz.", "onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.",
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun enteragís amb vos", "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun 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.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.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon !", "onboarding.page_one.welcome": "Benvengut a Mastodon !",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.", "onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"onboarding.page_six.almost_done": "Gaireben acabat…", "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 daplicacions per mobil per iOS, Android e mai.", "onboarding.page_six.apps_available": "I a daplicacions 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.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", "onboarding.page_six.guidelines": "guida de la comunitat",
@ -170,7 +173,7 @@
"status.report": "Senhalar @{name}", "status.report": "Senhalar @{name}",
"status.sensitive_toggle": "Clicar per mostrar", "status.sensitive_toggle": "Clicar per mostrar",
"status.sensitive_warning": "Contengut sensible", "status.sensitive_warning": "Contengut sensible",
"status.share": "Share", "status.share": "Partejar",
"status.show_less": "Tornar plegar", "status.show_less": "Tornar plegar",
"status.show_more": "Desplegar", "status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat", "status.unmute_conversation": "Conversacions amb silenci levat",

View File

@ -13,6 +13,7 @@
"account.posts": "Posty", "account.posts": "Posty",
"account.report": "Zgłoś @{name}", "account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba", "account.requested": "Oczekująca prośba",
"account.share": "Udostępnij profil @{name}",
"account.unblock": "Odblokuj @{name}", "account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}", "account.unblock_domain": "Odblokuj domenę {domain}",
"account.unfollow": "Przestań śledzić", "account.unfollow": "Przestań śledzić",
@ -35,6 +36,8 @@
"column.public": "Globalna oś czasu", "column.public": "Globalna oś czasu",
"column_back_button.label": "Wróć", "column_back_button.label": "Wróć",
"column_header.hide_settings": "Ukryj ustawienia", "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.pin": "Przypnij",
"column_header.show_settings": "Pokaż ustawienia", "column_header.show_settings": "Pokaż ustawienia",
"column_header.unpin": "Cofnij przypięcie", "column_header.unpin": "Cofnij przypięcie",

View File

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Denunciar @{name}", "account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação", "account.requested": "A aguardar aprovação",
"account.share": "Share @{name}'s profile",
"account.unblock": "Não bloquear @{name}", "account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
@ -35,6 +36,8 @@
"column.public": "Global", "column.public": "Global",
"column_back_button.label": "Voltar", "column_back_button.label": "Voltar",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Denunciar @{name}", "account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação", "account.requested": "A aguardar aprovação",
"account.share": "Share @{name}'s profile",
"account.unblock": "Não bloquear @{name}", "account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
@ -35,6 +36,8 @@
"column.public": "Global", "column.public": "Global",
"column_back_button.label": "Voltar", "column_back_button.label": "Voltar",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Посты", "account.posts": "Посты",
"account.report": "Пожаловаться", "account.report": "Пожаловаться",
"account.requested": "Ожидает подтверждения", "account.requested": "Ожидает подтверждения",
"account.share": "Share @{name}'s profile",
"account.unblock": "Разблокировать", "account.unblock": "Разблокировать",
"account.unblock_domain": "Разблокировать {domain}", "account.unblock_domain": "Разблокировать {domain}",
"account.unfollow": "Отписаться", "account.unfollow": "Отписаться",
@ -35,6 +36,8 @@
"column.public": "Глобальная лента", "column.public": "Глобальная лента",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings", "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.pin": "Закрепить",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Открепить", "column_header.unpin": "Открепить",

View File

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Awaiting approval", "account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Unblock @{name}", "account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow", "account.unfollow": "Unfollow",
@ -35,6 +36,8 @@
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Gönderiler", "account.posts": "Gönderiler",
"account.report": "Rapor et @{name}", "account.report": "Rapor et @{name}",
"account.requested": "Onay bekleniyor", "account.requested": "Onay bekleniyor",
"account.share": "Share @{name}'s profile",
"account.unblock": "Engeli kaldır @{name}", "account.unblock": "Engeli kaldır @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Takipten vazgeç", "account.unfollow": "Takipten vazgeç",
@ -35,6 +36,8 @@
"column.public": "Federe zaman tüneli", "column.public": "Federe zaman tüneli",
"column_back_button.label": "Geri", "column_back_button.label": "Geri",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "Пости", "account.posts": "Пости",
"account.report": "Поскаржитися", "account.report": "Поскаржитися",
"account.requested": "Очікує підтвердження", "account.requested": "Очікує підтвердження",
"account.share": "Share @{name}'s profile",
"account.unblock": "Розблокувати", "account.unblock": "Розблокувати",
"account.unblock_domain": "Розблокувати {domain}", "account.unblock_domain": "Розблокувати {domain}",
"account.unfollow": "Відписатися", "account.unfollow": "Відписатися",
@ -35,6 +36,8 @@
"column.public": "Глобальна стрічка", "column.public": "Глобальна стрічка",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "嘟文", "account.posts": "嘟文",
"account.report": "举报 @{name}", "account.report": "举报 @{name}",
"account.requested": "等待审批", "account.requested": "等待审批",
"account.share": "Share @{name}'s profile",
"account.unblock": "解除对 @{name} 的屏蔽", "account.unblock": "解除对 @{name} 的屏蔽",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "取消关注", "account.unfollow": "取消关注",
@ -35,6 +36,8 @@
"column.public": "跨站公共时间轴", "column.public": "跨站公共时间轴",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "文章", "account.posts": "文章",
"account.report": "舉報 @{name}", "account.report": "舉報 @{name}",
"account.requested": "等候審批", "account.requested": "等候審批",
"account.share": "Share @{name}'s profile",
"account.unblock": "解除對 @{name} 的封鎖", "account.unblock": "解除對 @{name} 的封鎖",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "取消關注", "account.unfollow": "取消關注",
@ -35,6 +36,8 @@
"column.public": "跨站時間軸", "column.public": "跨站時間軸",
"column_back_button.label": "返回", "column_back_button.label": "返回",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -13,6 +13,7 @@
"account.posts": "貼文", "account.posts": "貼文",
"account.report": "檢舉 @{name}", "account.report": "檢舉 @{name}",
"account.requested": "正在等待許可", "account.requested": "正在等待許可",
"account.share": "Share @{name}'s profile",
"account.unblock": "取消封鎖 @{name}", "account.unblock": "取消封鎖 @{name}",
"account.unblock_domain": "不再隱藏 {domain}", "account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注", "account.unfollow": "取消關注",
@ -35,6 +36,8 @@
"column.public": "聯盟時間軸", "column.public": "聯盟時間軸",
"column_back_button.label": "上一頁", "column_back_button.label": "上一頁",
"column_header.hide_settings": "Hide settings", "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.pin": "Pin",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",

View File

@ -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(notifications[0].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: notifications[0].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 handlePush = (event) => {
const options = event.data.json(); const options = event.data.json();
@ -17,7 +59,7 @@ const handlePush = (event) => {
options.actions = options.data.actions; options.actions = options.data.actions;
} }
event.waitUntil(self.registration.showNotification(options.title, options)); event.waitUntil(notify(options));
}; };
const cloneNotification = (notification) => { const cloneNotification = (notification) => {
@ -50,22 +92,37 @@ const makeRequest = (notification, action) =>
credentials: 'include', 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 => const openUrl = url =>
self.clients.matchAll({ type: 'window' }).then(clientList => { self.clients.matchAll({ type: 'window' }).then(clientList => {
if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate if (clientList.length !== 0) {
const webClients = clientList const webClients = clientList.filter(client => /\/web\//.test(client.url));
.filter(client => /\/web\//.test(client.url))
.sort(client => client !== 'visible');
const visibleClient = clientList.find(client => client.visibilityState === 'visible'); if (webClients.length !== 0) {
const focusedClient = clientList.find(client => client.focused); 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()); if (pathname.startsWith('/web/')) {
} else { return client.focus().then(client => client.postMessage({
return self.clients.openWindow(url); 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) => { const removeActionFromNotification = (notification, action) => {

View File

@ -121,7 +121,7 @@
.information-board { .information-board {
background: darken($ui-base-color, 4%); background: darken($ui-base-color, 4%);
padding: 40px 0; padding: 20px 0;
.panel { .panel {
position: absolute; position: absolute;
@ -162,13 +162,14 @@
.information-board-sections { .information-board-sections {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
} }
.section { .section {
flex: 1 0 0; flex: 1 0 0;
font: 16px/28px 'mastodon-font-sans-serif', sans-serif; font: 16px/28px 'mastodon-font-sans-serif', sans-serif;
text-align: right; text-align: right;
padding: 0 15px; padding: 10px 15px;
span, span,
strong { strong {
@ -190,14 +191,6 @@
color: $primary-text-color; color: $primary-text-color;
} }
} }
@media screen and (max-width: 500px) {
flex-direction: column;
.section {
text-align: left;
}
}
} }
.owner { .owner {
@ -547,6 +540,7 @@
} }
#mastodon-timeline { #mastodon-timeline {
display: flex;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar; -ms-overflow-style: -ms-autohiding-scrollbar;
font-family: 'mastodon-font-sans-serif', sans-serif; font-family: 'mastodon-font-sans-serif', sans-serif;
@ -561,11 +555,20 @@
overflow: hidden; overflow: hidden;
box-shadow: 0 0 6px rgba($black, 0.1); 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 { .column {
padding: 0; padding: 0;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
height: 100%;
} }
.scrollable { .scrollable {
@ -651,16 +654,12 @@
padding: 0 20px; padding: 0 20px;
} }
.information-board {
padding-bottom: 20px;
}
.information-board .container { .information-board .container {
padding-right: 20px; padding-right: 20px;
.panel { .panel {
position: static; position: static;
margin-top: 30px; margin-top: 20px;
width: 100%; width: 100%;
border-radius: 4px; border-radius: 4px;

View File

@ -214,16 +214,18 @@
} }
.dropdown--active::after { .dropdown--active::after {
content: ""; @media screen and (min-width: 1025px) {
display: block; content: "";
position: absolute; display: block;
width: 0; position: absolute;
height: 0; width: 0;
border-style: solid; height: 0;
border-width: 0 4.5px 7.8px; border-style: solid;
border-color: transparent transparent $ui-secondary-color; border-width: 0 4.5px 7.8px;
bottom: 8px; border-color: transparent transparent $ui-secondary-color;
right: 104px; bottom: 8px;
right: 104px;
}
} }
.invisible { .invisible {
@ -3402,7 +3404,8 @@ button.icon-button.active i.fa-retweet {
.boost-modal, .boost-modal,
.confirmation-modal, .confirmation-modal,
.report-modal { .report-modal,
.actions-modal {
background: lighten($ui-secondary-color, 8%); background: lighten($ui-secondary-color, 8%);
color: $ui-base-color; color: $ui-base-color;
border-radius: 8px; border-radius: 8px;
@ -3427,6 +3430,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 { .boost-modal__container {
overflow-x: scroll; overflow-x: scroll;
padding: 10px; padding: 10px;
@ -3493,6 +3505,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__action-bar {
.confirmation-modal__cancel-button { .confirmation-modal__cancel-button {
background-color: transparent; background-color: transparent;

View File

@ -53,6 +53,7 @@ class Web::PushSubscription < ApplicationRecord
url: url, url: url,
actions: actions, actions: actions,
access_token: access_token, access_token: access_token,
message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker
} }
), ),
endpoint: endpoint, endpoint: endpoint,
@ -117,7 +118,7 @@ class Web::PushSubscription < ApplicationRecord
when :mention then [ when :mention then [
{ {
title: translate('push_notifications.mention.action_favourite'), 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', todo: 'request',
method: 'POST', method: 'POST',
action: "/api/v1/statuses/#{notification.target_status.id}/favourite", 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? can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden?
if should_hide 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 end
if can_boost 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_boost.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
end end
actions actions

View File

@ -18,7 +18,7 @@ class AccountSearchService < BaseService
return [] if query_blank_or_hashtag? || limit < 1 return [] if query_blank_or_hashtag? || limit < 1
if resolving_non_matching_remote_account? if resolving_non_matching_remote_account?
[ResolveRemoteAccountService.new.call("#{query_username}@#{query_domain}")] [ResolveRemoteAccountService.new.call("#{query_username}@#{query_domain}")].compact
else else
search_results_and_exact_match.compact.uniq.slice(0, limit) search_results_and_exact_match.compact.uniq.slice(0, limit)
end end

View File

@ -19,10 +19,10 @@
%td %td
%samp= session.ip %samp= session.ip
%td %td
- if request.session['auth_id'] == session.session_id - if current_session.session_id == session.session_id
= t 'sessions.current_session' = t 'sessions.current_session'
- else - else
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at) %time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)
%td %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 = table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete

View File

@ -3,7 +3,7 @@
class Pubsubhubbub::SubscribeWorker class Pubsubhubbub::SubscribeWorker
include Sidekiq::Worker 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| sidekiq_retry_in do |count|
case count case count
@ -18,6 +18,12 @@ class Pubsubhubbub::SubscribeWorker
end end
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) def perform(account_id)
account = Account.find(account_id) account = Account.find(account_id)
logger.debug "PuSH re-subscribing to #{account.acct}" logger.debug "PuSH re-subscribing to #{account.acct}"

View File

@ -7,18 +7,19 @@ class WebPushNotificationWorker
def perform(session_activation_id, notification_id) def perform(session_activation_id, notification_id)
session_activation = SessionActivation.find(session_activation_id) session_activation = SessionActivation.find(session_activation_id)
notification = Notification.find(notification_id) notification = Notification.find(notification_id)
return if session_activation.nil? || notification.nil? return if session_activation.web_push_subscription.nil? || notification.activity.nil?
begin session_activation.web_push_subscription.push(notification)
session_activation.web_push_subscription.push(notification) rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription => e # Subscription expiration is not currently implemented in any browser
# Subscription expiration is not currently implemented in any browser
session_activation.web_push_subscription.destroy!
session_activation.update!(web_push_subscription: nil)
raise e session_activation.web_push_subscription.destroy!
end session_activation.update!(web_push_subscription: nil)
true
rescue ActiveRecord::RecordNotFound
true
end end
end end

View File

@ -1,7 +1,7 @@
run.config: run.config:
engine: ruby engine: ruby
engine.config: engine.config:
runtime: ruby-2.4.1 runtime: ruby-2.4
extra_packages: extra_packages:
# basic servers: # basic servers:
@ -20,6 +20,9 @@ run.config:
# for node-gyp, used in the asset compilation process: # for node-gyp, used in the asset compilation process:
- python-2 - python-2
# i18n:
- libidn
cache_dirs: cache_dirs:
- node_modules - node_modules
@ -35,10 +38,6 @@ run.config:
extra_steps: extra_steps:
- envsubst < .env.nanobox > .env - envsubst < .env.nanobox > .env
- gem install bundler
- bundle config build.nokogiri --with-iconv-dir=/data/ --with-zlib-dir=/data/
- bundle config build.nokogumbo --with-iconv-dir=/data/ --with-zlib-dir=/data/
- bundle install --clean
- yarn - yarn
fs_watch: true fs_watch: true

View File

@ -348,6 +348,8 @@ en:
title: "%{name} favourited your status" title: "%{name} favourited your status"
follow: follow:
title: "%{name} is now following you" title: "%{name} is now following you"
group:
title: "%{count} notifications"
mention: mention:
action_boost: Boost action_boost: Boost
action_expand: Show more action_expand: Show more

View File

@ -1,19 +1,38 @@
--- ---
oc: oc:
about: about:
about_mastodon_html: Mastodon es un malhum social <em>liure e open-source</em>. Una alternativa <em>descentralizada</em> a las plat-formas comercialas, aquò evita quuna sola companhiá monopolize vòstra comunicacion. Causissètz un servidor que vos fisatz, quina que siasque vòstra causida, podètz interagir amb tot lo mond. Qual que siasque pòt aver son instància Mastodon e participar al <em>malhum social</em> sens cap de problèmas. about_mastodon_html: Mastodon es un malhum social bastit amb de protocòls liures e gratuits. Es descentralizat coma los corrièls.
about_this: A prepaus daquesta instància about_this: A prepaus daquesta instància
closed_registrations: Las inscripcions son clavadas pel moment sus aquesta instància. closed_registrations: Las inscripcions son clavadas pel moment sus aquesta instància.
contact: Contacte contact: Contacte
contact_missing: Pas parametrat
contact_unavailable: Pas disponible
description_headline: Qué es %{domain} ? description_headline: Qué es %{domain} ?
domain_count_after: autras instàncias domain_count_after: autras instàncias
domain_count_before: Connectat a domain_count_before: Connectat a
other_instances: Autras instàncias extended_description_html: |
<h3>Una bona plaça per las règlas</h3>
<p>La descripcion longa es pas estada causida pel moment.</p>
features:
humane_approach_body: Amb lexperiéncia dels fracasses dautres malhums, Mastodon ten per objectiu de lutar contra los abuses dels malhums socials en far de causidas eticas.
humane_approach_title: Un biais mai uman
not_a_product_body: Mastodon es pas un malhum comercial. Pas cap de reclama, dutilizacion de vòstras donadas o dòrt daurat clavat. I a pas cap dautoritat centrala.
not_a_product_title: Sètz una persona, non pas un produit
real_conversation_body: Amb 500 caractèrs a vòstra disposicion e un nivèl de confidencialitat per cada publicacion, podètz vos exprimir coma volètz.
real_conversation_title: Fach per de conversacions vertadièras
within_reach_body: Multiplas aplicacion per iOS, Android, e autras plataformas mercés a un entorn API de bon utilizar, vos permet de gardar lo contacte pertot.
within_reach_title: Totjorn al costat
find_another_instance: Trobar mai instàncias
generic_description: "%{domain} es un dels servidors del malhum"
hosted_on: Mastodon albergat sus %{domain}
learn_more: Ne saber mai
other_instances: Lista dinstàncias
source_code: Còdi font source_code: Còdi font
status_count_after: estatuts status_count_after: estatuts
status_count_before: quan escrich status_count_before: quan escrich
user_count_after: personas user_count_after: personas
user_count_before: Ostal de user_count_before: Ostal de
what_is_mastodon: Ques Mastodon ?
accounts: accounts:
follow: Sègre follow: Sègre
followers: Seguidors followers: Seguidors
@ -23,6 +42,7 @@ oc:
people_who_follow: Lo mond que sègon %{name} people_who_follow: Lo mond que sègon %{name}
posts: Estatuts posts: Estatuts
remote_follow: Sègre a distància remote_follow: Sègre a distància
reserved_username: Aqueste nom dutilizaire es reservat
unfollow: Quitar de sègre unfollow: Quitar de sègre
admin: admin:
accounts: accounts:
@ -60,8 +80,10 @@ oc:
profile_url: URL del perfil profile_url: URL del perfil
public: Public public: Public
push_subscription_expires: Fin de labonament PuSH push_subscription_expires: Fin de labonament PuSH
redownload: Actualizar los avatars
reset: Reïnicializar reset: Reïnicializar
reset_password: Reïnicializar lo senhal reset_password: Reïnicializar lo senhal
resubscribe: Se tornar abonar
salmon_url: URL Salmon salmon_url: URL Salmon
search: Cercar search: Cercar
show: show:
@ -70,13 +92,14 @@ oc:
targeted_reports: Rapòrts faches tocant aqueste compte targeted_reports: Rapòrts faches tocant aqueste compte
silence: Silenci silence: Silenci
statuses: Estatuts statuses: Estatuts
subscribe: Sabonar
title: Comptes title: Comptes
undo_silenced: Levar lo silenci undo_silenced: Levar lo silenci
undo_suspension: Levar la suspension undo_suspension: Levar la suspension
username: Nom dutilizaire username: Nom dutilizaire
web: Web web: Web
domain_blocks: domain_blocks:
add_new: Ajustar un nòu add_new: Najustar un nòu
created_msg: Domeni blocat es a èsser tractat created_msg: Domeni blocat es a èsser tractat
destroyed_msg: Lo blocatge del domeni es estat levat destroyed_msg: Lo blocatge del domeni es estat levat
domain: Domeni domain: Domeni
@ -85,12 +108,14 @@ oc:
hint: Lo blocatge empacharà pas la creacion de compte dins la basa de donadas, mai aplicarà la moderacion sus aquestes comptes. hint: Lo blocatge empacharà pas la creacion de compte dins la basa de donadas, mai aplicarà la moderacion sus aquestes comptes.
severity: severity:
desc_html: "<strong>Silenci</strong> farà venir invisibles los estatuts del compte al mond que son pas de seguidors. <strong>Suspendre</strong> levarà tot lo contengut del compte, los mèdias e las donadas de perfil." desc_html: "<strong>Silenci</strong> farà venir invisibles los estatuts del compte al mond que son pas de seguidors. <strong>Suspendre</strong> levarà tot lo contengut del compte, los mèdias e las donadas de perfil."
noop: Cap
silence: Silenci silence: Silenci
suspend: Suspendre suspend: Suspendre
title: Nòu blocatge domeni title: Nòu blocatge domeni
reject_media: Regetar los fichièrs mèdias reject_media: Regetar los fichièrs mèdias
reject_media_hint: Lèva los fichièrs gardats localament e regèta las demandas de telecargament dins lo futur. Servís pas a res per las suspensions reject_media_hint: Lèva los fichièrs gardats localament e regèta las demandas de telecargament dins lo futur. Servís pas a res per las suspensions
severities: severities:
noop: Cap
silence: Silenci silence: Silenci
suspend: Suspendre suspend: Suspendre
severity: Severitat severity: Severitat
@ -110,6 +135,7 @@ oc:
domain_name: Domeni domain_name: Domeni
title: Instàncias conegudas title: Instàncias conegudas
reports: reports:
action_taken_by: Accion menada per
are_you_sure: Es segur ? are_you_sure: Es segur ?
comment: comment:
label: Comentari label: Comentari
@ -147,7 +173,7 @@ oc:
desc_html: Autorizar lo monde a se marcar desc_html: Autorizar lo monde a se marcar
title: Inscripcions title: Inscripcions
site_description: site_description:
desc_html: Afichada jos la forma de paragrafe sus la pagina dacuèlh e utilizada coma balisa meta.<br> Podètz utilizar de balisas HTML, coma <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>. desc_html: Afichada jos la forma de paragraf sus la pagina dacuèlh e utilizada coma balisa meta.<br> Podètz utilizar de balisas HTML, coma <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
title: Descripcion del site title: Descripcion del site
site_description_extended: site_description_extended:
desc_html: Afichada sus la pagina dinformacion complementària del site<br>Podètz utilizar de balisas HTML desc_html: Afichada sus la pagina dinformacion complementària del site<br>Podètz utilizar de balisas HTML
@ -183,6 +209,10 @@ oc:
title: WebSub title: WebSub
topic: Subjècte topic: Subjècte
title: Administracion title: Administracion
admin_mailer:
new_report:
body: "%{reporter} a senhalat %{target}"
subject: Novèl senhalament per %{instance} (#%{id})
application_mailer: application_mailer:
settings: 'Cambiar las preferéncias de corrièl : %{link}' settings: 'Cambiar las preferéncias de corrièl : %{link}'
signature: Notificacion de Mastodon sus %{instance} signature: Notificacion de Mastodon sus %{instance}
@ -190,7 +220,8 @@ oc:
applications: applications:
invalid_url: LURL donada es invalida invalid_url: LURL donada es invalida
auth: auth:
change_password: Cambiar lo senhal agreement_html: En vos marcar acceptatz <a href="%{rules_path}">nòstres tèrmes de servici</a> e <a href="%{terms_path}">politica de confidencialitat</a>.
change_password: Seguretat
delete_account: Suprimir lo compte delete_account: Suprimir lo compte
delete_account_html: Se volètz suprimir vòstre compte, podètz <a href="%{path}">o far aquí</a>. Vos demandarem que confirmetz. delete_account_html: Se volètz suprimir vòstre compte, podètz <a href="%{path}">o far aquí</a>. Vos demandarem que confirmetz.
didnt_get_confirmation: Avètz pas recebut las instruccions de confirmacion ? didnt_get_confirmation: Avètz pas recebut las instruccions de confirmacion ?
@ -204,6 +235,12 @@ oc:
authorize_follow: authorize_follow:
error: O planhèm, i a agut una error al moment de cercar lo compte error: O planhèm, i a agut una error al moment de cercar lo compte
follow: Sègre follow: Sègre
follow_request: 'Avètz demandat de sègre :'
following: 'Felicitacion ! Seguètz ara :'
post_follow:
close: O podètz tampar aquesta fenèstra.
return: Tornar al perfil
web: Tornar a linterfàcia Web
prompt_html: 'Avètz (<strong>%{self}</strong>) demandat de sègre :' prompt_html: 'Avètz (<strong>%{self}</strong>) demandat de sègre :'
title: Sègre %{acct} title: Sègre %{acct}
date: date:
@ -288,12 +325,14 @@ oc:
warning_html: La supression del contengut daquesta instància es sola assegurada. Lo contengut fòrça partejat daissarà probablament de traças. Los servidors fòra-linha e los que vos sègon pas mai auràn pas la mesa a jorn de lor basa de donada. warning_html: La supression del contengut daquesta instància es sola assegurada. Lo contengut fòrça partejat daissarà probablament de traças. Los servidors fòra-linha e los que vos sègon pas mai auràn pas la mesa a jorn de lor basa de donada.
warning_title: Disponibilitat del contengut difusat warning_title: Disponibilitat del contengut difusat
errors: errors:
'403': Avètz pas lautorizacion de veire aquesta pagina.
'404': La pagina que recercatz existís pas. '404': La pagina que recercatz existís pas.
'410': La pagina que cercatz existís pas mai. '410': La pagina que cercatz existís pas mai.
'422': '422':
content: Verificacion de seguretat fracassada. Blocatz los cookies ? content: Verificacion de seguretat fracassada. Blocatz los cookies ?
title: Verificacion de seguretat fracassada title: Verificacion de seguretat fracassada
'429': Lo servidor mòla (subrecargada) '429': Lo servidor mòla (subrecargada)
noscript: Per utilizar laplicacion web de Mastodon, mercés dactivar JavaScript. O podètz utilizar una aplicacion per vòstra plataforma coma alernativa.
exports: exports:
blocks: Personas que blocatz blocks: Personas que blocatz
csv: CSV csv: CSV
@ -327,7 +366,7 @@ oc:
following: Lista de mond que seguètz following: Lista de mond que seguètz
muting: Lista de mond que volètz pas legir muting: Lista de mond que volètz pas legir
upload: Importar upload: Importar
landing_strip_html: "<strong>%{name}</strong> es un utilizaire de %{link_to_root_path}. Podètz lo/la sègre o interagir amb el o ela savètz un compte ont que siasque sul fediverse." landing_strip_html: "<strong>%{name}</strong> utiliza %{link_to_root_path}. Podètz lo/la sègre o interagir amb el o ela savètz un compte ont que siasque sul fediverse."
landing_strip_signup_html: Ses pas lo cas, podètz <a href="%{sign_up_path}">vos marcar aquí</a>. landing_strip_signup_html: Ses pas lo cas, podètz <a href="%{sign_up_path}">vos marcar aquí</a>.
media_attachments: media_attachments:
validations: validations:
@ -362,6 +401,23 @@ oc:
next: Seguent next: Seguent
prev: Precedent prev: Precedent
truncate: "&hellip;" truncate: "&hellip;"
push_notifications:
favourite:
title: "%{name} a mes vòstre estatut en favorit"
follow:
title: "%{name} vos sèc ara"
group:
title: "%{count} notificacions"
mention:
action_boost: Partejar
action_expand: Ne veire mai
action_favourite: Ajustar als favorits
title: "%{name} vos a mencionat"
reblog:
title: "%{name} a partejat vòstre estatut"
subscribed:
body: Podètz ara recebre las notificacions push.
title: Abonament enregistrat !
remote_follow: remote_follow:
acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire
missing_resource: URL de redireccion pas trobada missing_resource: URL de redireccion pas trobada
@ -438,7 +494,7 @@ oc:
<h3 id="collect">Quinas informacions collectem ?</h3> <h3 id="collect">Quinas informacions collectem ?</h3>
<p>Collectem informacions sus vos quand vos marcatz sus nòstre site e juntem las donadas quand participatz a nòstre forum ne legissent, escrivent e notant lo contengut partejat aquí.</p> <p>Collectem informacions sus vos quand vos marcatz sus nòstre site e juntem las donadas quand participatz a nòstre forum en legissent, escrivent e notant lo contengut partejat aquí.</p>
<p>Pendent linscripcion podèm vos demandar vòstre nom e adreça de corrièl. Podètz çaquelà visitar nòstre site sens vos marcar. Verificarem vòstra adreça amb un messatge donant un ligam unic. Se clicatz sul ligam sauprem quavètz lo contraròtle de ladreça.</p> <p>Pendent linscripcion podèm vos demandar vòstre nom e adreça de corrièl. Podètz çaquelà visitar nòstre site sens vos marcar. Verificarem vòstra adreça amb un messatge donant un ligam unic. Se clicatz sul ligam sauprem quavètz lo contraròtle de ladreça.</p>
@ -472,13 +528,13 @@ oc:
<p>Òc-ben. Los cookies son de pichons fichièrs quun site o sos forneires de servicis plaçan dins lo disc dur de vòstre ordenador via lo navigator Web (Se los acceptatz). Aqueles cookies permeton al site de reconéisser vòstre navigator e se tenètz un compte enregistrat de lassociar a vòstre compte.</p> <p>Òc-ben. Los cookies son de pichons fichièrs quun site o sos forneires de servicis plaçan dins lo disc dur de vòstre ordenador via lo navigator Web (Se los acceptatz). Aqueles cookies permeton al site de reconéisser vòstre navigator e se tenètz un compte enregistrat de lassociar a vòstre compte.</p>
<p>Empleguem de cookies per comprendre e enregistrar vòstras preferéncias per vòstras visitas venentas, per recampar de donadas sul trafic del site e las interaccions per fin que posquem ofrir una melhora experiéncia del site e de las aisinas pel futur. Pòt arribar que contractèssem amb de forneires de servicis tèrces per nos ajudar a comprendre melhor nòstres visitors. Aqueles forneires an pas lo drech que dutilizar las donadas collectadas per nos ajudar a menar e melhorar nòstre afar.</p> <p>Empleguem de cookies per comprendre e enregistrar vòstras preferéncias per vòstras visitas venentas, per recampar de donadas sul trafic del site e las interaccions per dire que posquem ofrir una melhora experiéncia del site e de las aisinas pel futur. Pòt arribar que contractèssem amb de forneires de servicis tèrces per nos ajudar a comprendre melhor nòstres visitors. Aqueles forneires an pas lo drech que dutilizar las donadas collectadas per nos ajudar a menar e melhorar nòstre afar.</p>
<h3 id="disclose">Divulguem dinformacions a de partits exteriors ?</h3> <h3 id="disclose">Divulguem dinformacions a de tèrces ?</h3>
<p>Vendèm pas, comercem o qualque transferiment que siasque a de partits exteriors vòstras informacions personalas identificablas. Aquò inclutz pas los tèrces partits de confisança que nos assiston a menar nòstre site, menar nòstre afar o vos servir, baste que son dacòrd per gardar aquelas informacions confidencialas. Pòt tanben arribar que liberèssem vòstras informacions quand cresèm ques apropriat do far per se sometre a la lei, per refortir nòstras politicas, o per protegir los dreches, proprietats o seguritat de qualquun o de nosautres. Pasmens es possible que mandèssem dinformacions non-personalas e identificablas de nòstres visitors a dautres partits per dutilizacion en marketing, publicitat o un emplec mai.</p> <p>Vendèm pas, comercem o qualque transferiment que siasque a de tèrces vòstras informacions personalas identificablas. Aquò inclutz pas los tèrces partits de confisança que nos assiston a menar nòstre site, menar nòstre afar o vos servir, baste que son dacòrd per gardar aquelas informacions confidencialas. Pòt tanben arribar que liberèssem vòstras informacions quand cresèm ques apropriat do far per se sometre a la lei, per refortir nòstras politicas, o per protegir los dreches, proprietats o seguritat de qualquun o de nosautres. Pasmens es possible que mandèssem dinformacions non-personalas e identificablas de nòstres visitors a dautres partits per dutilizacion en marketing, publicitat o un emplec mai.</p>
<h3 id="third-party">Ligams de tèrces partits</h3> <h3 id="third-party">Ligams de tèrces</h3>
<p>Pòt arribar, a nòstra discrecion, quincluguèssem o ofriguèssem de produches o servicis de tèrces partits sus nòstre site. Aqueles sites tèrces an de politicas de confidencialitats separadas e independentas. En consequéncia avèm pas cap de responsabilitat pel contengut e las activitats daqueles sites ligats. Pasmens cerquem de protegir lintegritat de nòstre site e aculhèm los comentaris tocant aqueles sites.</p> <p>Pòt arribar, a nòstra discrecion, quincluguèssem o ofriguèssem de produches o servicis de tèrces partits sus nòstre site. Aqueles sites tèrces an de politicas de confidencialitats separadas e independentas. En consequéncia avèm pas cap de responsabilitat pel contengut e las activitats daqueles sites ligats. Pasmens cerquem de protegir lintegritat de nòstre site e aculhèm los comentaris tocant aqueles sites.</p>
@ -515,6 +571,7 @@ oc:
instructions_html: "<strong>Escanatz aqueste còdi QR amb Google Authenticator o una aplicacion similària sus vòstre mobil</strong>. A partir dara, aquesta aplicacion generarà un geton que vos caldrà picar per vos connectar." instructions_html: "<strong>Escanatz aqueste còdi QR amb Google Authenticator o una aplicacion similària sus vòstre mobil</strong>. A partir dara, aquesta aplicacion generarà un geton que vos caldrà picar per vos connectar."
lost_recovery_codes: Los còdi de recuperacion vos permeton daccedir a vòstre compte se perdètz vòstre mobil. Savètz perdut vòstres còdis de recuperacion los podètz tornar generar aquí. Los ancians còdis seràn pas mai valides. lost_recovery_codes: Los còdi de recuperacion vos permeton daccedir a vòstre compte se perdètz vòstre mobil. Savètz perdut vòstres còdis de recuperacion los podètz tornar generar aquí. Los ancians còdis seràn pas mai valides.
manual_instructions: 'Se podètz pas numerizar lo còdi QR e que vos cal picar lo còdi a la man, vaquí lo còdi en clar :' manual_instructions: 'Se podètz pas numerizar lo còdi QR e que vos cal picar lo còdi a la man, vaquí lo còdi en clar :'
recovery_codes: Salvar los còdis de recuperacion
recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar
recovery_instructions_html: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants. recovery_instructions_html: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants.
setup: Paramètres setup: Paramètres

View File

@ -350,6 +350,8 @@ pl:
title: "%{name} dodał Twój status do ulubionych" title: "%{name} dodał Twój status do ulubionych"
follow: follow:
title: "%{name} zaczął Cię śledzić" title: "%{name} zaczął Cię śledzić"
group:
title: "%{count} powiadomień"
mention: mention:
action_boost: Podbij action_boost: Podbij
action_expand: Pokaż więcej action_expand: Pokaż więcej

View File

@ -267,6 +267,21 @@ ru:
next: След next: След
prev: Пред prev: Пред
truncate: "&hellip;" truncate: "&hellip;"
push_notifications:
favourite:
title: "Ваш статус понравился %{name}"
follow:
title: "%{name} теперь подписан(а) на Вас"
mention:
action_boost: Продвинуть
action_expand: Развернуть
action_favourite: Нравится
title: "Вас упомянул(а) %{name}"
reblog:
title: "%{name} продвинул(а) Ваш статус"
subscribed:
body: Теперь Вы можете получать push-уведомления.
title: Подписка зарегистрирована!
remote_follow: remote_follow:
acct: Введите username@domain, откуда Вы хотите подписаться acct: Введите username@domain, откуда Вы хотите подписаться
missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей
@ -335,6 +350,8 @@ ru:
click_to_show: Показать click_to_show: Показать
reblogged: продвинул(а) reblogged: продвинул(а)
sensitive_content: Чувствительный контент sensitive_content: Чувствительный контент
terms:
title: "Условия обслуживания и политика конфиденциальности %{instance}"
time: time:
formats: formats:
default: "%b %d, %Y, %H:%M" default: "%b %d, %Y, %H:%M"

View File

@ -16,6 +16,7 @@ ru:
many: Осталось <span class="name-counter">%{count}</span> символов many: Осталось <span class="name-counter">%{count}</span> символов
one: Остался <span class="name-counter">1</span> символ one: Остался <span class="name-counter">1</span> символ
other: Осталось <span class="name-counter">%{count}</span> символов other: Осталось <span class="name-counter">%{count}</span> символов
setting_noindex: Относится к Вашему публичному профилю и страницам статусов
imports: imports:
data: Файл CSV, экспортированный с другого узла Mastodon data: Файл CSV, экспортированный с другого узла Mastodon
sessions: sessions:
@ -42,7 +43,11 @@ ru:
setting_auto_play_gif: Автоматически проигрывать анимированные GIF setting_auto_play_gif: Автоматически проигрывать анимированные GIF
setting_boost_modal: Показывать диалог подтверждения перед продвижением setting_boost_modal: Показывать диалог подтверждения перед продвижением
setting_default_privacy: Видимость постов setting_default_privacy: Видимость постов
setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный
setting_delete_modal: Показывать диалог подтверждения перед удалением setting_delete_modal: Показывать диалог подтверждения перед удалением
setting_noindex: Отказаться от индексации в поисковых машинах
setting_system_font_ui: Использовать шрифт системы по умолчанию
setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта
severity: Строгость severity: Строгость
type: Тип импорта type: Тип импорта
username: Имя пользователя username: Имя пользователя

View File

@ -3,6 +3,8 @@
require 'sidekiq/web' require 'sidekiq/web'
require 'sidekiq-scheduler/web' require 'sidekiq-scheduler/web'
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
Rails.application.routes.draw do Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development? mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development?

View File

@ -17,9 +17,11 @@ defaults: &defaults
closed_registrations_message: '' closed_registrations_message: ''
open_deletion: true open_deletion: true
timeline_preview: true timeline_preview: true
default_sensitive: false
unfollow_modal: false
boost_modal: false boost_modal: false
auto_play_gif: false
delete_modal: true delete_modal: true
auto_play_gif: false
system_font_ui: false system_font_ui: false
noindex: false noindex: false
notification_emails: notification_emails:

View File

@ -10,7 +10,11 @@ const { publicPath } = require('./configuration.js');
const path = require('path'); const path = require('path');
module.exports = merge(sharedConfig, { module.exports = merge(sharedConfig, {
output: { filename: '[name]-[chunkhash].js' }, output: {
filename: '[name]-[chunkhash].js',
chunkFilename: '[name]-[chunkhash].js',
},
devtool: 'source-map', // separate sourcemap file, suitable for production devtool: 'source-map', // separate sourcemap file, suitable for production
stats: 'normal', stats: 'normal',
@ -48,7 +52,7 @@ module.exports = merge(sharedConfig, {
ServiceWorker: { ServiceWorker: {
entry: path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'), entry: path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'),
cacheName: 'mastodon', cacheName: 'mastodon',
output: '../sw.js', output: '../assets/sw.js',
publicPath: '/sw.js', publicPath: '/sw.js',
minify: true, minify: true,
}, },

View File

@ -33,7 +33,7 @@ module.exports = {
output: { output: {
filename: '[name].js', filename: '[name].js',
chunkFilename: '[name]-[chunkhash].js', chunkFilename: '[name].js',
path: output.path, path: output.path,
publicPath: output.publicPath, publicPath: output.publicPath,
}, },

View File

@ -21,7 +21,7 @@ module Mastodon
end end
def flags def flags
'rc1' 'rc2'
end end
def to_a def to_a

View File

@ -7,9 +7,8 @@
"build:production": "cross-env RAILS_ENV=production ./bin/webpack", "build:production": "cross-env RAILS_ENV=production ./bin/webpack",
"manage:translations": "node ./config/webpack/translationRunner.js", "manage:translations": "node ./config/webpack/translationRunner.js",
"start": "node ./streaming/index.js", "start": "node ./streaming/index.js",
"storybook": "cross-env NODE_ENV=test start-storybook -s ./public -p 9001 -c storybook",
"test": "npm run test:lint && npm run test:mocha", "test": "npm run test:lint && npm run test:mocha",
"test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ storybook/ streaming/", "test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ streaming/",
"test:mocha": "cross-env NODE_ENV=test mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.js", "test:mocha": "cross-env NODE_ENV=test mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.js",
"postinstall": "npm rebuild node-sass" "postinstall": "npm rebuild node-sass"
}, },
@ -57,7 +56,7 @@
"glob": "^7.1.1", "glob": "^7.1.1",
"http-link-header": "^0.8.0", "http-link-header": "^0.8.0",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"intersection-observer": "^0.3.2", "intersection-observer": "^0.4.0",
"intl": "^1.2.5", "intl": "^1.2.5",
"intl-relativeformat": "^2.0.0", "intl-relativeformat": "^2.0.0",
"is-nan": "^1.2.1", "is-nan": "^1.2.1",
@ -113,15 +112,13 @@
"tiny-queue": "^0.2.1", "tiny-queue": "^0.2.1",
"uuid": "^3.1.0", "uuid": "^3.1.0",
"uws": "^8.14.0", "uws": "^8.14.0",
"webpack": "^3.0.0", "webpack": "^3.4.1",
"webpack-bundle-analyzer": "^2.8.2", "webpack-bundle-analyzer": "^2.8.3",
"webpack-manifest-plugin": "^1.1.2", "webpack-manifest-plugin": "^1.2.1",
"webpack-merge": "^4.1.0", "webpack-merge": "^4.1.0",
"websocket.js": "^0.1.12" "websocket.js": "^0.1.12"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "^3.1.8",
"@storybook/react": "^3.1.8",
"babel-eslint": "^7.2.3", "babel-eslint": "^7.2.3",
"chai": "^4.1.0", "chai": "^4.1.0",
"chai-enzyme": "^0.8.0", "chai-enzyme": "^0.8.0",
@ -134,7 +131,7 @@
"react-intl-translations-manager": "^5.0.0", "react-intl-translations-manager": "^5.0.0",
"react-test-renderer": "^15.6.1", "react-test-renderer": "^15.6.1",
"sinon": "^2.3.7", "sinon": "^2.3.7",
"webpack-dev-server": "^2.5.1", "webpack-dev-server": "^2.6.1",
"yargs": "^8.0.2" "yargs": "^8.0.2"
}, },
"optionalDependencies": { "optionalDependencies": {

1
public/sw.js Symbolic link
View File

@ -0,0 +1 @@
assets/sw.js

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::CredentialsController do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write') }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowerAccountsController do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
before do before do
Fabricate(:follow, target_account: user.account) Fabricate(:follow, target_account: user.account)

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowingAccountsController do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
before do before do
Fabricate(:follow, account: user.account) Fabricate(:follow, account: user.account)

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::RelationshipsController do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::Accounts::SearchController, type: :controller do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::StatusesController do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow read') }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
before do before do
Fabricate(:block, account: user.account) Fabricate(:block, account: user.account)

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
before do before do
user.account.block_domain!('example.com') user.account.block_domain!('example.com')

View File

@ -3,19 +3,77 @@ require 'rails_helper'
RSpec.describe Api::V1::FavouritesController, type: :controller do RSpec.describe Api::V1::FavouritesController, type: :controller do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
before do
Fabricate(:favourite, account: user.account)
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do describe 'GET #index' do
it 'returns http success' do context 'without token' do
get :index, params: { limit: 1 } it 'returns http unauthorized' do
get :index
expect(response).to have_http_status :unauthorized
end
end
expect(response).to have_http_status(:success) context 'with token' do
context 'without read scope' do
before do
allow(controller).to receive(:doorkeeper_token) do
Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: '')
end
end
it 'returns http forbidden' do
get :index
expect(response).to have_http_status :forbidden
end
end
context 'without valid resource owner' do
before do
token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read')
user.destroy!
allow(controller).to receive(:doorkeeper_token) { token }
end
it 'returns http unprocessable entity' do
get :index
expect(response).to have_http_status :unprocessable_entity
end
end
context 'with read scope and valid resource owner' do
before do
allow(controller).to receive(:doorkeeper_token) do
Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read')
end
end
it 'shows favourites owned by the user' do
favourite_by_user = Fabricate(:favourite, account: user.account)
favourite_by_others = Fabricate(:favourite)
get :index
expect(assigns(:statuses)).to match_array [favourite_by_user.status]
end
it 'adds pagination headers if necessary' do
favourite = Fabricate(:favourite, account: user.account)
get :index, params: { limit: 1 }
expect(response.headers['Link'].find_link(['rel', 'next']).href).to eq "http://test.host/api/v1/favourites?limit=1&max_id=#{favourite.id}"
expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/favourites?limit=1&since_id=#{favourite.id}"
end
it 'does not add pagination headers if not necessary' do
get :index
expect(response.headers['Link'].find_link(['rel', 'next'])).to eq nil
expect(response.headers['Link'].find_link(['rel', 'prev'])).to eq nil
end
end
end end
end end
end end

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice', locked: true)) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice', locked: true)) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
let(:follower) { Fabricate(:account, username: 'bob') } let(:follower) { Fabricate(:account, username: 'bob') }
before do before do

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
render_views render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }

Some files were not shown because too many files have changed in this diff Show More