Merge upstream (#111)

main
kibigo! 7 years ago
commit 8150689b48

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

@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt #SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer #SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true #SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files. # Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system # PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system

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

1
.gitignore vendored

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

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

@ -27,6 +27,7 @@ Metrics/AbcSize:
Max: 100 Max: 100
Metrics/BlockLength: Metrics/BlockLength:
Max: 35
Exclude: Exclude:
- 'lib/tasks/**/*' - 'lib/tasks/**/*'
@ -35,10 +36,10 @@ Metrics/BlockNesting:
Metrics/ClassLength: Metrics/ClassLength:
CountComments: false CountComments: false
Max: 200 Max: 300
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 15 Max: 25
Metrics/LineLength: Metrics/LineLength:
AllowURI: true AllowURI: true
@ -53,11 +54,11 @@ Metrics/ModuleLength:
Max: 200 Max: 200
Metrics/ParameterLists: Metrics/ParameterLists:
Max: 4 Max: 5
CountKeywordArgs: true CountKeywordArgs: true
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 10 Max: 20
Rails: Rails:
Enabled: true Enabled: true

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

@ -1,7 +1,9 @@
protobuf-compiler
libprotobuf-dev
ffmpeg ffmpeg
libxdamage1
libxfixes3
libicu-dev libicu-dev
libidn11
libidn11-dev libidn11-dev
libpq-dev
libprotobuf-dev
libxdamage1
libxfixes3
protobuf-compiler

@ -8,4 +8,3 @@ So here's the deal: we all work on this code, and then it runs on dev.glitch.soc
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/). - You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/). - And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).

@ -2,7 +2,7 @@
"name": "Mastodon", "name": "Mastodon",
"description": "A GNU Social-compatible microblogging server", "description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon", "repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/javascript/images/logo.svg", "logo": "https://github.com/tootsuite.png",
"env": { "env": {
"HEROKU": { "HEROKU": {
"description": "Leave this as true", "description": "Leave this as true",

@ -18,7 +18,7 @@ module Settings
end end
def destroy def destroy
if current_user.validate_and_consume_otp!(confirmation_params[:code]) if acceptable_code?
current_user.otp_required_for_login = false current_user.otp_required_for_login = false
current_user.save! current_user.save!
redirect_to settings_two_factor_authentication_path redirect_to settings_two_factor_authentication_path
@ -38,5 +38,10 @@ module Settings
def verify_otp_required def verify_otp_required
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login? redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
end end
def acceptable_code?
current_user.validate_and_consume_otp!(confirmation_params[:code]) ||
current_user.invalidate_otp_backup_code!(confirmation_params[:code])
end
end end
end end

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

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

@ -19,10 +19,10 @@ export default class ColumnBackButton extends React.PureComponent {
render () { render () {
return ( return (
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'> <button onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' /> <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div> </button>
); );
} }

@ -8,6 +8,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container'; import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
}); });
@ -19,11 +23,13 @@ export default class ColumnHeader extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired,
title: PropTypes.node.isRequired, title: PropTypes.node.isRequired,
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
active: PropTypes.bool, active: PropTypes.bool,
localSettings : ImmutablePropTypes.map, localSettings : ImmutablePropTypes.map,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
focusable: PropTypes.bool,
showBackButton: PropTypes.bool, showBackButton: PropTypes.bool,
notifCleaning: PropTypes.bool, // true only for the notification column notifCleaning: PropTypes.bool, // true only for the notification column
notifCleaningActive: PropTypes.bool, notifCleaningActive: PropTypes.bool,
@ -36,6 +42,10 @@ export default class ColumnHeader extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
static defaultProps = {
focusable: true,
}
state = { state = {
collapsed: true, collapsed: true,
animating: false, animating: false,
@ -82,10 +92,9 @@ export default class ColumnHeader extends React.PureComponent {
} }
render () { render () {
const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, notifCleaningActive } = this.props; const { intl, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
const { collapsed, animating, animatingNCD } = this.state; const { collapsed, animating, animatingNCD } = this.state;
let title = this.props.title; let title = this.props.title;
const wrapperClassName = classNames('column-header__wrapper', { const wrapperClassName = classNames('column-header__wrapper', {
@ -132,8 +141,8 @@ export default class ColumnHeader extends React.PureComponent {
moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <div key='move-buttons' className='column-header__setting-arrows'>
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
</div> </div>
); );
} else if (multiColumn) { } else if (multiColumn) {
@ -159,12 +168,12 @@ export default class ColumnHeader extends React.PureComponent {
} }
if (children || multiColumn) { if (children || multiColumn) {
collapseButton = <button className={collapsibleButtonClassName} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
} }
return ( return (
<div className={wrapperClassName}> <div className={wrapperClassName}>
<div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}> <h1 tabIndex={focusable && '0'} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} /> <i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title} {title}
<div className='column-header__buttons'> <div className='column-header__buttons'>
@ -181,7 +190,7 @@ export default class ColumnHeader extends React.PureComponent {
) : null} ) : null}
{collapseButton} {collapseButton}
</div> </div>
</div> </h1>
{ notifCleaning ? ( { notifCleaning ? (
<div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}> <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
@ -191,7 +200,7 @@ export default class ColumnHeader extends React.PureComponent {
</div> </div>
) : null} ) : null}
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}> <div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'> <div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent} {(!collapsed || animating) && collapsedContent}
</div> </div>

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

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

@ -215,10 +215,10 @@ export default class MediaGallery extends React.PureComponent {
} }
children = ( children = (
<div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}> <button className='media-spoiler' onClick={this.handleOpen}>
<span className='media-spoiler__warning'>{warning}</span> <span className='media-spoiler__warning'>{warning}</span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div> </button>
); );
} else { } else {
const size = media.take(4).size; const size = media.take(4).size;

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

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

@ -5,7 +5,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IconButton from './icon_button'; import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -154,12 +154,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton} {shareButton}
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>
<DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' /> <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
</div> </div>
</div> </div>
); );

