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/packs
node_modules
storybook
neo4j
vendor/bundle
.DS_Store

View File

@ -112,7 +112,7 @@ rules:
jsx-a11y/iframe-has-title: warn
jsx-a11y/img-has-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/no-access-key: warn
jsx-a11y/no-distracting-elements: warn
@ -121,6 +121,6 @@ rules:
jsx-a11y/onclick-has-focus: warn
jsx-a11y/onclick-has-role: 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/tabindex-no-positive: warn

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,8 @@ export default class Status extends ImmutablePureComponent {
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
intersectionObserverWrapper: PropTypes.object,
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
state = {
@ -59,6 +61,7 @@ export default class Status extends ImmutablePureComponent {
'boostModal',
'autoPlayGif',
'muted',
'listLength',
]
updateOnStates = ['isExpanded']
@ -67,8 +70,8 @@ export default class Status extends ImmutablePureComponent {
if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter.
return this.state.isIntersecting || !this.state.isHidden;
// the only things that matter (and updated ARIA attributes).
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
@ -107,17 +110,12 @@ export default class Status extends ImmutablePureComponent {
this.height = getRectFromEntry(entry).height;
}
// Edge 15 doesn't support isIntersecting, but we can infer it
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
// https://github.com/WICG/IntersectionObserver/issues/211
const isIntersecting = (typeof entry.isIntersecting === 'boolean') ?
entry.isIntersecting : entry.intersectionRect.height > 0;
this.setState((prevState) => {
if (prevState.isIntersecting && !isIntersecting) {
if (prevState.isIntersecting && !entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: isIntersecting,
isIntersecting: entry.isIntersecting,
isHidden: false,
};
});
@ -174,7 +172,7 @@ export default class Status extends ImmutablePureComponent {
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status, account, intersectionObserverWrapper, ...other } = this.props;
const { status, account, intersectionObserverWrapper, index, listLength, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
if (status === null) {
@ -183,10 +181,10 @@ export default class Status extends ImmutablePureComponent {
if (!isIntersecting && isHidden) {
return (
<div ref={this.handleRef} data-id={status.get('id')} style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
</article>
);
}
@ -200,14 +198,14 @@ export default class Status extends ImmutablePureComponent {
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
return (
<div className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} >
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
</div>
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
</div>
</article>
);
}
@ -236,7 +234,7 @@ export default class Status extends ImmutablePureComponent {
}
return (
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
<article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex='0' ref={this.handleRef}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@ -254,7 +252,7 @@ export default class Status extends ImmutablePureComponent {
{media}
<StatusActionBar {...this.props} />
</div>
</article>
);
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -156,7 +156,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
{shareButton}
<div className='status__action-bar-dropdown'>
<DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
</div>
</div>
);

View File

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

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

View File

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

View File

@ -159,7 +159,10 @@ export default class ComposeForm extends ImmutablePureComponent {
<div className='compose-form'>
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
<div className='spoiler-input'>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' />
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' />
</label>
</div>
</Collapsable>

View File

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

View File

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

View File

@ -24,6 +24,10 @@ const iconStyle = {
export default class PrivacyDropdown extends React.PureComponent {
static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
@ -34,14 +38,36 @@ export default class PrivacyDropdown extends React.PureComponent {
};
handleToggle = () => {
this.setState({ open: !this.state.open });
if (this.props.isUserTouching()) {
if (this.state.open) {
this.props.onModalClose();
} else {
this.props.onModalOpen({
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
onClick: this.handleModalActionClick,
});
}
} else {
this.setState({ open: !this.state.open });
}
}
handleModalActionClick = (e) => {
e.preventDefault();
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
this.props.onModalClose();
this.props.onChange(value);
}
handleClick = (e) => {
const value = e.currentTarget.getAttribute('data-index');
e.preventDefault();
this.setState({ open: false });
this.props.onChange(value);
if (e.key === 'Escape') {
this.setState({ open: false });
} else if (!e.key || e.key === 'Enter') {
const value = e.currentTarget.getAttribute('data-index');
e.preventDefault();
this.setState({ open: false });
this.props.onChange(value);
}
}
onGlobalClick = (e) => {
@ -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 () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
@ -68,25 +105,18 @@ export default class PrivacyDropdown extends React.PureComponent {
const { value, intl } = this.props;
const { open } = this.state;
const options = [
{ icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) },
{ icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) },
{ icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) },
];
const valueOption = options.find(item => item.value === value);
const valueOption = this.options.find(item => item.value === value);
return (
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} pressed={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__dropdown'>
{open && options.map(item =>
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
{open && this.options.map(item =>
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
<div className='privacy-dropdown__option__content'>
<strong>{item.shortText}</strong>
{item.longText}
<strong>{item.text}</strong>
{item.meta}
</div>
</div>
)}

View File

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

View File

@ -57,16 +57,19 @@ export default class UploadButton extends ImmutablePureComponent {
return (
<div className='compose-form__upload-button'>
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
<input
key={resetFileKey}
ref={this.setRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
style={{ display: 'none' }}
/>
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
<input
key={resetFileKey}
ref={this.setRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
style={{ display: 'none' }}
/>
</label>
</div>
);
}

View File

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

View File

@ -65,21 +65,21 @@ export default class Compose extends React.PureComponent {
const { columns } = this.props;
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') && (
<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') && (
<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') && (
<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') && (
<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='/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='/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)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a>
</nav>
);
}

View File

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

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenu from '../../../components/dropdown_menu';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
@ -13,6 +13,7 @@ const messages = defineMessages({
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' },
});
@injectIntl
@ -58,6 +59,13 @@ export default class ActionBar extends React.PureComponent {
this.props.onReport(this.props.status);
}
handleShare = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
});
}
render () {
const { status, me, intl } = this.props;
@ -71,6 +79,10 @@ export default class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
}
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
);
let reblogIcon = 'retweet';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
@ -82,9 +94,10 @@ export default class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
{shareButton}
<div className='detailed-status__action-bar-dropdown'>
<DropdownMenu size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' />
<DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' />
</div>
</div>
);

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

View File

@ -49,6 +49,10 @@ const mapStateToProps = state => ({
@connect(mapStateToProps)
export default class UI extends React.PureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
}
static propTypes = {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
@ -123,6 +127,14 @@ export default class UI extends React.PureComponent {
this.setState({ draggingOver: false });
}
handleServiceWorkerPostMessage = ({ data }) => {
if (data.type === 'navigate') {
this.context.router.history.push(data.path);
} else {
console.warn('Unknown message type:', data.type); // eslint-disable-line no-console
}
}
componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false);
@ -131,6 +143,10 @@ export default class UI extends React.PureComponent {
document.addEventListener('dragleave', this.handleDragLeave, false);
document.addEventListener('dragend', this.handleDragEnd, false);
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
}
this.props.dispatch(refreshHomeTimeline());
this.props.dispatch(refreshNotifications());
}

View File

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

View File

@ -5,6 +5,15 @@ export function isMobile(width) {
};
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
let userTouching = false;
window.addEventListener('touchstart', () => {
userTouching = true;
}, { once: true });
export function isUserTouching() {
return userTouching;
}
export function isIOS() {
return iOS;

View File

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

View File

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

View File

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

View File

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

View File

@ -61,6 +61,14 @@
"defaultMessage": "Hide settings",
"id": "column_header.hide_settings"
},
{
"defaultMessage": "Move column to the left",
"id": "column_header.moveLeft_settings"
},
{
"defaultMessage": "Move column to the right",
"id": "column_header.moveRight_settings"
},
{
"defaultMessage": "Unpin",
"id": "column_header.unpin"
@ -366,6 +374,10 @@
"defaultMessage": "Report @{name}",
"id": "account.report"
},
{
"defaultMessage": "Share @{name}'s profile",
"id": "account.share"
},
{
"defaultMessage": "Media",
"id": "account.media"
@ -1019,6 +1031,10 @@
{
"defaultMessage": "Report @{name}",
"id": "status.report"
},
{
"defaultMessage": "Share",
"id": "status.share"
}
],
"path": "app/javascript/mastodon/features/status/components/action_bar.json"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"account.block": "Blocar @{name}",
"account.block_domain": "Tot amagar del domeni {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incompletas.",
"account.edit_profile": "Modificar lo perfil",
"account.follow": "Sègre",
"account.followers": "Seguidors",
@ -13,18 +13,19 @@
"account.posts": "Estatuts",
"account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada",
"account.share": "Partejar lo perfil a @{name}",
"account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}",
"account.unfollow": "Quitar de sègre",
"account.unmute": "Quitar de rescondre @{name}",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "Veire lo perfil complet",
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
"bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament daqueste compausant.",
"bundle_column_error.retry": "Tornar ensejar",
"bundle_column_error.retry": "Tornar ensajar",
"bundle_column_error.title": "Error de ret",
"bundle_modal_error.close": "Tampar",
"bundle_modal_error.message": "Quicòm a fach meuca pendent lo cargament daqueste compausant.",
"bundle_modal_error.retry": "Tornar ensejar",
"bundle_modal_error.message": "Quicòm a fach mèuca pendent lo cargament daqueste compausant.",
"bundle_modal_error.retry": "Tornar ensajar",
"column.blocks": "Personas blocadas",
"column.community": "Flux public local",
"column.favourites": "Favorits",
@ -34,9 +35,11 @@
"column.notifications": "Notificacions",
"column.public": "Flux public global",
"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.show_settings": "Show settings",
"column_header.show_settings": "Mostrar los paramètres",
"column_header.unpin": "Despenjar",
"column_subheading.navigation": "Navigacion",
"column_subheading.settings": "Paramètres",
@ -48,35 +51,35 @@
"compose_form.publish_loud": "{publish} !",
"compose_form.sensitive": "Marcar lo mèdia coma sensible",
"compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
"compose_form.spoiler_placeholder": "Avertiment",
"compose_form.spoiler_placeholder": "Escrivètz lavertiment aquí",
"confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar",
"confirmations.block.message": "Sètz segur de voler blocar {name} ?",
"confirmations.delete.confirm": "Suprimir",
"confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.mute.confirm": "Metre en silenci",
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
"confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
"emoji_button.activity": "Activitat",
"emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar",
"emoji_button.label": "Inserir un emoji",
"emoji_button.nature": "Natura",
"emoji_button.objects": "Objèctes",
"emoji_button.people": "Gents",
"emoji_button.search": "Cercar...",
"emoji_button.search": "Cercar",
"emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo flux public local es void. Escribètz quicòm per lo garnir !",
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
"empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
"empty_column.home": "Pel moment segètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a 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.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.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.reject": "Regetar",
"getting_started.appsshort": "Apps",
@ -105,11 +108,11 @@
"navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global",
"notification.favourite": "{name} a ajustat a sos favorits :",
"notification.follow": "{name} vos sèc.",
"notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat :",
"notification.reblog": "{name} a partejat vòstre estatut :",
"notifications.clear": "Levar",
"notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?",
"notifications.clear": "Escafar",
"notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
"notifications.column_settings.alert": "Notificacions localas",
"notifications.column_settings.favourite": "Favorits :",
"notifications.column_settings.follow": "Nòus seguidors :",
@ -121,15 +124,15 @@
"notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach",
"onboarding.next": "Seguent",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra intància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_four.home": "Lo flux 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.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon !",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"onboarding.page_six.almost_done": "Gaireben acabat…",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.appetoot": "Bon Appetut!",
"onboarding.page_six.apps_available": "I a 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.guidelines": "guida de la comunitat",
@ -170,7 +173,7 @@
"status.report": "Senhalar @{name}",
"status.sensitive_toggle": "Clicar per mostrar",
"status.sensitive_warning": "Contengut sensible",
"status.share": "Share",
"status.share": "Partejar",
"status.show_less": "Tornar plegar",
"status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -53,6 +53,7 @@ class Web::PushSubscription < ApplicationRecord
url: url,
actions: actions,
access_token: access_token,
message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker
}
),
endpoint: endpoint,
@ -117,7 +118,7 @@ class Web::PushSubscription < ApplicationRecord
when :mention then [
{
title: translate('push_notifications.mention.action_favourite'),
icon: full_asset_url('emoji/2764.png', skip_pipeline: true),
icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true),
todo: 'request',
method: 'POST',
action: "/api/v1/statuses/#{notification.target_status.id}/favourite",
@ -130,11 +131,11 @@ class Web::PushSubscription < ApplicationRecord
can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden?
if should_hide
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('emoji/1f441.png'), todo: 'expand', action: 'expand')
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), todo: 'expand', action: 'expand')
end
if can_boost
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('web-push-icon_boost.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
end
actions

View File

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

View File

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

View File

@ -3,7 +3,7 @@
class Pubsubhubbub::SubscribeWorker
include Sidekiq::Worker
sidekiq_options queue: 'push', retry: 10, unique: :until_executed
sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false
sidekiq_retry_in do |count|
case count
@ -18,6 +18,12 @@ class Pubsubhubbub::SubscribeWorker
end
end
sidekiq_retries_exhausted do |msg, _e|
account = Account.find(msg['args'].first)
logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing"
::UnsubscribeService.new.call(account)
end
def perform(account_id)
account = Account.find(account_id)
logger.debug "PuSH re-subscribing to #{account.acct}"

View File

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

View File

@ -1,7 +1,7 @@
run.config:
engine: ruby
engine.config:
runtime: ruby-2.4.1
runtime: ruby-2.4
extra_packages:
# basic servers:
@ -20,6 +20,9 @@ run.config:
# for node-gyp, used in the asset compilation process:
- python-2
# i18n:
- libidn
cache_dirs:
- node_modules
@ -35,10 +38,6 @@ run.config:
extra_steps:
- 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
fs_watch: true

View File

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

View File

@ -1,19 +1,38 @@
---
oc:
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
closed_registrations: Las inscripcions son clavadas pel moment sus aquesta instància.
contact: Contacte
contact_missing: Pas parametrat
contact_unavailable: Pas disponible
description_headline: Qué es %{domain} ?
domain_count_after: autras instàncias
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
status_count_after: estatuts
status_count_before: quan escrich
user_count_after: personas
user_count_before: Ostal de
what_is_mastodon: Ques Mastodon ?
accounts:
follow: Sègre
followers: Seguidors
@ -23,6 +42,7 @@ oc:
people_who_follow: Lo mond que sègon %{name}
posts: Estatuts
remote_follow: Sègre a distància
reserved_username: Aqueste nom dutilizaire es reservat
unfollow: Quitar de sègre
admin:
accounts:
@ -60,8 +80,10 @@ oc:
profile_url: URL del perfil
public: Public
push_subscription_expires: Fin de labonament PuSH
redownload: Actualizar los avatars
reset: Reïnicializar
reset_password: Reïnicializar lo senhal
resubscribe: Se tornar abonar
salmon_url: URL Salmon
search: Cercar
show:
@ -70,13 +92,14 @@ oc:
targeted_reports: Rapòrts faches tocant aqueste compte
silence: Silenci
statuses: Estatuts
subscribe: Sabonar
title: Comptes
undo_silenced: Levar lo silenci
undo_suspension: Levar la suspension
username: Nom dutilizaire
web: Web
domain_blocks:
add_new: Ajustar un nòu
add_new: Najustar un nòu
created_msg: Domeni blocat es a èsser tractat
destroyed_msg: Lo blocatge del domeni es estat levat
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.
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."
noop: Cap
silence: Silenci
suspend: Suspendre
title: Nòu blocatge domeni
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
severities:
noop: Cap
silence: Silenci
suspend: Suspendre
severity: Severitat
@ -110,6 +135,7 @@ oc:
domain_name: Domeni
title: Instàncias conegudas
reports:
action_taken_by: Accion menada per
are_you_sure: Es segur ?
comment:
label: Comentari
@ -147,7 +173,7 @@ oc:
desc_html: Autorizar lo monde a se marcar
title: Inscripcions
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
site_description_extended:
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
topic: Subjècte
title: Administracion
admin_mailer:
new_report:
body: "%{reporter} a senhalat %{target}"
subject: Novèl senhalament per %{instance} (#%{id})
application_mailer:
settings: 'Cambiar las preferéncias de corrièl : %{link}'
signature: Notificacion de Mastodon sus %{instance}
@ -190,7 +220,8 @@ oc:
applications:
invalid_url: LURL donada es invalida
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_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 ?
@ -204,6 +235,12 @@ oc:
authorize_follow:
error: O planhèm, i a agut una error al moment de cercar lo compte
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 :'
title: Sègre %{acct}
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_title: Disponibilitat del contengut difusat
errors:
'403': Avètz pas lautorizacion de veire aquesta pagina.
'404': La pagina que recercatz existís pas.
'410': La pagina que cercatz existís pas mai.
'422':
content: Verificacion de seguretat fracassada. Blocatz los cookies ?
title: Verificacion de seguretat fracassada
'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:
blocks: Personas que blocatz
csv: CSV
@ -327,7 +366,7 @@ oc:
following: Lista de mond que seguètz
muting: Lista de mond que volètz pas legir
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>.
media_attachments:
validations:
@ -362,6 +401,23 @@ oc:
next: Seguent
prev: Precedent
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:
acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire
missing_resource: URL de redireccion pas trobada
@ -438,7 +494,7 @@ oc:
<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>
@ -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>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>
@ -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."
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 :'
recovery_codes: Salvar los còdis de recuperacion
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.
setup: Paramètres

View File

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

View File

@ -267,6 +267,21 @@ ru:
next: След
prev: Пред
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:
acct: Введите username@domain, откуда Вы хотите подписаться
missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей
@ -335,6 +350,8 @@ ru:
click_to_show: Показать
reblogged: продвинул(а)
sensitive_content: Чувствительный контент
terms:
title: "Условия обслуживания и политика конфиденциальности %{instance}"
time:
formats:
default: "%b %d, %Y, %H:%M"

View File

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

View File

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

View File

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

View File

@ -10,7 +10,11 @@ const { publicPath } = require('./configuration.js');
const path = require('path');
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
stats: 'normal',
@ -48,7 +52,7 @@ module.exports = merge(sharedConfig, {
ServiceWorker: {
entry: path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'),
cacheName: 'mastodon',
output: '../sw.js',
output: '../assets/sw.js',
publicPath: '/sw.js',
minify: true,
},

View File

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

View File

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

View File

@ -7,9 +7,8 @@
"build:production": "cross-env RAILS_ENV=production ./bin/webpack",
"manage:translations": "node ./config/webpack/translationRunner.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: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",
"postinstall": "npm rebuild node-sass"
},
@ -57,7 +56,7 @@
"glob": "^7.1.1",
"http-link-header": "^0.8.0",
"immutable": "^3.8.1",
"intersection-observer": "^0.3.2",
"intersection-observer": "^0.4.0",
"intl": "^1.2.5",
"intl-relativeformat": "^2.0.0",
"is-nan": "^1.2.1",
@ -113,15 +112,13 @@
"tiny-queue": "^0.2.1",
"uuid": "^3.1.0",
"uws": "^8.14.0",
"webpack": "^3.0.0",
"webpack-bundle-analyzer": "^2.8.2",
"webpack-manifest-plugin": "^1.1.2",
"webpack": "^3.4.1",
"webpack-bundle-analyzer": "^2.8.3",
"webpack-manifest-plugin": "^1.2.1",
"webpack-merge": "^4.1.0",
"websocket.js": "^0.1.12"
},
"devDependencies": {
"@storybook/addon-actions": "^3.1.8",
"@storybook/react": "^3.1.8",
"babel-eslint": "^7.2.3",
"chai": "^4.1.0",
"chai-enzyme": "^0.8.0",
@ -134,7 +131,7 @@
"react-intl-translations-manager": "^5.0.0",
"react-test-renderer": "^15.6.1",
"sinon": "^2.3.7",
"webpack-dev-server": "^2.5.1",
"webpack-dev-server": "^2.6.1",
"yargs": "^8.0.2"
},
"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
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
allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowerAccountsController do
render_views
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
Fabricate(:follow, target_account: user.account)

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowingAccountsController do
render_views
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
Fabricate(:follow, account: user.account)

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::RelationshipsController do
render_views
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
allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::Accounts::SearchController, type: :controller do
render_views
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
allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ describe Api::V1::Accounts::StatusesController do
render_views
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
allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
render_views
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
allow(controller).to receive(:doorkeeper_token) { token }

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
render_views
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
Fabricate(:block, account: user.account)

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
render_views
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
user.account.block_domain!('example.com')

View File

@ -3,19 +3,77 @@ require 'rails_helper'
RSpec.describe Api::V1::FavouritesController, type: :controller do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { double acceptable?: true, resource_owner_id: user.id }
before do
Fabricate(:favourite, account: user.account)
allow(controller).to receive(:doorkeeper_token) { token }
end
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
describe 'GET #index' do
it 'returns http success' do
get :index, params: { limit: 1 }
context 'without token' do
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

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
render_views
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') }
before do

View File

@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
render_views
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
allow(controller).to receive(:doorkeeper_token) { token }

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