Merge upstream (#111)

main
kibigo! 7 years ago
commit 8150689b48

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

@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#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.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system

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

1
.gitignore vendored

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

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

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

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

@ -1,7 +1,9 @@
protobuf-compiler
libprotobuf-dev
ffmpeg
libxdamage1
libxfixes3
libicu-dev
libidn11
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/).
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).

@ -2,7 +2,7 @@
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"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": {
"HEROKU": {
"description": "Leave this as true",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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} />
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
<div role='group' aria-labelledby='notifications-follow'>
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
</div>
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div role='group' aria-labelledby='notifications-reblog'>
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>
</div>
</div>
);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save