@ -149,7 +149,7 @@ export default class StatusContent extends React.PureComponent {
} }
return ( return (
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <div className={classNames} ref={this.setRef} tabIndex='0' aria-label={status.get('search_index')} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} /> <span dangerouslySetInnerHTML={spoilerContent} />
{' '} {' '}
@ -158,13 +158,15 @@ export default class StatusContent extends React.PureComponent {
{mentionsPlaceholder} {mentionsPlaceholder}
<div className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} /> <div tabIndex={!hidden && 0} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
</div> </div>
); );
} else if (this.props.onClick) { } else if (this.props.onClick) {
return ( return (
<div <div
ref={this.setRef} ref={this.setRef}
tabIndex='0'
aria-label={status.get('search_index')}
className={classNames} className={classNames}
style={directionStyle} style={directionStyle}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
@ -175,6 +177,8 @@ export default class StatusContent extends React.PureComponent {
} else { } else {
return ( return (
<div <div
tabIndex='0'
aria-label={status.get('search_index')}
ref={this.setRef} ref={this.setRef}
className='status__content' className='status__content'
style={directionStyle} style={directionStyle}

@ -6,7 +6,7 @@ import StatusContainer from '../../glitch/components/status/container';
import LoadMore from './load_more'; import LoadMore from './load_more';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { debounce } from 'lodash'; import { throttle } from 'lodash';
export default class StatusList extends ImmutablePureComponent { export default class StatusList extends ImmutablePureComponent {
@ -30,13 +30,13 @@ export default class StatusList extends ImmutablePureComponent {
intersectionObserverWrapper = new IntersectionObserverWrapper(); intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = debounce(() => { handleScroll = throttle(() => {
if (this.node) { if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node; const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight; const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop; this._oldScrollPosition = scrollHeight - scrollTop;
if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) { if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom(); this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) { } else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop(); this.props.onScrollToTop();
@ -44,7 +44,7 @@ export default class StatusList extends ImmutablePureComponent {
this.props.onScroll(); this.props.onScroll();
} }
} }
}, 200, { }, 150, {
trailing: true, trailing: true,
}); });
@ -104,6 +104,32 @@ export default class StatusList extends ImmutablePureComponent {
this.props.onScrollToBottom(); this.props.onScrollToBottom();
} }
handleKeyDown = (e) => {
if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
case 'PageUp':
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':
return this.node.querySelector('[role="feed"] > article:first-of-type');
default:
return null;
}
})();
if (article) {
e.preventDefault();
article.focus();
article.scrollIntoView();
}
}
}
render () { render () {
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
@ -113,11 +139,11 @@ export default class StatusList extends ImmutablePureComponent {
if (isLoading || statusIds.size > 0 || !emptyMessage) { if (isLoading || statusIds.size > 0 || !emptyMessage) {
scrollableArea = ( scrollableArea = (
<div className='scrollable' ref={this.setRef}> <div className='scrollable' ref={this.setRef}>
<div className='status-list'> <div role='feed' className='status-list' onKeyDown={this.handleKeyDown}>
{prepend} {prepend}
{statusIds.map((statusId) => { {statusIds.map((statusId, index) => {
return <StatusContainer key={statusId} id={statusId} intersectionObserverWrapper={this.intersectionObserverWrapper} />; return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
})} })}
{loadMore} {loadMore}

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

@ -3,6 +3,8 @@ import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping)); const trie = new Trie(Object.keys(unicodeMapping));
const excluded = ['™', '©', '®'];
function emojify(str) { function emojify(str) {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
// and replacing valid unicode strings // and replacing valid unicode strings
@ -19,7 +21,7 @@ function emojify(str) {
insideTag = true; insideTag = true;
} else if (!insideTag && (match = trie.search(str.substring(i)))) { } else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match; const unicodeStr = match;
if (unicodeStr in unicodeMapping) { if (unicodeStr in unicodeMapping && excluded.indexOf(unicodeStr) === -1) {
const [filename, shortCode] = unicodeMapping[unicodeStr]; const [filename, shortCode] = unicodeMapping[unicodeStr];
const alt = unicodeStr; const alt = unicodeStr;
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;

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

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

@ -13,12 +13,12 @@ export default class CharacterCounter extends React.PureComponent {
if (diff < 0) { if (diff < 0) {
return <span className='character-counter character-counter--over'>{diff}</span>; return <span className='character-counter character-counter--over'>{diff}</span>;
} }
return <span className='character-counter'>{diff}</span>; return <span className='character-counter'>{diff}</span>;
} }
render () { render () {
const diff = this.props.max - length(this.props.text); const diff = this.props.max - length(this.props.text);
return this.checkRemainingText(diff); return this.checkRemainingText(diff);
} }

@ -19,6 +19,7 @@ import WarningContainer from '../containers/warning_container';
import { isMobile } from '../../../is_mobile'; import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz'; import { length } from 'stringz';
import { countableText } from '../util/counter';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@ -150,9 +151,9 @@ export default class ComposeForm extends ImmutablePureComponent {
const { intl, onPaste, showSearch } = this.props; const { intl, onPaste, showSearch } = this.props;
const disabled = this.props.is_submitting; const disabled = this.props.is_submitting;
const maybeEye = this.props.advanced_options.get('do_not_federate') ? ' 👁️' : ''; const maybeEye = this.props.advanced_options.get('do_not_federate') ? ' 👁️' : '';
const text = [this.props.spoiler_text, this.props.text, maybeEye].join(''); const text = [this.props.spoiler_text, countableText(this.props.text), maybeEye].join('');
let publishText = ''; let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') { if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
@ -164,7 +165,10 @@ export default class ComposeForm extends ImmutablePureComponent {
<div className='compose-form'> <div className='compose-form'>
<Collapsable isVisible={this.props.spoiler} fullHeight={50}> <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
<div className='spoiler-input'> <div className='spoiler-input'>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' /> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' />
</label>
</div> </div>
</Collapsable> </Collapsable>
@ -206,7 +210,7 @@ export default class ComposeForm extends ImmutablePureComponent {
<div className='compose-form__publish'> <div className='compose-form__publish'>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !==0 && text.trim().length === 0)} block /></div> <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
</div> </div>
</div> </div>
</div> </div>

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

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

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

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

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

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

@ -15,6 +15,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
visible: state.getIn(['compose', 'media_attachments']).size > 0, visible: state.getIn(['compose', 'media_attachments']).size > 0,
active: state.getIn(['compose', 'sensitive']), active: state.getIn(['compose', 'sensitive']),
disabled: state.getIn(['compose', 'spoiler']),
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
@ -30,12 +31,13 @@ class SensitiveButton extends React.PureComponent {
static propTypes = { static propTypes = {
visible: PropTypes.bool, visible: PropTypes.bool,
active: PropTypes.bool, active: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
render () { render () {
const { visible, active, onClick, intl } = this.props; const { visible, active, disabled, onClick, intl } = this.props;
return ( return (
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
@ -53,6 +55,7 @@ class SensitiveButton extends React.PureComponent {
onClick={onClick} onClick={onClick}
size={18} size={18}
active={active} active={active}
disabled={disabled}
style={{ lineHeight: null, height: null }} style={{ lineHeight: null, height: null }}
inverted inverted
/> />

@ -76,23 +76,23 @@ export default class Compose extends React.PureComponent {
if (multiColumn) { if (multiColumn) {
const { columns } = this.props; const { columns } = this.props;
header = ( header = (
<div className='drawer__header'> <nav className='drawer__header'>
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link> <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link>
{!columns.some(column => column.get('id') === 'HOME') && ( {!columns.some(column => column.get('id') === 'HOME') && (
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' aria-label={intl.formatMessage(messages.home_timeline)} /></Link> <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link>
)} )}
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' aria-label={intl.formatMessage(messages.notifications)} /></Link> <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link>
)} )}
{!columns.some(column => column.get('id') === 'COMMUNITY') && ( {!columns.some(column => column.get('id') === 'COMMUNITY') && (
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link> <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link>
)} )}
{!columns.some(column => column.get('id') === 'PUBLIC') && ( {!columns.some(column => column.get('id') === 'PUBLIC') && (
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link> <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link>
)} )}
<a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a> <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)} aria-label={intl.formatMessage(messages.settings)}><i role='img' className='fa fa-fw fa-cogs' /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a> <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a>
</div> </nav>
); );
} }

@ -0,0 +1,7 @@
const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) {
return inputText
.replace(/https?:\/\/\S+/g, urlPlaceholder)
.replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2');
};

@ -36,40 +36,48 @@ export default class ColumnSettings extends React.PureComponent {
<ClearColumnButton onClick={onClear} /> <ClearColumnButton onClick={onClear} />
</div> </div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> <div role='group' aria-labelledby='notifications-follow'>
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} /> <div className='column-settings__row'>
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} /> {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
</div> </div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> <div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
</div> </div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> <div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} /> <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
</div> </div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> <div role='group' aria-labelledby='notifications-reblog'>
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>
</div> </div>
</div> </div>
); );

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

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

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

@ -3,6 +3,7 @@ import ColumnHeader from './column_header';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import scrollTop from '../../../scroll'; import scrollTop from '../../../scroll';
import { isMobile } from '../../../is_mobile';
export default class Column extends React.PureComponent { export default class Column extends React.PureComponent {
@ -37,13 +38,12 @@ export default class Column extends React.PureComponent {
render () { render () {
const { heading, icon, children, active, hideHeadingOnMobile } = this.props; const { heading, icon, children, active, hideHeadingOnMobile } = this.props;
let columnHeaderId = null; const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth)));
let header = '';
if (heading) { const columnHeaderId = showHeading && heading.replace(/ /g, '-');
columnHeaderId = heading.replace(/ /g, '-'); const header = showHeading && (
header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} hideOnMobile={hideHeadingOnMobile} columnHeaderId={columnHeaderId} />; <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} />
} );
return ( return (
<div <div
ref={this.setRef} ref={this.setRef}

@ -8,7 +8,6 @@ export default class ColumnHeader extends React.PureComponent {
type: PropTypes.string, type: PropTypes.string,
active: PropTypes.bool, active: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,
hideOnMobile: PropTypes.bool,
columnHeaderId: PropTypes.string, columnHeaderId: PropTypes.string,
}; };
@ -17,7 +16,7 @@ export default class ColumnHeader extends React.PureComponent {
} }
render () { render () {
const { type, active, hideOnMobile, columnHeaderId } = this.props; const { type, active, columnHeaderId } = this.props;
let icon = ''; let icon = '';
@ -26,7 +25,7 @@ export default class ColumnHeader extends React.PureComponent {
} }
return ( return (
<div role='button heading' tabIndex='0' className={`column-header ${active ? 'active' : ''} ${hideOnMobile ? 'hidden-on-mobile' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> <div role='heading' tabIndex='0' className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}>
{icon} {icon}
{type} {type}
</div> </div>

@ -2,24 +2,24 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Link from 'react-router-dom/Link'; import Link from 'react-router-dom/Link';
const ColumnLink = ({ icon, text, to, onClick, href, method, hideOnMobile }) => { const ColumnLink = ({ icon, text, to, onClick, href, method }) => {
if (href) { if (href) {
return ( return (
<a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}> <a href={href} className='column-link' data-method={method}>
<i className={`fa fa-fw fa-${icon} column-link__icon`} /> <i className={`fa fa-fw fa-${icon} column-link__icon`} />
{text} {text}
</a> </a>
); );
} else if (to) { } else if (to) {
return ( return (
<Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}> <Link to={to} className='column-link'>
<i className={`fa fa-fw fa-${icon} column-link__icon`} /> <i className={`fa fa-fw fa-${icon} column-link__icon`} />
{text} {text}
</Link> </Link>
); );
} else { } else {
return ( return (
<a onClick={onClick} role='button' tabIndex='0' className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}> <a onClick={onClick} role='button' tabIndex='0' data-method={method}>
<i className={`fa fa-fw fa-${icon} column-link__icon`} /> <i className={`fa fa-fw fa-${icon} column-link__icon`} />
{text} {text}
</a> </a>
@ -34,7 +34,6 @@ ColumnLink.propTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
href: PropTypes.string, href: PropTypes.string,
method: PropTypes.string, method: PropTypes.string,
hideOnMobile: PropTypes.bool,
}; };
export default ColumnLink; export default ColumnLink;

@ -6,7 +6,7 @@ import ColumnHeader from '../../../components/column_header';
const ColumnLoading = ({ title = '', icon = ' ' }) => ( const ColumnLoading = ({ title = '', icon = ' ' }) => (
<Column> <Column>
<ColumnHeader icon={icon} title={title} multiColumn={false} /> <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} />
<div className='scrollable' /> <div className='scrollable' />
</Column> </Column>
); );

@ -56,6 +56,15 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleSwipe = (index) => { handleSwipe = (index) => {
this.pendingIndex = index; this.pendingIndex = index;
const nextLinkTranslationId = links[index].props['data-preview-title-id'];
const currentLinkSelector = '.tabs-bar__link.active';
const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`;
// HACK: Remove the active class from the current link and set it to the next one
// React-router does this for us, but too late, feeling laggy.
document.querySelector(currentLinkSelector).classList.remove('active');
document.querySelector(nextLinkSelector).classList.add('active');
} }
handleAnimationEnd = () => { handleAnimationEnd = () => {

@ -10,6 +10,8 @@ import ImageLoader from './image_loader';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
}); });
@injectIntl @injectIntl
@ -66,16 +68,10 @@ export default class MediaModal extends ImmutablePureComponent {
const index = this.getIndex(); const index = this.getIndex();
let leftNav, rightNav, content; const leftNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>;
const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>;
leftNav = rightNav = content = ''; const content = media.map((image) => {
if (media.size > 1) {
leftNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
rightNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
}
content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null; const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null; const height = image.getIn(['meta', 'original', 'height']) || null;

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

@ -1,16 +1,19 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import NavLink from 'react-router-dom/NavLink'; import NavLink from 'react-router-dom/NavLink';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile';
export const links = [ export const links = [
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
<NavLink className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link secondary' activeClassName='active' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
<NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='tabs_bar.federated_timeline' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>, <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>,
]; ];
export function getIndex (path) { export function getIndex (path) {
@ -21,13 +24,60 @@ export function getLink (index) {
return links[index].props.to; return links[index].props.to;
} }
@injectIntl
export default class TabsBar extends React.Component { export default class TabsBar extends React.Component {
static contextTypes = {
router: PropTypes.object.isRequired,
}
static propTypes = {
intl: PropTypes.object.isRequired,
}
setRef = ref => {
this.node = ref;
}
handleClick = (e) => {
// Only apply optimization for touch devices, which we assume are slower
// We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
if (isUserTouching()) {
e.preventDefault();
e.persist();
requestAnimationFrame(() => {
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
const currentTab = tabs.find(tab => tab.classList.contains('active'));
const nextTab = tabs.find(tab => tab.contains(e.target));
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
if (currentTab !== nextTab) {
if (currentTab) {
currentTab.classList.remove('active');
}
const listener = debounce(() => {
nextTab.removeEventListener('transitionend', listener);
this.context.router.history.push(to);
}, 50);
nextTab.addEventListener('transitionend', listener);
nextTab.classList.add('active');
}
});
}
}
render () { render () {
const { intl: { formatMessage } } = this.props;
return ( return (
<div className='tabs-bar'> <nav className='tabs-bar' ref={this.setRef}>
{React.Children.toArray(links)} {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
</div> </nav>
); );
} }

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

@ -110,9 +110,9 @@ export function SettingsModal () {
// IF MASTODON EVER CHANGES DETAILED STATUSES TO REQUIRE THEM, WE'LL NEED TO UPDATE THE URLS OR SOMETHING LOL. // // IF MASTODON EVER CHANGES DETAILED STATUSES TO REQUIRE THEM, WE'LL NEED TO UPDATE THE URLS OR SOMETHING LOL. //
export function MediaGallery () { export function MediaGallery () {
return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery'); return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
} }
export function VideoPlayer () { export function VideoPlayer () {
return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player'); return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
} }

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

@ -24,6 +24,8 @@ function loadPolyfills() {
// This avoids shipping them all the polyfills. // This avoids shipping them all the polyfills.
const needsExtraPolyfills = !( const needsExtraPolyfills = !(
window.IntersectionObserver && window.IntersectionObserver &&
window.IntersectionObserverEntry &&
'isIntersecting' in IntersectionObserverEntry.prototype &&
window.requestIdleCallback && window.requestIdleCallback &&
'object-fit' in (new Image()).style 'object-fit' in (new Image()).style
); );

@ -13,6 +13,7 @@
"account.posts": "المشاركات", "account.posts": "المشاركات",
"account.report": "أبلغ عن @{name}", "account.report": "أبلغ عن @{name}",
"account.requested": "في انتظار الموافقة", "account.requested": "في انتظار الموافقة",
"account.share": "Share @{name}'s profile",
"account.unblock": "إلغاء الحظر عن @{name}", "account.unblock": "إلغاء الحظر عن @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "إلغاء المتابعة", "account.unfollow": "إلغاء المتابعة",
@ -34,7 +35,11 @@
"column.notifications": "الإشعارات", "column.notifications": "الإشعارات",
"column.public": "الخيط العام الموحد", "column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "التصفح", "column_subheading.navigation": "التصفح",
"column_subheading.settings": "الإعدادات", "column_subheading.settings": "الإعدادات",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "عرض الردود", "home.column_settings.show_replies": "عرض الردود",
"home.settings": "إعدادات العمود", "home.settings": "إعدادات العمود",
"lightbox.close": "إغلاق", "lightbox.close": "إغلاق",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "تحميل ...", "loading_indicator.label": "تحميل ...",
"media_gallery.toggle_visible": "عرض / إخفاء", "media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "تعذر العثور عليه", "missing_indicator.label": "تعذر العثور عليه",
@ -168,6 +175,7 @@
"status.report": "إبلِغ عن @{name}", "status.report": "إبلِغ عن @{name}",
"status.sensitive_toggle": "اضغط للعرض", "status.sensitive_toggle": "اضغط للعرض",
"status.sensitive_warning": "محتوى حساس", "status.sensitive_warning": "محتوى حساس",
"status.share": "Share",
"status.show_less": "إعرض أقلّ", "status.show_less": "إعرض أقلّ",
"status.show_more": "أظهر المزيد", "status.show_more": "أظهر المزيد",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Публикации", "account.posts": "Публикации",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "В очакване на одобрение", "account.requested": "В очакване на одобрение",
"account.share": "Share @{name}'s profile",
"account.unblock": "Не блокирай", "account.unblock": "Не блокирай",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Не следвай", "account.unfollow": "Не следвай",
@ -34,7 +35,11 @@
"column.notifications": "Известия", "column.notifications": "Известия",
"column.public": "Публичен канал", "column.public": "Публичен канал",
"column_back_button.label": "Назад", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings", "home.settings": "Column settings",
"lightbox.close": "Затвори", "lightbox.close": "Затвори",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Зареждане...", "loading_indicator.label": "Зареждане...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
@ -168,6 +175,7 @@
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Покажи", "status.sensitive_toggle": "Покажи",
"status.sensitive_warning": "Деликатно съдържание", "status.sensitive_warning": "Деликатно съдържание",
"status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Publicacions", "account.posts": "Publicacions",
"account.report": "Informe @{name}", "account.report": "Informe @{name}",
"account.requested": "Esperant aprovació", "account.requested": "Esperant aprovació",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desbloquejar @{name}", "account.unblock": "Desbloquejar @{name}",
"account.unblock_domain": "Mostra {domain}", "account.unblock_domain": "Mostra {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
@ -34,7 +35,11 @@
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.public": "Línia de temps federada", "column.public": "Línia de temps federada",
"column_back_button.label": "Enrere", "column_back_button.label": "Enrere",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navegació", "column_subheading.navigation": "Navegació",
"column_subheading.settings": "Configuració", "column_subheading.settings": "Configuració",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar respostes", "home.column_settings.show_replies": "Mostrar respostes",
"home.settings": "Ajustos de columna", "home.settings": "Ajustos de columna",
"lightbox.close": "Tancar", "lightbox.close": "Tancar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregant...", "loading_indicator.label": "Carregant...",
"media_gallery.toggle_visible": "Alternar visibilitat", "media_gallery.toggle_visible": "Alternar visibilitat",
"missing_indicator.label": "No trobat", "missing_indicator.label": "No trobat",
@ -168,6 +175,7 @@
"status.report": "Informar sobre @{name}", "status.report": "Informar sobre @{name}",
"status.sensitive_toggle": "Clic per veure", "status.sensitive_toggle": "Clic per veure",
"status.sensitive_warning": "Contingut sensible", "status.sensitive_warning": "Contingut sensible",
"status.share": "Share",
"status.show_less": "Mostra menys", "status.show_less": "Mostra menys",
"status.show_more": "Mostra més", "status.show_more": "Mostra més",
"status.unmute_conversation": "Activar conversació", "status.unmute_conversation": "Activar conversació",

@ -13,6 +13,7 @@
"account.posts": "Beiträge", "account.posts": "Beiträge",
"account.report": "@{name} melden", "account.report": "@{name} melden",
"account.requested": "Warte auf Erlaubnis", "account.requested": "Warte auf Erlaubnis",
"account.share": "Share @{name}'s profile",
"account.unblock": "@{name} entblocken", "account.unblock": "@{name} entblocken",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Entfolgen", "account.unfollow": "Entfolgen",
@ -34,7 +35,11 @@
"column.notifications": "Mitteilungen", "column.notifications": "Mitteilungen",
"column.public": "Gesamtes bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Antworten anzeigen", "home.column_settings.show_replies": "Antworten anzeigen",
"home.settings": "Spalteneinstellungen", "home.settings": "Spalteneinstellungen",
"lightbox.close": "Schließen", "lightbox.close": "Schließen",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Lade…", "loading_indicator.label": "Lade…",
"media_gallery.toggle_visible": "Sichtbarkeit einstellen", "media_gallery.toggle_visible": "Sichtbarkeit einstellen",
"missing_indicator.label": "Nicht gefunden", "missing_indicator.label": "Nicht gefunden",
@ -168,6 +175,7 @@
"status.report": "@{name} melden", "status.report": "@{name} melden",
"status.sensitive_toggle": "Klicke, um sie zu sehen", "status.sensitive_toggle": "Klicke, um sie zu sehen",
"status.sensitive_warning": "Heikle Inhalte", "status.sensitive_warning": "Heikle Inhalte",
"status.share": "Share",
"status.show_less": "Weniger anzeigen", "status.show_less": "Weniger anzeigen",
"status.show_more": "Mehr anzeigen", "status.show_more": "Mehr anzeigen",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -53,6 +53,22 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Show settings",
"id": "column_header.show_settings"
},
{
"defaultMessage": "Hide settings",
"id": "column_header.hide_settings"
},
{
"defaultMessage": "Move column to the left",
"id": "column_header.moveLeft_settings"
},
{
"defaultMessage": "Move column to the right",
"id": "column_header.moveRight_settings"
},
{ {
"defaultMessage": "Unpin", "defaultMessage": "Unpin",
"id": "column_header.unpin" "id": "column_header.unpin"
@ -138,6 +154,10 @@
"defaultMessage": "Reply", "defaultMessage": "Reply",
"id": "status.reply" "id": "status.reply"
}, },
{
"defaultMessage": "Share",
"id": "status.share"
},
{ {
"defaultMessage": "Reply to thread", "defaultMessage": "Reply to thread",
"id": "status.replyAll" "id": "status.replyAll"
@ -354,6 +374,10 @@
"defaultMessage": "Report @{name}", "defaultMessage": "Report @{name}",
"id": "account.report" "id": "account.report"
}, },
{
"defaultMessage": "Share @{name}'s profile",
"id": "account.share"
},
{ {
"defaultMessage": "Media", "defaultMessage": "Media",
"id": "account.media" "id": "account.media"
@ -1007,6 +1031,10 @@
{ {
"defaultMessage": "Report @{name}", "defaultMessage": "Report @{name}",
"id": "status.report" "id": "status.report"
},
{
"defaultMessage": "Share",
"id": "status.share"
} }
], ],
"path": "app/javascript/mastodon/features/status/components/action_bar.json" "path": "app/javascript/mastodon/features/status/components/action_bar.json"
@ -1085,6 +1113,14 @@
{ {
"defaultMessage": "Close", "defaultMessage": "Close",
"id": "lightbox.close" "id": "lightbox.close"
},
{
"defaultMessage": "Previous",
"id": "lightbox.previous"
},
{
"defaultMessage": "Next",
"id": "lightbox.next"
} }
], ],
"path": "app/javascript/mastodon/features/ui/components/media_modal.json" "path": "app/javascript/mastodon/features/ui/components/media_modal.json"

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Awaiting approval", "account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Unblock @{name}", "account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow", "account.unfollow": "Unfollow",
@ -34,7 +35,11 @@
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings", "home.settings": "Column settings",
"lightbox.close": "Close", "lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
@ -168,6 +175,7 @@
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Click to view", "status.sensitive_toggle": "Click to view",
"status.sensitive_warning": "Sensitive content", "status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Mesaĝoj", "account.posts": "Mesaĝoj",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Atendas aprobon", "account.requested": "Atendas aprobon",
"account.share": "Share @{name}'s profile",
"account.unblock": "Malbloki @{name}", "account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Malsekvi", "account.unfollow": "Malsekvi",
@ -34,7 +35,11 @@
"column.notifications": "Sciigoj", "column.notifications": "Sciigoj",
"column.public": "Fratara tempolinio", "column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni", "column_back_button.label": "Reveni",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings", "home.settings": "Column settings",
"lightbox.close": "Fermi", "lightbox.close": "Fermi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ŝarĝanta...", "loading_indicator.label": "Ŝarĝanta...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
@ -168,6 +175,7 @@
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Alklaki por vidi", "status.sensitive_toggle": "Alklaki por vidi",
"status.sensitive_warning": "Tikla enhavo", "status.sensitive_warning": "Tikla enhavo",
"status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Publicaciones", "account.posts": "Publicaciones",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Esperando aprobación", "account.requested": "Esperando aprobación",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desbloquear", "account.unblock": "Desbloquear",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Dejar de seguir", "account.unfollow": "Dejar de seguir",
@ -34,7 +35,11 @@
"column.notifications": "Notificaciones", "column.notifications": "Notificaciones",
"column.public": "Historia federada", "column.public": "Historia federada",
"column_back_button.label": "Atrás", "column_back_button.label": "Atrás",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings", "home.settings": "Column settings",
"lightbox.close": "Cerrar", "lightbox.close": "Cerrar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Cargando...", "loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
@ -168,6 +175,7 @@
"status.report": "Reportar", "status.report": "Reportar",
"status.sensitive_toggle": "Click para ver", "status.sensitive_toggle": "Click para ver",
"status.sensitive_warning": "Contenido sensible", "status.sensitive_warning": "Contenido sensible",
"status.share": "Share",
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar más", "status.show_more": "Mostrar más",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,18 +13,19 @@
"account.posts": "نوشته‌ها", "account.posts": "نوشته‌ها",
"account.report": "گزارش @{name}", "account.report": "گزارش @{name}",
"account.requested": "در انتظار پذیرش", "account.requested": "در انتظار پذیرش",
"account.share": "Share @{name}'s profile",
"account.unblock": "رفع انسداد @{name}", "account.unblock": "رفع انسداد @{name}",
"account.unblock_domain": "رفع پنهان‌سازی از {domain}", "account.unblock_domain": "رفع پنهان‌سازی از {domain}",
"account.unfollow": "پایان پیگیری", "account.unfollow": "پایان پیگیری",
"account.unmute": "باصدا کردن @{name}", "account.unmute": "باصدا کردن @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "نمایش نمایهٔ کامل",
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "تلاش دوباره",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "خطای شبکه",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "بستن",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "هنگام بازکردن این بخش خطایی رخ داد.",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "تلاش دوباره",
"column.blocks": "کاربران مسدودشده", "column.blocks": "کاربران مسدودشده",
"column.community": "نوشته‌های محلی", "column.community": "نوشته‌های محلی",
"column.favourites": "پسندیده‌ها", "column.favourites": "پسندیده‌ها",
@ -34,8 +35,12 @@
"column.notifications": "اعلان‌ها", "column.notifications": "اعلان‌ها",
"column.public": "نوشته‌های همه‌جا", "column.public": "نوشته‌های همه‌جا",
"column_back_button.label": "بازگشت", "column_back_button.label": "بازگشت",
"column_header.pin": "Pin", "column_header.hide_settings": "نهفتن تنظیمات",
"column_header.unpin": "Unpin", "column_header.moveLeft_settings": "انتقال ستون به چپ",
"column_header.moveRight_settings": "انتقال ستون به راست",
"column_header.pin": "ثابت‌کردن",
"column_header.show_settings": "نمایش تنظیمات",
"column_header.unpin": "رهاکردن",
"column_subheading.navigation": "گشت و گذار", "column_subheading.navigation": "گشت و گذار",
"column_subheading.settings": "تنظیمات", "column_subheading.settings": "تنظیمات",
"compose_form.lock_disclaimer": "حساب شما {locked} نیست. هر کسی می‌تواند پیگیر شما شود و نوشته‌های ویژهٔ پیگیران شما را ببیند.", "compose_form.lock_disclaimer": "حساب شما {locked} نیست. هر کسی می‌تواند پیگیر شما شود و نوشته‌های ویژهٔ پیگیران شما را ببیند.",
@ -56,8 +61,8 @@
"confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.", "confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.",
"confirmations.mute.confirm": "بی‌صدا کن", "confirmations.mute.confirm": "بی‌صدا کن",
"confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟", "confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "لغو پیگیری",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
"emoji_button.activity": "فعالیت", "emoji_button.activity": "فعالیت",
"emoji_button.flags": "پرچم‌ها", "emoji_button.flags": "پرچم‌ها",
"emoji_button.food": "غذا و نوشیدنی", "emoji_button.food": "غذا و نوشیدنی",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "نمایش پاسخ‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها",
"home.settings": "تنظیمات ستون", "home.settings": "تنظیمات ستون",
"lightbox.close": "بستن", "lightbox.close": "بستن",
"lightbox.next": "بعدی",
"lightbox.previous": "قبلی",
"loading_indicator.label": "بارگیری...", "loading_indicator.label": "بارگیری...",
"media_gallery.toggle_visible": "تغییر پیدایی", "media_gallery.toggle_visible": "تغییر پیدایی",
"missing_indicator.label": "پیدا نشد", "missing_indicator.label": "پیدا نشد",
@ -112,8 +119,8 @@
"notifications.column_settings.favourite": "پسندیده‌ها:", "notifications.column_settings.favourite": "پسندیده‌ها:",
"notifications.column_settings.follow": "پیگیران تازه:", "notifications.column_settings.follow": "پیگیران تازه:",
"notifications.column_settings.mention": "نام‌بردن‌ها:", "notifications.column_settings.mention": "نام‌بردن‌ها:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "اعلان‌ها از سمت سرور",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "این دستگاه",
"notifications.column_settings.reblog": "بازبوق‌ها:", "notifications.column_settings.reblog": "بازبوق‌ها:",
"notifications.column_settings.show": "نمایش در ستون", "notifications.column_settings.show": "نمایش در ستون",
"notifications.column_settings.sound": "پخش صدا", "notifications.column_settings.sound": "پخش صدا",
@ -152,7 +159,7 @@
"report.target": "گزارش‌دادن", "report.target": "گزارش‌دادن",
"search.placeholder": "جستجو", "search.placeholder": "جستجو",
"search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "نگاهی به کاربران این سرور...",
"status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید", "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
"status.delete": "پاک‌کردن", "status.delete": "پاک‌کردن",
"status.favourite": "پسندیدن", "status.favourite": "پسندیدن",
@ -168,6 +175,7 @@
"status.report": "گزارش دادن @{name}", "status.report": "گزارش دادن @{name}",
"status.sensitive_toggle": "برای دیدن کلیک کنید", "status.sensitive_toggle": "برای دیدن کلیک کنید",
"status.sensitive_warning": "محتوای حساس", "status.sensitive_warning": "محتوای حساس",
"status.share": "هم‌رسانی",
"status.show_less": "نهفتن", "status.show_less": "نهفتن",
"status.show_more": "نمایش", "status.show_more": "نمایش",
"status.unmute_conversation": "باصداکردن گفتگو", "status.unmute_conversation": "باصداکردن گفتگو",

@ -13,6 +13,7 @@
"account.posts": "Postit", "account.posts": "Postit",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Odottaa hyväksyntää", "account.requested": "Odottaa hyväksyntää",
"account.share": "Share @{name}'s profile",
"account.unblock": "Salli @{name}", "account.unblock": "Salli @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Lopeta seuraaminen", "account.unfollow": "Lopeta seuraaminen",
@ -34,7 +35,11 @@
"column.notifications": "Ilmoitukset", "column.notifications": "Ilmoitukset",
"column.public": "Yleinen aikajana", "column.public": "Yleinen aikajana",
"column_back_button.label": "Takaisin", "column_back_button.label": "Takaisin",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings", "home.settings": "Column settings",
"lightbox.close": "Sulje", "lightbox.close": "Sulje",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ladataan...", "loading_indicator.label": "Ladataan...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
@ -168,6 +175,7 @@
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Klikkaa nähdäksesi", "status.sensitive_toggle": "Klikkaa nähdäksesi",
"status.sensitive_warning": "Arkaluontoista sisältöä", "status.sensitive_warning": "Arkaluontoista sisältöä",
"status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,11 +13,12 @@
"account.posts": "Statuts", "account.posts": "Statuts",
"account.report": "Signaler", "account.report": "Signaler",
"account.requested": "Invitation envoyée", "account.requested": "Invitation envoyée",
"account.share": "Share @{name}'s profile",
"account.unblock": "Débloquer", "account.unblock": "Débloquer",
"account.unblock_domain": "Ne plus masquer {domain}", "account.unblock_domain": "Ne plus masquer {domain}",
"account.unfollow": "Ne plus suivre", "account.unfollow": "Ne plus suivre",
"account.unmute": "Ne plus masquer", "account.unmute": "Ne plus masquer",
"account.view_full_profile": "Afficher le profil complet", "account.view_full_profile": "Afficher le profil complet",
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
"bundle_column_error.body": "Une erreur s'est produite lors du chargement de ce composant.", "bundle_column_error.body": "Une erreur s'est produite lors du chargement de ce composant.",
"bundle_column_error.retry": "Réessayer", "bundle_column_error.retry": "Réessayer",
@ -34,7 +35,11 @@
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Fil public global", "column.public": "Fil public global",
"column_back_button.label": "Retour", "column_back_button.label": "Retour",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Épingler", "column_header.pin": "Épingler",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Retirer", "column_header.unpin": "Retirer",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Paramètres", "column_subheading.settings": "Paramètres",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Afficher les réponses", "home.column_settings.show_replies": "Afficher les réponses",
"home.settings": "Paramètres de la colonne", "home.settings": "Paramètres de la colonne",
"lightbox.close": "Fermer", "lightbox.close": "Fermer",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Chargement…", "loading_indicator.label": "Chargement…",
"media_gallery.toggle_visible": "Modifier la visibilité", "media_gallery.toggle_visible": "Modifier la visibilité",
"missing_indicator.label": "Non trouvé", "missing_indicator.label": "Non trouvé",
@ -168,6 +175,7 @@
"status.report": "Signaler @{name}", "status.report": "Signaler @{name}",
"status.sensitive_toggle": "Cliquer pour afficher", "status.sensitive_toggle": "Cliquer pour afficher",
"status.sensitive_warning": "Contenu sensible", "status.sensitive_warning": "Contenu sensible",
"status.share": "Share",
"status.show_less": "Replier", "status.show_less": "Replier",
"status.show_more": "Déplier", "status.show_more": "Déplier",
"status.unmute_conversation": "Ne plus masquer la conversation", "status.unmute_conversation": "Ne plus masquer la conversation",

@ -13,6 +13,7 @@
"account.posts": "הודעות", "account.posts": "הודעות",
"account.report": "לדווח על @{name}", "account.report": "לדווח על @{name}",
"account.requested": "בהמתנה לאישור", "account.requested": "בהמתנה לאישור",
"account.share": "Share @{name}'s profile",
"account.unblock": "הסרת חסימה מעל @{name}", "account.unblock": "הסרת חסימה מעל @{name}",
"account.unblock_domain": "הסר חסימה מקהילת {domain}", "account.unblock_domain": "הסר חסימה מקהילת {domain}",
"account.unfollow": "הפסקת מעקב", "account.unfollow": "הפסקת מעקב",
@ -34,7 +35,11 @@
"column.notifications": "התראות", "column.notifications": "התראות",
"column.public": "בפרהסיה", "column.public": "בפרהסיה",
"column_back_button.label": "חזרה", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "ניווט", "column_subheading.navigation": "ניווט",
"column_subheading.settings": "אפשרויות", "column_subheading.settings": "אפשרויות",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "הצגת תגובות", "home.column_settings.show_replies": "הצגת תגובות",
"home.settings": "הגדרות טור", "home.settings": "הגדרות טור",
"lightbox.close": "סגירה", "lightbox.close": "סגירה",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "טוען...", "loading_indicator.label": "טוען...",
"media_gallery.toggle_visible": "נראה\\בלתי נראה", "media_gallery.toggle_visible": "נראה\\בלתי נראה",
"missing_indicator.label": "לא נמצא", "missing_indicator.label": "לא נמצא",
@ -168,6 +175,7 @@
"status.report": "דיווח על @{name}", "status.report": "דיווח על @{name}",
"status.sensitive_toggle": "לחצו כדי לראות", "status.sensitive_toggle": "לחצו כדי לראות",
"status.sensitive_warning": "תוכן רגיש", "status.sensitive_warning": "תוכן רגיש",
"status.share": "Share",
"status.show_less": "הראה פחות", "status.show_less": "הראה פחות",
"status.show_more": "הראה יותר", "status.show_more": "הראה יותר",
"status.unmute_conversation": "הסרת השתקת שיחה", "status.unmute_conversation": "הסרת השתקת שיחה",

@ -13,6 +13,7 @@
"account.posts": "Postovi", "account.posts": "Postovi",
"account.report": "Prijavi @{name}", "account.report": "Prijavi @{name}",
"account.requested": "Čeka pristanak", "account.requested": "Čeka pristanak",
"account.share": "Share @{name}'s profile",
"account.unblock": "Deblokiraj @{name}", "account.unblock": "Deblokiraj @{name}",
"account.unblock_domain": "Otkrij {domain}", "account.unblock_domain": "Otkrij {domain}",
"account.unfollow": "Prestani slijediti", "account.unfollow": "Prestani slijediti",
@ -34,7 +35,11 @@
"column.notifications": "Notifikacije", "column.notifications": "Notifikacije",
"column.public": "Federalni timeline", "column.public": "Federalni timeline",
"column_back_button.label": "Natrag", "column_back_button.label": "Natrag",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigacija", "column_subheading.navigation": "Navigacija",
"column_subheading.settings": "Postavke", "column_subheading.settings": "Postavke",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Pokaži odgovore", "home.column_settings.show_replies": "Pokaži odgovore",
"home.settings": "Postavke Stupca", "home.settings": "Postavke Stupca",
"lightbox.close": "Zatvori", "lightbox.close": "Zatvori",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Učitavam...", "loading_indicator.label": "Učitavam...",
"media_gallery.toggle_visible": "Preklopi vidljivost", "media_gallery.toggle_visible": "Preklopi vidljivost",
"missing_indicator.label": "Nije nađen", "missing_indicator.label": "Nije nađen",
@ -168,6 +175,7 @@
"status.report": "Prijavi @{name}", "status.report": "Prijavi @{name}",
"status.sensitive_toggle": "Klikni da bi vidio", "status.sensitive_toggle": "Klikni da bi vidio",
"status.sensitive_warning": "Osjetljiv sadržaj", "status.sensitive_warning": "Osjetljiv sadržaj",
"status.share": "Share",
"status.show_less": "Pokaži manje", "status.show_less": "Pokaži manje",
"status.show_more": "Pokaži više", "status.show_more": "Pokaži više",
"status.unmute_conversation": "Poništi utišavanje razgovora", "status.unmute_conversation": "Poništi utišavanje razgovora",

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Awaiting approval", "account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Blokkolás levétele", "account.unblock": "Blokkolás levétele",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Követés abbahagyása", "account.unfollow": "Követés abbahagyása",
@ -34,7 +35,11 @@
"column.notifications": "Értesítések", "column.notifications": "Értesítések",
"column.public": "Nyilvános", "column.public": "Nyilvános",
"column_back_button.label": "Vissza", "column_back_button.label": "Vissza",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings", "home.settings": "Column settings",
"lightbox.close": "Bezárás", "lightbox.close": "Bezárás",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Betöltés...", "loading_indicator.label": "Betöltés...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
@ -168,6 +175,7 @@
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Katt a megtekintéshez", "status.sensitive_toggle": "Katt a megtekintéshez",
"status.sensitive_warning": "Érzékeny tartalom", "status.sensitive_warning": "Érzékeny tartalom",
"status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Postingan", "account.posts": "Postingan",
"account.report": "Laporkan @{name}", "account.report": "Laporkan @{name}",
"account.requested": "Menunggu persetujuan", "account.requested": "Menunggu persetujuan",
"account.share": "Share @{name}'s profile",
"account.unblock": "Hapus blokir @{name}", "account.unblock": "Hapus blokir @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Berhenti mengikuti", "account.unfollow": "Berhenti mengikuti",
@ -34,7 +35,11 @@
"column.notifications": "Notifikasi", "column.notifications": "Notifikasi",
"column.public": "Linimasa gabunggan", "column.public": "Linimasa gabunggan",
"column_back_button.label": "Kembali", "column_back_button.label": "Kembali",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigasi", "column_subheading.navigation": "Navigasi",
"column_subheading.settings": "Pengaturan", "column_subheading.settings": "Pengaturan",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Tampilkan balasan", "home.column_settings.show_replies": "Tampilkan balasan",
"home.settings": "Pengaturan kolom", "home.settings": "Pengaturan kolom",
"lightbox.close": "Tutup", "lightbox.close": "Tutup",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Tunggu sebentar...", "loading_indicator.label": "Tunggu sebentar...",
"media_gallery.toggle_visible": "Tampil/Sembunyikan", "media_gallery.toggle_visible": "Tampil/Sembunyikan",
"missing_indicator.label": "Tidak ditemukan", "missing_indicator.label": "Tidak ditemukan",
@ -168,6 +175,7 @@
"status.report": "Laporkan @{name}", "status.report": "Laporkan @{name}",
"status.sensitive_toggle": "Klik untuk menampilkan", "status.sensitive_toggle": "Klik untuk menampilkan",
"status.sensitive_warning": "Konten sensitif", "status.sensitive_warning": "Konten sensitif",
"status.share": "Share",
"status.show_less": "Tampilkan lebih sedikit", "status.show_less": "Tampilkan lebih sedikit",
"status.show_more": "Tampilkan semua", "status.show_more": "Tampilkan semua",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Mesaji", "account.posts": "Mesaji",
"account.report": "Denuncar @{name}", "account.report": "Denuncar @{name}",
"account.requested": "Vartante aprobo", "account.requested": "Vartante aprobo",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desblokusar @{name}", "account.unblock": "Desblokusar @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Ne plus sequar", "account.unfollow": "Ne plus sequar",
@ -34,7 +35,11 @@
"column.notifications": "Savigi", "column.notifications": "Savigi",
"column.public": "Federata tempolineo", "column.public": "Federata tempolineo",
"column_back_button.label": "Retro", "column_back_button.label": "Retro",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Montrar respondi", "home.column_settings.show_replies": "Montrar respondi",
"home.settings": "Aranji di la kolumno", "home.settings": "Aranji di la kolumno",
"lightbox.close": "Klozar", "lightbox.close": "Klozar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Kargante...", "loading_indicator.label": "Kargante...",
"media_gallery.toggle_visible": "Chanjar videbleso", "media_gallery.toggle_visible": "Chanjar videbleso",
"missing_indicator.label": "Ne trovita", "missing_indicator.label": "Ne trovita",
@ -168,6 +175,7 @@
"status.report": "Denuncar @{name}", "status.report": "Denuncar @{name}",
"status.sensitive_toggle": "Kliktar por vidar", "status.sensitive_toggle": "Kliktar por vidar",
"status.sensitive_warning": "Trubliva kontenajo", "status.sensitive_warning": "Trubliva kontenajo",
"status.share": "Share",
"status.show_less": "Montrar mine", "status.show_less": "Montrar mine",
"status.show_more": "Montrar plue", "status.show_more": "Montrar plue",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Segnala @{name}", "account.report": "Segnala @{name}",
"account.requested": "In attesa di approvazione", "account.requested": "In attesa di approvazione",
"account.share": "Share @{name}'s profile",
"account.unblock": "Sblocca @{name}", "account.unblock": "Sblocca @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Non seguire", "account.unfollow": "Non seguire",
@ -34,7 +35,11 @@
"column.notifications": "Notifiche", "column.notifications": "Notifiche",
"column.public": "Timeline federata", "column.public": "Timeline federata",
"column_back_button.label": "Indietro", "column_back_button.label": "Indietro",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostra risposte", "home.column_settings.show_replies": "Mostra risposte",
"home.settings": "Impostazioni colonna", "home.settings": "Impostazioni colonna",
"lightbox.close": "Chiudi", "lightbox.close": "Chiudi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carico...", "loading_indicator.label": "Carico...",
"media_gallery.toggle_visible": "Imposta visibilità", "media_gallery.toggle_visible": "Imposta visibilità",
"missing_indicator.label": "Non trovato", "missing_indicator.label": "Non trovato",
@ -168,6 +175,7 @@
"status.report": "Segnala @{name}", "status.report": "Segnala @{name}",
"status.sensitive_toggle": "Clicca per vedere", "status.sensitive_toggle": "Clicca per vedere",
"status.sensitive_warning": "Materiale sensibile", "status.sensitive_warning": "Materiale sensibile",
"status.share": "Share",
"status.show_less": "Mostra meno", "status.show_less": "Mostra meno",
"status.show_more": "Mostra di più", "status.show_more": "Mostra di più",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -1,7 +1,7 @@
{ {
"account.block": "ブロック", "account.block": "ブロック",
"account.block_domain": "{domain}全体を非表示", "account.block_domain": "{domain}全体を非表示",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "以下の情報は不正確な可能性があります。",
"account.edit_profile": "プロフィールを編集", "account.edit_profile": "プロフィールを編集",
"account.follow": "フォロー", "account.follow": "フォロー",
"account.followers": "フォロワー", "account.followers": "フォロワー",
@ -13,11 +13,12 @@
"account.posts": "投稿", "account.posts": "投稿",
"account.report": "通報", "account.report": "通報",
"account.requested": "承認待ち", "account.requested": "承認待ち",
"account.share": "@{name} のプロフィールを共有する",
"account.unblock": "ブロック解除", "account.unblock": "ブロック解除",
"account.unblock_domain": "{domain}を表示", "account.unblock_domain": "{domain}を表示",
"account.unfollow": "フォロー解除", "account.unfollow": "フォロー解除",
"account.unmute": "ミュート解除", "account.unmute": "ミュート解除",
"account.view_full_profile": "View full profile", "account.view_full_profile": "全ての情報を見る",
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。", "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。", "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
"bundle_column_error.retry": "再試行", "bundle_column_error.retry": "再試行",
@ -34,7 +35,11 @@
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "連合タイムライン", "column.public": "連合タイムライン",
"column_back_button.label": "戻る", "column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する",
"column_header.moveRight_settings": "カラムを右に移動する",
"column_header.pin": "ピン留めする", "column_header.pin": "ピン留めする",
"column_header.show_settings": "設定を表示",
"column_header.unpin": "ピン留めを外す", "column_header.unpin": "ピン留めを外す",
"column_subheading.navigation": "ナビゲーション", "column_subheading.navigation": "ナビゲーション",
"column_subheading.settings": "設定", "column_subheading.settings": "設定",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "返信表示", "home.column_settings.show_replies": "返信表示",
"home.settings": "カラム設定", "home.settings": "カラム設定",
"lightbox.close": "閉じる", "lightbox.close": "閉じる",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "読み込み中...", "loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え", "media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません", "missing_indicator.label": "見つかりません",
@ -149,7 +156,7 @@
"reply_indicator.cancel": "キャンセル", "reply_indicator.cancel": "キャンセル",
"report.placeholder": "コメント", "report.placeholder": "コメント",
"report.submit": "通報する", "report.submit": "通報する",
"report.target": "問題のユーザー", "report.target": "{target} を通報する",
"search.placeholder": "検索", "search.placeholder": "検索",
"search_results.total": "{count, number}件の結果", "search_results.total": "{count, number}件の結果",
"standalone.public_title": "連合タイムライン", "standalone.public_title": "連合タイムライン",
@ -168,6 +175,7 @@
"status.report": "通報", "status.report": "通報",
"status.sensitive_toggle": "クリックして表示", "status.sensitive_toggle": "クリックして表示",
"status.sensitive_warning": "閲覧注意", "status.sensitive_warning": "閲覧注意",
"status.share": "共有",
"status.show_less": "隠す", "status.show_less": "隠す",
"status.show_more": "もっと見る", "status.show_more": "もっと見る",
"status.unmute_conversation": "会話のミュートを解除", "status.unmute_conversation": "会話のミュートを解除",

@ -13,6 +13,7 @@
"account.posts": "포스트", "account.posts": "포스트",
"account.report": "신고", "account.report": "신고",
"account.requested": "승인 대기 중", "account.requested": "승인 대기 중",
"account.share": "Share @{name}'s profile",
"account.unblock": "차단 해제", "account.unblock": "차단 해제",
"account.unblock_domain": "{domain} 숨김 해제", "account.unblock_domain": "{domain} 숨김 해제",
"account.unfollow": "팔로우 해제", "account.unfollow": "팔로우 해제",
@ -34,7 +35,11 @@
"column.notifications": "알림", "column.notifications": "알림",
"column.public": "연합 타임라인", "column.public": "연합 타임라인",
"column_back_button.label": "돌아가기", "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.pin": "고정하기",
"column_header.show_settings": "Show settings",
"column_header.unpin": "고정 해제", "column_header.unpin": "고정 해제",
"column_subheading.navigation": "내비게이션", "column_subheading.navigation": "내비게이션",
"column_subheading.settings": "설정", "column_subheading.settings": "설정",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "답글 표시", "home.column_settings.show_replies": "답글 표시",
"home.settings": "컬럼 설정", "home.settings": "컬럼 설정",
"lightbox.close": "닫기", "lightbox.close": "닫기",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "불러오는 중...", "loading_indicator.label": "불러오는 중...",
"media_gallery.toggle_visible": "표시 전환", "media_gallery.toggle_visible": "표시 전환",
"missing_indicator.label": "찾을 수 없습니다", "missing_indicator.label": "찾을 수 없습니다",
@ -168,6 +175,7 @@
"status.report": "신고", "status.report": "신고",
"status.sensitive_toggle": "클릭해서 표시하기", "status.sensitive_toggle": "클릭해서 표시하기",
"status.sensitive_warning": "민감한 미디어", "status.sensitive_warning": "민감한 미디어",
"status.share": "Share",
"status.show_less": "숨기기", "status.show_less": "숨기기",
"status.show_more": "더 보기", "status.show_more": "더 보기",
"status.unmute_conversation": "이 대화의 뮤트 해제하기", "status.unmute_conversation": "이 대화의 뮤트 해제하기",

@ -13,11 +13,12 @@
"account.posts": "Toots", "account.posts": "Toots",
"account.report": "Rapporteer @{name}", "account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring", "account.requested": "Wacht op goedkeuring",
"account.share": "Profiel van @{name} delen",
"account.unblock": "Deblokkeer @{name}", "account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "{domain} niet meer negeren", "account.unblock_domain": "{domain} niet meer negeren",
"account.unfollow": "Ontvolgen", "account.unfollow": "Ontvolgen",
"account.unmute": "@{name} niet meer negeren", "account.unmute": "@{name} niet meer negeren",
"account.view_full_profile": "Volledig profiel tonen", "account.view_full_profile": "Volledig profiel tonen",
"boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
"bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.", "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
"bundle_column_error.retry": "Opnieuw proberen", "bundle_column_error.retry": "Opnieuw proberen",
@ -34,7 +35,11 @@
"column.notifications": "Meldingen", "column.notifications": "Meldingen",
"column.public": "Globale tijdlijn", "column.public": "Globale tijdlijn",
"column_back_button.label": "terug", "column_back_button.label": "terug",
"column_header.hide_settings": "Instellingen verbergen",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Vastmaken", "column_header.pin": "Vastmaken",
"column_header.show_settings": "Instellingen tonen",
"column_header.unpin": "Losmaken", "column_header.unpin": "Losmaken",
"column_subheading.navigation": "Navigatie", "column_subheading.navigation": "Navigatie",
"column_subheading.settings": "Instellingen", "column_subheading.settings": "Instellingen",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Reacties tonen", "home.column_settings.show_replies": "Reacties tonen",
"home.settings": "Kolom-instellingen", "home.settings": "Kolom-instellingen",
"lightbox.close": "Sluiten", "lightbox.close": "Sluiten",
"lightbox.next": "Volgende",
"lightbox.previous": "Vorige",
"loading_indicator.label": "Laden…", "loading_indicator.label": "Laden…",
"media_gallery.toggle_visible": "Media wel/niet tonen", "media_gallery.toggle_visible": "Media wel/niet tonen",
"missing_indicator.label": "Niet gevonden", "missing_indicator.label": "Niet gevonden",
@ -147,12 +154,12 @@
"privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen",
"privacy.unlisted.short": "Minder openbaar", "privacy.unlisted.short": "Minder openbaar",
"reply_indicator.cancel": "Annuleren", "reply_indicator.cancel": "Annuleren",
"report.heading": "Rapporteren",
"report.placeholder": "Extra opmerkingen", "report.placeholder": "Extra opmerkingen",
"report.submit": "Verzenden", "report.submit": "Verzenden",
"report.target": "Rapporteren van", "report.target": "Rapporteren van",
"search.placeholder": "Zoeken", "search.placeholder": "Zoeken",
"search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
"standalone.public_title": "Een kijkje binnenin...",
"status.cannot_reblog": "Deze toot kan niet geboost worden", "status.cannot_reblog": "Deze toot kan niet geboost worden",
"status.delete": "Verwijderen", "status.delete": "Verwijderen",
"status.favourite": "Favoriet", "status.favourite": "Favoriet",
@ -166,8 +173,9 @@
"status.reply": "Reageren", "status.reply": "Reageren",
"status.replyAll": "Reageer op iedereen", "status.replyAll": "Reageer op iedereen",
"status.report": "Rapporteer @{name}", "status.report": "Rapporteer @{name}",
"status.sensitive_toggle": "Klik om te zien", "status.sensitive_toggle": "Klik om te bekijken",
"status.sensitive_warning": "Gevoelige inhoud", "status.sensitive_warning": "Gevoelige inhoud",
"status.share": "Delen",
"status.show_less": "Minder tonen", "status.show_less": "Minder tonen",
"status.show_more": "Meer tonen", "status.show_more": "Meer tonen",
"status.unmute_conversation": "Conversatie niet meer negeren", "status.unmute_conversation": "Conversatie niet meer negeren",

@ -13,6 +13,7 @@
"account.posts": "Innlegg", "account.posts": "Innlegg",
"account.report": "Rapportér @{name}", "account.report": "Rapportér @{name}",
"account.requested": "Venter på godkjennelse", "account.requested": "Venter på godkjennelse",
"account.share": "Share @{name}'s profile",
"account.unblock": "Avblokker @{name}", "account.unblock": "Avblokker @{name}",
"account.unblock_domain": "Vis {domain}", "account.unblock_domain": "Vis {domain}",
"account.unfollow": "Avfølg", "account.unfollow": "Avfølg",
@ -34,7 +35,11 @@
"column.notifications": "Varsler", "column.notifications": "Varsler",
"column.public": "Felles tidslinje", "column.public": "Felles tidslinje",
"column_back_button.label": "Tilbake", "column_back_button.label": "Tilbake",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigasjon", "column_subheading.navigation": "Navigasjon",
"column_subheading.settings": "Innstillinger", "column_subheading.settings": "Innstillinger",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Vis svar", "home.column_settings.show_replies": "Vis svar",
"home.settings": "Kolonneinnstillinger", "home.settings": "Kolonneinnstillinger",
"lightbox.close": "Lukk", "lightbox.close": "Lukk",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Laster...", "loading_indicator.label": "Laster...",
"media_gallery.toggle_visible": "Veksle synlighet", "media_gallery.toggle_visible": "Veksle synlighet",
"missing_indicator.label": "Ikke funnet", "missing_indicator.label": "Ikke funnet",
@ -168,6 +175,7 @@
"status.report": "Rapporter @{name}", "status.report": "Rapporter @{name}",
"status.sensitive_toggle": "Klikk for å vise", "status.sensitive_toggle": "Klikk for å vise",
"status.sensitive_warning": "Følsomt innhold", "status.sensitive_warning": "Følsomt innhold",
"status.share": "Share",
"status.show_less": "Vis mindre", "status.show_less": "Vis mindre",
"status.show_more": "Vis mer", "status.show_more": "Vis mer",
"status.unmute_conversation": "Ikke demp samtale", "status.unmute_conversation": "Ikke demp samtale",

@ -1,7 +1,7 @@
{ {
"account.block": "Blocar @{name}", "account.block": "Blocar @{name}",
"account.block_domain": "Tot amagar del domeni {domain}", "account.block_domain": "Tot amagar del domeni {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incompletas.",
"account.edit_profile": "Modificar lo perfil", "account.edit_profile": "Modificar lo perfil",
"account.follow": "Sègre", "account.follow": "Sègre",
"account.followers": "Seguidors", "account.followers": "Seguidors",
@ -13,18 +13,19 @@
"account.posts": "Estatuts", "account.posts": "Estatuts",
"account.report": "Senhalar @{name}", "account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada", "account.requested": "Invitacion mandada",
"account.share": "Partejar lo perfil a @{name}",
"account.unblock": "Desblocar @{name}", "account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}", "account.unblock_domain": "Desblocar {domain}",
"account.unfollow": "Quitar de sègre", "account.unfollow": "Quitar de sègre",
"account.unmute": "Quitar de rescondre @{name}", "account.unmute": "Quitar de rescondre @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "Veire lo perfil complet",
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven", "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
"bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament daqueste compausant.", "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament daqueste compausant.",
"bundle_column_error.retry": "Tornar ensejar", "bundle_column_error.retry": "Tornar ensajar",
"bundle_column_error.title": "Error de ret", "bundle_column_error.title": "Error de ret",
"bundle_modal_error.close": "Tampar", "bundle_modal_error.close": "Tampar",
"bundle_modal_error.message": "Quicòm a fach meuca pendent lo cargament daqueste compausant.", "bundle_modal_error.message": "Quicòm a fach mèuca pendent lo cargament daqueste compausant.",
"bundle_modal_error.retry": "Tornar ensejar", "bundle_modal_error.retry": "Tornar ensajar",
"column.blocks": "Personas blocadas", "column.blocks": "Personas blocadas",
"column.community": "Flux public local", "column.community": "Flux public local",
"column.favourites": "Favorits", "column.favourites": "Favorits",
@ -34,7 +35,11 @@
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.public": "Flux public global", "column.public": "Flux public global",
"column_back_button.label": "Tornar", "column_back_button.label": "Tornar",
"column_header.hide_settings": "Amagar los paramètres",
"column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
"column_header.moveRight_settings": "Desplaçar la colomna a man esquèrra",
"column_header.pin": "Penjar", "column_header.pin": "Penjar",
"column_header.show_settings": "Mostrar los paramètres",
"column_header.unpin": "Despenjar", "column_header.unpin": "Despenjar",
"column_subheading.navigation": "Navigacion", "column_subheading.navigation": "Navigacion",
"column_subheading.settings": "Paramètres", "column_subheading.settings": "Paramètres",
@ -46,35 +51,35 @@
"compose_form.publish_loud": "{publish} !", "compose_form.publish_loud": "{publish} !",
"compose_form.sensitive": "Marcar lo mèdia coma sensible", "compose_form.sensitive": "Marcar lo mèdia coma sensible",
"compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment", "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
"compose_form.spoiler_placeholder": "Avertiment", "compose_form.spoiler_placeholder": "Escrivètz lavertiment aquí",
"confirmation_modal.cancel": "Anullar", "confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar", "confirmations.block.confirm": "Blocar",
"confirmations.block.message": "Sètz segur de voler blocar {name} ?", "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
"confirmations.delete.confirm": "Suprimir", "confirmations.delete.confirm": "Suprimir",
"confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?", "confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni", "confirmations.domain_block.confirm": "Amagar tot lo domeni",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.", "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.mute.confirm": "Metre en silenci", "confirmations.mute.confirm": "Metre en silenci",
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
"confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
"emoji_button.activity": "Activitat", "emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus", "emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar", "emoji_button.food": "Beure e manjar",
"emoji_button.label": "Inserir un emoji", "emoji_button.label": "Inserir un emoji",
"emoji_button.nature": "Natura", "emoji_button.nature": "Natura",
"emoji_button.objects": "Objèctes", "emoji_button.objects": "Objèctes",
"emoji_button.people": "Gents", "emoji_button.people": "Gents",
"emoji_button.search": "Cercar...", "emoji_button.search": "Cercar",
"emoji_button.symbols": "Simbòls", "emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs", "emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo flux public local es void. Escribètz quicòm per lo garnir !", "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
"empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag", "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
"empty_column.home": "Pel moment segètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.", "empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.",
"empty_column.home.inactivity": "Vòstra pagina dacuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.", "empty_column.home.inactivity": "Vòstra pagina dacuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
"empty_column.home.public_timeline": "lo flux public", "empty_column.home.public_timeline": "lo flux public",
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.", "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.",
"empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.", "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.",
"follow_request.authorize": "Autorizar", "follow_request.authorize": "Autorizar",
"follow_request.reject": "Regetar", "follow_request.reject": "Regetar",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar las responsas", "home.column_settings.show_replies": "Mostrar las responsas",
"home.settings": "Paramètres de la colomna", "home.settings": "Paramètres de la colomna",
"lightbox.close": "Tampar", "lightbox.close": "Tampar",
"lightbox.next": "Seguent",
"lightbox.previous": "Precedent",
"loading_indicator.label": "Cargament…", "loading_indicator.label": "Cargament…",
"media_gallery.toggle_visible": "Modificar la visibilitat", "media_gallery.toggle_visible": "Modificar la visibilitat",
"missing_indicator.label": "Pas trobat", "missing_indicator.label": "Pas trobat",
@ -103,11 +110,11 @@
"navigation_bar.preferences": "Preferéncias", "navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global", "navigation_bar.public_timeline": "Flux public global",
"notification.favourite": "{name} a ajustat a sos favorits :", "notification.favourite": "{name} a ajustat a sos favorits :",
"notification.follow": "{name} vos sèc.", "notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat :", "notification.mention": "{name} vos a mencionat :",
"notification.reblog": "{name} a partejat vòstre estatut :", "notification.reblog": "{name} a partejat vòstre estatut :",
"notifications.clear": "Levar", "notifications.clear": "Escafar",
"notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?", "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
"notifications.column_settings.alert": "Notificacions localas", "notifications.column_settings.alert": "Notificacions localas",
"notifications.column_settings.favourite": "Favorits :", "notifications.column_settings.favourite": "Favorits :",
"notifications.column_settings.follow": "Nòus seguidors :", "notifications.column_settings.follow": "Nòus seguidors :",
@ -119,15 +126,15 @@
"notifications.column_settings.sound": "Emetre un son", "notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach", "onboarding.done": "Fach",
"onboarding.next": "Seguent", "onboarding.next": "Seguent",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra intància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.", "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.", "onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.",
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun enteragís amb vos", "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos",
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.", "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.",
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}", "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon !", "onboarding.page_one.welcome": "Benvengut a Mastodon !",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.", "onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"onboarding.page_six.almost_done": "Gaireben acabat…", "onboarding.page_six.almost_done": "Gaireben acabat…",
"onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.appetoot": "Bon Appetut!",
"onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.", "onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.",
"onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.", "onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.",
"onboarding.page_six.guidelines": "guida de la comunitat", "onboarding.page_six.guidelines": "guida de la comunitat",
@ -168,6 +175,7 @@
"status.report": "Senhalar @{name}", "status.report": "Senhalar @{name}",
"status.sensitive_toggle": "Clicar per mostrar", "status.sensitive_toggle": "Clicar per mostrar",
"status.sensitive_warning": "Contengut sensible", "status.sensitive_warning": "Contengut sensible",
"status.share": "Partejar",
"status.show_less": "Tornar plegar", "status.show_less": "Tornar plegar",
"status.show_more": "Desplegar", "status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat", "status.unmute_conversation": "Conversacions amb silenci levat",

@ -13,6 +13,7 @@
"account.posts": "Posty", "account.posts": "Posty",
"account.report": "Zgłoś @{name}", "account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba", "account.requested": "Oczekująca prośba",
"account.share": "Udostępnij profil @{name}",
"account.unblock": "Odblokuj @{name}", "account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}", "account.unblock_domain": "Odblokuj domenę {domain}",
"account.unfollow": "Przestań śledzić", "account.unfollow": "Przestań śledzić",
@ -34,7 +35,11 @@
"column.notifications": "Powiadomienia", "column.notifications": "Powiadomienia",
"column.public": "Globalna oś czasu", "column.public": "Globalna oś czasu",
"column_back_button.label": "Wróć", "column_back_button.label": "Wróć",
"column_header.hide_settings": "Ukryj ustawienia",
"column_header.moveLeft_settings": "Przesuń kolumnę w lewo",
"column_header.moveRight_settings": "Przesuń kolumnę w prawo",
"column_header.pin": "Przypnij", "column_header.pin": "Przypnij",
"column_header.show_settings": "Pokaż ustawienia",
"column_header.unpin": "Cofnij przypięcie", "column_header.unpin": "Cofnij przypięcie",
"column_subheading.navigation": "Nawigacja", "column_subheading.navigation": "Nawigacja",
"column_subheading.settings": "Ustawienia", "column_subheading.settings": "Ustawienia",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Pokazuj odpowiedzi", "home.column_settings.show_replies": "Pokazuj odpowiedzi",
"home.settings": "Ustawienia kolumny", "home.settings": "Ustawienia kolumny",
"lightbox.close": "Zamknij", "lightbox.close": "Zamknij",
"lightbox.next": "Następne",
"lightbox.previous": "Poprzednie",
"loading_indicator.label": "Ładowanie...", "loading_indicator.label": "Ładowanie...",
"media_gallery.toggle_visible": "Przełącz widoczność", "media_gallery.toggle_visible": "Przełącz widoczność",
"missing_indicator.label": "Nie znaleziono", "missing_indicator.label": "Nie znaleziono",
@ -168,6 +175,7 @@
"status.report": "Zgłoś @{name}", "status.report": "Zgłoś @{name}",
"status.sensitive_toggle": "Naciśnij aby wyświetlić", "status.sensitive_toggle": "Naciśnij aby wyświetlić",
"status.sensitive_warning": "Wrażliwa zawartość", "status.sensitive_warning": "Wrażliwa zawartość",
"status.share": "Udostępnij",
"status.show_less": "Pokaż mniej", "status.show_less": "Pokaż mniej",
"status.show_more": "Pokaż więcej", "status.show_more": "Pokaż więcej",
"status.unmute_conversation": "Cofnij wyciezenie konwersacji", "status.unmute_conversation": "Cofnij wyciezenie konwersacji",

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Denunciar @{name}", "account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação", "account.requested": "A aguardar aprovação",
"account.share": "Share @{name}'s profile",
"account.unblock": "Não bloquear @{name}", "account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
@ -34,7 +35,11 @@
"column.notifications": "Notificações", "column.notifications": "Notificações",
"column.public": "Global", "column.public": "Global",
"column_back_button.label": "Voltar", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar as respostas", "home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem", "home.settings": "Parâmetros da listagem",
"lightbox.close": "Fechar", "lightbox.close": "Fechar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregando...", "loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar", "media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado", "missing_indicator.label": "Não encontrado",
@ -168,6 +175,7 @@
"status.report": "Denúnciar @{name}", "status.report": "Denúnciar @{name}",
"status.sensitive_toggle": "Clique para ver", "status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível", "status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share",
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais", "status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Denunciar @{name}", "account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação", "account.requested": "A aguardar aprovação",
"account.share": "Share @{name}'s profile",
"account.unblock": "Não bloquear @{name}", "account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
@ -34,7 +35,11 @@
"column.notifications": "Notificações", "column.notifications": "Notificações",
"column.public": "Global", "column.public": "Global",
"column_back_button.label": "Voltar", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar as respostas", "home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem", "home.settings": "Parâmetros da listagem",
"lightbox.close": "Fechar", "lightbox.close": "Fechar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregando...", "loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar", "media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado", "missing_indicator.label": "Não encontrado",
@ -168,6 +175,7 @@
"status.report": "Denúnciar @{name}", "status.report": "Denúnciar @{name}",
"status.sensitive_toggle": "Clique para ver", "status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível", "status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share",
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais", "status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Посты", "account.posts": "Посты",
"account.report": "Пожаловаться", "account.report": "Пожаловаться",
"account.requested": "Ожидает подтверждения", "account.requested": "Ожидает подтверждения",
"account.share": "Share @{name}'s profile",
"account.unblock": "Разблокировать", "account.unblock": "Разблокировать",
"account.unblock_domain": "Разблокировать {domain}", "account.unblock_domain": "Разблокировать {domain}",
"account.unfollow": "Отписаться", "account.unfollow": "Отписаться",
@ -34,7 +35,11 @@
"column.notifications": "Уведомления", "column.notifications": "Уведомления",
"column.public": "Глобальная лента", "column.public": "Глобальная лента",
"column_back_button.label": "Назад", "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.pin": "Закрепить",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Открепить", "column_header.unpin": "Открепить",
"column_subheading.navigation": "Навигация", "column_subheading.navigation": "Навигация",
"column_subheading.settings": "Настройки", "column_subheading.settings": "Настройки",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Показывать ответы", "home.column_settings.show_replies": "Показывать ответы",
"home.settings": "Настройки колонки", "home.settings": "Настройки колонки",
"lightbox.close": "Закрыть", "lightbox.close": "Закрыть",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Загрузка...", "loading_indicator.label": "Загрузка...",
"media_gallery.toggle_visible": "Показать/скрыть", "media_gallery.toggle_visible": "Показать/скрыть",
"missing_indicator.label": "Не найдено", "missing_indicator.label": "Не найдено",
@ -168,6 +175,7 @@
"status.report": "Пожаловаться", "status.report": "Пожаловаться",
"status.sensitive_toggle": "Нажмите для просмотра", "status.sensitive_toggle": "Нажмите для просмотра",
"status.sensitive_warning": "Чувствительный контент", "status.sensitive_warning": "Чувствительный контент",
"status.share": "Share",
"status.show_less": "Свернуть", "status.show_less": "Свернуть",
"status.show_more": "Развернуть", "status.show_more": "Развернуть",
"status.unmute_conversation": "Снять глушение с треда", "status.unmute_conversation": "Снять глушение с треда",

@ -13,6 +13,7 @@
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Awaiting approval", "account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
"account.unblock": "Unblock @{name}", "account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow", "account.unfollow": "Unfollow",
@ -34,7 +35,11 @@
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Settings",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings", "home.settings": "Column settings",
"lightbox.close": "Close", "lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
@ -168,6 +175,7 @@
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_toggle": "Click to view", "status.sensitive_toggle": "Click to view",
"status.sensitive_warning": "Sensitive content", "status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Gönderiler", "account.posts": "Gönderiler",
"account.report": "Rapor et @{name}", "account.report": "Rapor et @{name}",
"account.requested": "Onay bekleniyor", "account.requested": "Onay bekleniyor",
"account.share": "Share @{name}'s profile",
"account.unblock": "Engeli kaldır @{name}", "account.unblock": "Engeli kaldır @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Takipten vazgeç", "account.unfollow": "Takipten vazgeç",
@ -34,7 +35,11 @@
"column.notifications": "Bildirimler", "column.notifications": "Bildirimler",
"column.public": "Federe zaman tüneli", "column.public": "Federe zaman tüneli",
"column_back_button.label": "Geri", "column_back_button.label": "Geri",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin", "column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigasyon", "column_subheading.navigation": "Navigasyon",
"column_subheading.settings": "Ayarlar", "column_subheading.settings": "Ayarlar",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Cevapları göster", "home.column_settings.show_replies": "Cevapları göster",
"home.settings": "Kolon ayarları", "home.settings": "Kolon ayarları",
"lightbox.close": "Kapat", "lightbox.close": "Kapat",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Yükleniyor...", "loading_indicator.label": "Yükleniyor...",
"media_gallery.toggle_visible": "Görünürlüğü değiştir", "media_gallery.toggle_visible": "Görünürlüğü değiştir",
"missing_indicator.label": "Bulunamadı", "missing_indicator.label": "Bulunamadı",
@ -168,6 +175,7 @@
"status.report": "@{name}'i raporla", "status.report": "@{name}'i raporla",
"status.sensitive_toggle": "Görmek için tıklayınız", "status.sensitive_toggle": "Görmek için tıklayınız",
"status.sensitive_warning": "Hassas içerik", "status.sensitive_warning": "Hassas içerik",
"status.share": "Share",
"status.show_less": "Daha azı", "status.show_less": "Daha azı",
"status.show_more": "Daha fazlası", "status.show_more": "Daha fazlası",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "Пости", "account.posts": "Пости",
"account.report": "Поскаржитися", "account.report": "Поскаржитися",
"account.requested": "Очікує підтвердження", "account.requested": "Очікує підтвердження",
"account.share": "Share @{name}'s profile",
"account.unblock": "Розблокувати", "account.unblock": "Розблокувати",
"account.unblock_domain": "Розблокувати {domain}", "account.unblock_domain": "Розблокувати {domain}",
"account.unfollow": "Відписатися", "account.unfollow": "Відписатися",
@ -34,7 +35,11 @@
"column.notifications": "Сповіщення", "column.notifications": "Сповіщення",
"column.public": "Глобальна стрічка", "column.public": "Глобальна стрічка",
"column_back_button.label": "Назад", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Навігація", "column_subheading.navigation": "Навігація",
"column_subheading.settings": "Налаштування", "column_subheading.settings": "Налаштування",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "Показувати відповіді", "home.column_settings.show_replies": "Показувати відповіді",
"home.settings": "Налаштування колонок", "home.settings": "Налаштування колонок",
"lightbox.close": "Закрити", "lightbox.close": "Закрити",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Завантаження...", "loading_indicator.label": "Завантаження...",
"media_gallery.toggle_visible": "Показати/приховати", "media_gallery.toggle_visible": "Показати/приховати",
"missing_indicator.label": "Не знайдено", "missing_indicator.label": "Не знайдено",
@ -168,6 +175,7 @@
"status.report": "Поскаржитися", "status.report": "Поскаржитися",
"status.sensitive_toggle": "Натисніть, щоб подивитися", "status.sensitive_toggle": "Натисніть, щоб подивитися",
"status.sensitive_warning": "Непристойний зміст", "status.sensitive_warning": "Непристойний зміст",
"status.share": "Share",
"status.show_less": "Згорнути", "status.show_less": "Згорнути",
"status.show_more": "Розгорнути", "status.show_more": "Розгорнути",
"status.unmute_conversation": "Зняти глушення з діалогу", "status.unmute_conversation": "Зняти глушення з діалогу",

@ -13,6 +13,7 @@
"account.posts": "嘟文", "account.posts": "嘟文",
"account.report": "举报 @{name}", "account.report": "举报 @{name}",
"account.requested": "等待审批", "account.requested": "等待审批",
"account.share": "Share @{name}'s profile",
"account.unblock": "解除对 @{name} 的屏蔽", "account.unblock": "解除对 @{name} 的屏蔽",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "取消关注", "account.unfollow": "取消关注",
@ -34,7 +35,11 @@
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "跨站公共时间轴", "column.public": "跨站公共时间轴",
"column_back_button.label": "Back", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "导航", "column_subheading.navigation": "导航",
"column_subheading.settings": "设置", "column_subheading.settings": "设置",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "显示回应嘟文", "home.column_settings.show_replies": "显示回应嘟文",
"home.settings": "字段设置", "home.settings": "字段设置",
"lightbox.close": "关闭", "lightbox.close": "关闭",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "加载中……", "loading_indicator.label": "加载中……",
"media_gallery.toggle_visible": "打开或关上", "media_gallery.toggle_visible": "打开或关上",
"missing_indicator.label": "找不到内容", "missing_indicator.label": "找不到内容",
@ -168,6 +175,7 @@
"status.report": "举报 @{name}", "status.report": "举报 @{name}",
"status.sensitive_toggle": "点击显示", "status.sensitive_toggle": "点击显示",
"status.sensitive_warning": "敏感内容", "status.sensitive_warning": "敏感内容",
"status.share": "Share",
"status.show_less": "减少显示", "status.show_less": "减少显示",
"status.show_more": "显示更多", "status.show_more": "显示更多",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "文章", "account.posts": "文章",
"account.report": "舉報 @{name}", "account.report": "舉報 @{name}",
"account.requested": "等候審批", "account.requested": "等候審批",
"account.share": "Share @{name}'s profile",
"account.unblock": "解除對 @{name} 的封鎖", "account.unblock": "解除對 @{name} 的封鎖",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
"account.unfollow": "取消關注", "account.unfollow": "取消關注",
@ -34,7 +35,11 @@
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "跨站時間軸", "column.public": "跨站時間軸",
"column_back_button.label": "返回", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "瀏覽", "column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定", "column_subheading.settings": "設定",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "顯示回應文章", "home.column_settings.show_replies": "顯示回應文章",
"home.settings": "欄位設定", "home.settings": "欄位設定",
"lightbox.close": "關閉", "lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "載入中...", "loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上", "media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容", "missing_indicator.label": "找不到內容",
@ -168,6 +175,7 @@
"status.report": "舉報 @{name}", "status.report": "舉報 @{name}",
"status.sensitive_toggle": "點擊顯示", "status.sensitive_toggle": "點擊顯示",
"status.sensitive_warning": "敏感內容", "status.sensitive_warning": "敏感內容",
"status.share": "Share",
"status.show_less": "減少顯示", "status.show_less": "減少顯示",
"status.show_more": "顯示更多", "status.show_more": "顯示更多",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",

@ -13,6 +13,7 @@
"account.posts": "貼文", "account.posts": "貼文",
"account.report": "檢舉 @{name}", "account.report": "檢舉 @{name}",
"account.requested": "正在等待許可", "account.requested": "正在等待許可",
"account.share": "Share @{name}'s profile",
"account.unblock": "取消封鎖 @{name}", "account.unblock": "取消封鎖 @{name}",
"account.unblock_domain": "不再隱藏 {domain}", "account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注", "account.unfollow": "取消關注",
@ -34,7 +35,11 @@
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "聯盟時間軸", "column.public": "聯盟時間軸",
"column_back_button.label": "上一頁", "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.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "瀏覽", "column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定", "column_subheading.settings": "設定",
@ -89,6 +94,8 @@
"home.column_settings.show_replies": "顯示回應", "home.column_settings.show_replies": "顯示回應",
"home.settings": "欄位設定", "home.settings": "欄位設定",
"lightbox.close": "關閉", "lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "讀取中...", "loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性", "media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到", "missing_indicator.label": "找不到",
@ -168,6 +175,7 @@
"status.report": "通報 @{name}", "status.report": "通報 @{name}",
"status.sensitive_toggle": "點來看", "status.sensitive_toggle": "點來看",
"status.sensitive_warning": "敏感內容", "status.sensitive_warning": "敏感內容",
"status.share": "Share",
"status.show_less": "看少點", "status.show_less": "看少點",
"status.show_more": "看更多", "status.show_more": "看更多",
"status.unmute_conversation": "不消音對話", "status.unmute_conversation": "不消音對話",

@ -96,7 +96,7 @@ function appendMedia(state, media) {
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
if (prevSize === 0 && state.get('default_sensitive')) { if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) {
map.set('sensitive', true); map.set('sensitive', true);
} }
}); });
@ -165,14 +165,22 @@ export default function compose(state = initialState, action) {
state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option]))) state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option])))
.set('idempotencyKey', uuid()); .set('idempotencyKey', uuid());
case COMPOSE_SENSITIVITY_CHANGE: case COMPOSE_SENSITIVITY_CHANGE:
return state return state.withMutations(map => {
.set('sensitive', !state.get('sensitive')) if (!state.get('spoiler')) {
.set('idempotencyKey', uuid()); map.set('sensitive', !state.get('sensitive'));
}
map.set('idempotencyKey', uuid());
});
case COMPOSE_SPOILERNESS_CHANGE: case COMPOSE_SPOILERNESS_CHANGE:
return state.withMutations(map => { return state.withMutations(map => {
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('spoiler', !state.get('spoiler')); map.set('spoiler', !state.get('spoiler'));
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
if (!state.get('sensitive') && state.get('media_attachments').size >= 1) {
map.set('sensitive', true);
}
}); });
case COMPOSE_SPOILER_TEXT_CHANGE: case COMPOSE_SPOILER_TEXT_CHANGE:
return state return state

@ -1,3 +1,45 @@
const MAX_NOTIFICATIONS = 5;
const GROUP_TAG = 'tag';
// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker
const formatGroupTitle = (message, count) => message.replace('%{count}', count);
const notify = options =>
self.registration.getNotifications().then(notifications => {
if (notifications.length === MAX_NOTIFICATIONS) {
// Reached the maximum number of notifications, proceed with grouping
const group = {
title: formatGroupTitle(options.data.message, notifications.length + 1),
body: notifications
.sort((n1, n2) => n1.timestamp < n2.timestamp)
.map(notification => notification.title).join('\n'),
badge: '/badge.png',
icon: '/android-chrome-192x192.png',
tag: GROUP_TAG,
data: {
url: (new URL('/web/notifications', self.location)).href,
count: notifications.length + 1,
message: options.data.message,
},
};
notifications.forEach(notification => notification.close());
return self.registration.showNotification(group.title, group);
} else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) {
// Already grouped, proceed with appending the notification to the group
const group = cloneNotification(notifications[0]);
group.title = formatGroupTitle(group.data.message, group.data.count + 1);
group.body = `${options.title}\n${group.body}`;
group.data = { ...group.data, count: group.data.count + 1 };
return self.registration.showNotification(group.title, group);
}
return self.registration.showNotification(options.title, options);
});
const handlePush = (event) => { const handlePush = (event) => {
const options = event.data.json(); const options = event.data.json();
@ -17,7 +59,7 @@ const handlePush = (event) => {
options.actions = options.data.actions; options.actions = options.data.actions;
} }
event.waitUntil(self.registration.showNotification(options.title, options)); event.waitUntil(notify(options));
}; };
const cloneNotification = (notification) => { const cloneNotification = (notification) => {
@ -50,22 +92,37 @@ const makeRequest = (notification, action) =>
credentials: 'include', credentials: 'include',
}); });
const findBestClient = clients => {
const focusedClient = clients.find(client => client.focused);
const visibleClient = clients.find(client => client.visibilityState === 'visible');
return focusedClient || visibleClient || clients[0];
};
const openUrl = url => const openUrl = url =>
self.clients.matchAll({ type: 'window' }).then(clientList => { self.clients.matchAll({ type: 'window' }).then(clientList => {
if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate if (clientList.length !== 0) {
const webClients = clientList const webClients = clientList.filter(client => /\/web\//.test(client.url));
.filter(client => /\/web\//.test(client.url))
.sort(client => client !== 'visible');
const visibleClient = clientList.find(client => client.visibilityState === 'visible'); if (webClients.length !== 0) {
const focusedClient = clientList.find(client => client.focused); const client = findBestClient(webClients);
const client = webClients[0] || visibleClient || focusedClient || clientList[0]; const { pathname } = new URL(url);
return client.navigate(url).then(client => client.focus()); if (pathname.startsWith('/web/')) {
} else { return client.focus().then(client => client.postMessage({
return self.clients.openWindow(url); type: 'navigate',
path: pathname.slice('/web/'.length - 1),
}));
}
} else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
const client = findBestClient(clientList);
return client.navigate(url).then(client => client.focus());
}
} }
return self.clients.openWindow(url);
}); });
const removeActionFromNotification = (notification, action) => { const removeActionFromNotification = (notification, action) => {

@ -121,7 +121,7 @@
.information-board { .information-board {
background: darken($ui-base-color, 4%); background: darken($ui-base-color, 4%);
padding: 40px 0; padding: 20px 0;
.panel { .panel {
position: absolute; position: absolute;
@ -147,10 +147,15 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
a,
span { span {
font-weight: 400; font-weight: 400;
color: lighten($ui-base-color, 34%); color: lighten($ui-base-color, 34%);
} }
a {
text-decoration: none;
}
} }
} }
@ -162,13 +167,14 @@
.information-board-sections { .information-board-sections {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
} }
.section { .section {
flex: 1 0 0; flex: 1 0 0;
font: 16px/28px 'mastodon-font-sans-serif', sans-serif; font: 16px/28px 'mastodon-font-sans-serif', sans-serif;
text-align: right; text-align: right;
padding: 0 15px; padding: 10px 15px;
span, span,
strong { strong {
@ -190,14 +196,6 @@
color: $primary-text-color; color: $primary-text-color;
} }
} }
@media screen and (max-width: 500px) {
flex-direction: column;
.section {
text-align: left;
}
}
} }
.owner { .owner {
@ -317,6 +315,17 @@
} }
} }
p,
li {
font: inherit;
font-weight: inherit;
margin-bottom: 0;
}
hr {
border-color: rgba($ui-base-lighter-color, .6);
}
.header { .header {
line-height: 30px; line-height: 30px;
overflow: hidden; overflow: hidden;
@ -553,6 +562,7 @@
} }
#mastodon-timeline { #mastodon-timeline {
display: flex;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar; -ms-overflow-style: -ms-autohiding-scrollbar;
font-family: 'mastodon-font-sans-serif', sans-serif; font-family: 'mastodon-font-sans-serif', sans-serif;
@ -567,11 +577,20 @@
overflow: hidden; overflow: hidden;
box-shadow: 0 0 6px rgba($black, 0.1); box-shadow: 0 0 6px rgba($black, 0.1);
.column-header {
color: inherit;
font-family: inherit;
font-size: 16px;
line-height: inherit;
font-weight: inherit;
margin: 0;
padding: 15px;
}
.column { .column {
padding: 0; padding: 0;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
height: 100%;
} }
.scrollable { .scrollable {
@ -652,21 +671,17 @@
} }
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 840px) {
.container { .container {
padding: 0 20px; padding: 0 20px;
} }
.information-board {
padding-bottom: 20px;
}
.information-board .container { .information-board .container {
padding-right: 20px; padding-right: 20px;
.panel { .panel {
position: static; position: static;
margin-top: 30px; margin-top: 20px;
width: 100%; width: 100%;
border-radius: 4px; border-radius: 4px;
@ -694,6 +709,10 @@
@media screen and (max-width: 675px) { @media screen and (max-width: 675px) {
.header-wrapper { .header-wrapper {
padding-top: 0; padding-top: 0;
&.compact .hero .heading {
padding-bottom: 20px;
}
} }
.header .container, .header .container,
@ -707,14 +726,13 @@
} }
.header { .header {
padding-top: 0;
.hero { .hero {
margin-top: 30px; margin-top: 30px;
padding: 0; padding: 0;
.heading { .heading {
padding-bottom: 20px; padding: 0 20px 20px;
} }
} }

@ -149,12 +149,16 @@
color: $ui-base-lighter-color; color: $ui-base-lighter-color;
} }
&.disabled {
color: $ui-primary-color;
}
&.active { &.active {
color: $ui-highlight-color; color: $ui-highlight-color;
}
&.disabled { &.disabled {
color: $ui-primary-color; color: lighten($ui-highlight-color, 13%);
}
} }
} }
@ -215,16 +219,18 @@
} }
.dropdown--active::after { .dropdown--active::after {
content: ""; @media screen and (min-width: 1025px) {
display: block; content: "";
position: absolute; display: block;
width: 0; position: absolute;
height: 0; width: 0;
border-style: solid; height: 0;
border-width: 0 4.5px 7.8px; border-style: solid;
border-color: transparent transparent $ui-secondary-color; border-width: 0 4.5px 7.8px;
bottom: 8px; border-color: transparent transparent $ui-secondary-color;
right: 104px; bottom: 8px;
right: 104px;
}
} }
.invisible { .invisible {
@ -1837,6 +1843,8 @@
cursor: pointer; cursor: pointer;
flex: 0 0 auto; flex: 0 0 auto;
font-size: 16px; font-size: 16px;
border: 0;
text-align: start;
padding: 15px; padding: 15px;
z-index: 3; z-index: 3;
@ -1999,12 +2007,6 @@
&:hover { &:hover {
background: lighten($ui-base-color, 11%); background: lighten($ui-base-color, 11%);
} }
&.hidden-on-mobile {
@include single-column('screen and (max-width: 1024px)') {
display: none;
}
}
} }
.column-link__icon { .column-link__icon {
@ -2388,12 +2390,6 @@ button.icon-button.active i.fa-retweet {
} }
} }
&.hidden-on-mobile {
@include single-column('screen and (max-width: 1024px)') {
display: none;
}
}
&:focus, &:focus,
&:active { &:active {
outline: 0; outline: 0;
@ -2672,6 +2668,8 @@ button.icon-button.active i.fa-retweet {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 0;
width: 100%;
height: 100%; height: 100%;
justify-content: center; justify-content: center;
position: relative; position: relative;
@ -2754,6 +2752,7 @@ button.icon-button.active i.fa-retweet {
align-items: center; align-items: center;
background: rgba($base-overlay-background, 0.5); background: rgba($base-overlay-background, 0.5);
box-sizing: border-box; box-sizing: border-box;
border: 0;
color: $primary-text-color; color: $primary-text-color;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -3848,7 +3847,8 @@ button.icon-button.active i.fa-retweet {
.boost-modal, .boost-modal,
.confirmation-modal, .confirmation-modal,
.report-modal { .report-modal,
.actions-modal {
background: lighten($ui-secondary-color, 8%); background: lighten($ui-secondary-color, 8%);
color: $ui-base-color; color: $ui-base-color;
border-radius: 8px; border-radius: 8px;
@ -3873,6 +3873,15 @@ button.icon-button.active i.fa-retweet {
} }
} }
.actions-modal {
.status {
background: $white;
border-bottom-color: $ui-secondary-color;
padding-top: 10px;
padding-bottom: 10px;
}
}
.boost-modal__container { .boost-modal__container {
overflow-x: scroll; overflow-x: scroll;
padding: 10px; padding: 10px;
@ -3914,7 +3923,7 @@ button.icon-button.active i.fa-retweet {
} }
.confirmation-modal { .confirmation-modal {
max-width: 280px; max-width: 85vw;
@media screen and (min-width: 480px) { @media screen and (min-width: 480px) {
max-width: 380px; max-width: 380px;
@ -3939,6 +3948,47 @@ button.icon-button.active i.fa-retweet {
} }
} }
.actions-modal {
.status {
overflow-y: auto;
max-height: 300px;
}
max-height: 80vh;
max-width: 80vw;
ul {
overflow-y: auto;
flex-shrink: 0;
li:empty {
margin: 0;
}
li:not(:empty) {
a {
color: $ui-base-color;
display: flex;
padding: 10px;
align-items: center;
text-decoration: none;
&.active {
&,
button {
background: $ui-highlight-color;
color: $primary-text-color;
}
}
button:first-child {
margin-right: 10px;
}
}
}
}
}
.confirmation-modal__action-bar { .confirmation-modal__action-bar {
.confirmation-modal__cancel-button { .confirmation-modal__cancel-button {
background-color: transparent; background-color: transparent;

@ -6,7 +6,7 @@ class Emoji
include Singleton include Singleton
def initialize def initialize
data = Oj.load(File.open(File.join(Rails.root, 'lib', 'assets', 'emoji.json'))) data = Oj.load(File.open(Rails.root.join('lib', 'assets', 'emoji.json')))
@map = {} @map = {}
@ -32,7 +32,7 @@ class Emoji
def codepoint_to_unicode(codepoint) def codepoint_to_unicode(codepoint)
if codepoint.include?('-') if codepoint.include?('-')
codepoint.split('-').map(&:hex).pack('U') codepoint.split('-').map(&:hex).pack('U*')
else else
[codepoint.hex].pack('U') [codepoint.hex].pack('U')
end end

@ -8,11 +8,11 @@ module Mastodon
class UnexpectedResponseError < Error class UnexpectedResponseError < Error
def initialize(response = nil) def initialize(response = nil)
@response = response if response.respond_to? :uri
end super("#{response.uri} returned code #{response.code}")
else
def to_s super
"#{@response.uri} returned code #{@response.code}" end
end end
end end
end end

@ -33,9 +33,7 @@ class LanguageDetector
def simplified_text def simplified_text
text.dup.tap do |new_text| text.dup.tap do |new_text|
URI.extract(new_text).each do |url| new_text.gsub!(FetchLinkCardService::URL_PATTERN, '')
new_text.gsub!(url, '')
end
new_text.gsub!(Account::MENTION_RE, '') new_text.gsub!(Account::MENTION_RE, '')
new_text.gsub!(Tag::HASHTAG_RE, '') new_text.gsub!(Tag::HASHTAG_RE, '')
new_text.gsub!(/\s+/, ' ') new_text.gsub!(/\s+/, ' ')

@ -44,7 +44,7 @@
# #
class Account < ApplicationRecord class Account < ApplicationRecord
MENTION_RE = /(?:^|[^\/[:word:]])@([a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
include AccountAvatar include AccountAvatar
include AccountFinderConcern include AccountFinderConcern

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

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

@ -90,7 +90,7 @@ class BatchedRemoveStatusService < BaseService
key = FeedManager.instance.key(:home, follower_id) key = FeedManager.instance.key(:home, follower_id)
originals = statuses.reject(&:reblog?) originals = statuses.reject(&:reblog?)
reblogs = statuses.reject { |s| !s.reblog? } reblogs = statuses.select(&:reblog?)
# Quickly remove all originals # Quickly remove all originals
redis.pipelined do redis.pipelined do

@ -5,6 +5,27 @@ class StatusLengthValidator < ActiveModel::Validator
def validate(status) def validate(status)
return unless status.local? && !status.reblog? return unless status.local? && !status.reblog?
status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if [status.text, status.spoiler_text].join.mb_chars.grapheme_length > MAX_CHARS status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if too_long?(status)
end
private
def too_long?(status)
countable_length(status) > MAX_CHARS
end
def countable_length(status)
total_text(status).mb_chars.grapheme_length
end
def total_text(status)
[status.spoiler_text, countable_text(status)].join
end
def countable_text(status)
status.text.dup.tap do |new_text|
new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23)
new_text.gsub!(Account::MENTION_RE, '@\2')
end
end end
end end

@ -2,7 +2,10 @@
.panel-header .panel-header
= succeed ':' do = succeed ':' do
= t 'about.contact' = t 'about.contact'
%span{ title: contact.site_contact_email.presence }= contact.site_contact_email.presence - if contact.site_contact_email.present?
= mail_to contact.site_contact_email, nil, title: contact.site_contact_email
- else
%span= t 'about.contact_unavailable'
.panel-body .panel-body
- if contact.contact_account - if contact.contact_account
.owner .owner

@ -14,15 +14,13 @@
required: true, required: true,
input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
= f.input :password, = f.input :password,
autocomplete: 'off',
placeholder: t('simple_form.labels.defaults.password'), placeholder: t('simple_form.labels.defaults.password'),
required: true, required: true,
input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
= f.input :password_confirmation, = f.input :password_confirmation,
autocomplete: 'off',
placeholder: t('simple_form.labels.defaults.confirm_password'), placeholder: t('simple_form.labels.defaults.confirm_password'),
required: true, required: true,
input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
.actions .actions
= f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative' = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative'

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

Loading…
Cancel
Save