Merge tag 'v1.5.0rc2' into kosmos
This commit is contained in:
commit
3704f3ccb3
@ -4,7 +4,6 @@ public/system
|
||||
public/assets
|
||||
public/packs
|
||||
node_modules
|
||||
storybook
|
||||
neo4j
|
||||
vendor/bundle
|
||||
.DS_Store
|
||||
|
@ -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
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/
|
||||
|
@ -2,4 +2,3 @@ node_modules/
|
||||
.cache/
|
||||
docs/
|
||||
spec/
|
||||
storybook/
|
||||
|
@ -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) => (
|
||||
|
@ -6,6 +6,8 @@ import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
const messages = defineMessages({
|
||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
@ -101,8 +103,8 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
|
||||
moveButtons = (
|
||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
||||
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
|
||||
<button className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn) {
|
||||
@ -133,7 +135,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<div role='heading' tabIndex={focusable && '0'} className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
|
||||
<h1 tabIndex={focusable && '0'} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
|
||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||
{title}
|
||||
|
||||
@ -141,7 +143,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
{backButton}
|
||||
{collapseButton}
|
||||
</div>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}>
|
||||
<div className='column-header__collapsible-inner'>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@ -9,16 +10,23 @@ export default class DropdownMenu extends React.PureComponent {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
icon: PropTypes.string.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
direction: PropTypes.string,
|
||||
status: ImmutablePropTypes.map,
|
||||
ariaLabel: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
ariaLabel: 'Menu',
|
||||
isModalOpen: false,
|
||||
isUserTouching: () => false,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -34,6 +42,10 @@ export default class DropdownMenu extends React.PureComponent {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const { action, to } = this.props.items[i];
|
||||
|
||||
if (this.props.isModalOpen) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
// Don't call e.preventDefault() when the item uses 'href' property.
|
||||
// ex. "Edit profile" on the account action bar
|
||||
|
||||
@ -48,10 +60,32 @@ export default class DropdownMenu extends React.PureComponent {
|
||||
this.dropdown.hide();
|
||||
}
|
||||
|
||||
handleShow = () => this.setState({ expanded: true })
|
||||
handleShow = () => {
|
||||
if (this.props.isUserTouching()) {
|
||||
this.props.onModalOpen({
|
||||
status: this.props.status,
|
||||
actions: this.props.items,
|
||||
onClick: this.handleClick,
|
||||
});
|
||||
} else {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleHide = () => this.setState({ expanded: false })
|
||||
|
||||
handleToggle = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (this.props.isUserTouching()) {
|
||||
this.handleShow();
|
||||
} else {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
this.setState({ expanded: false });
|
||||
}
|
||||
}
|
||||
|
||||
renderItem = (item, i) => {
|
||||
if (item === null) {
|
||||
return <li key={`sep-${i}`} className='dropdown__sep' />;
|
||||
@ -61,7 +95,7 @@ export default class DropdownMenu extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<li className='dropdown__content-list-item' key={`${text}-${i}`}>
|
||||
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
|
||||
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
@ -71,6 +105,7 @@ export default class DropdownMenu extends React.PureComponent {
|
||||
render () {
|
||||
const { icon, items, size, direction, ariaLabel, disabled } = this.props;
|
||||
const { expanded } = this.state;
|
||||
const isUserTouching = this.props.isUserTouching();
|
||||
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
|
||||
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
|
||||
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
|
||||
@ -84,20 +119,26 @@ export default class DropdownMenu extends React.PureComponent {
|
||||
}
|
||||
|
||||
const dropdownItems = expanded && (
|
||||
<ul className='dropdown__content-list'>
|
||||
<ul role='group' className='dropdown__content-list' onClick={this.handleHide}>
|
||||
{items.map(this.renderItem)}
|
||||
</ul>
|
||||
);
|
||||
|
||||
// No need to render the actual dropdown if we use the modal. If we
|
||||
// don't render anything <Dropdow /> breaks, so we just put an empty div.
|
||||
const dropdownContent = !isUserTouching ? (
|
||||
<DropdownContent className={directionClass} >
|
||||
{dropdownItems}
|
||||
</DropdownContent>
|
||||
) : <div />;
|
||||
|
||||
return (
|
||||
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
|
||||
<DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}>
|
||||
<Dropdown ref={this.setRef} active={isUserTouching ? false : expanded} onShow={this.handleShow} onHide={this.handleHide}>
|
||||
<DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-pressed={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
|
||||
<i className={iconClassname} aria-hidden />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent className={directionClass}>
|
||||
{dropdownItems}
|
||||
</DropdownContent>
|
||||
{dropdownContent}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
@ -12,12 +12,14 @@ export default class IconButton extends React.PureComponent {
|
||||
onClick: PropTypes.func,
|
||||
size: PropTypes.number,
|
||||
active: PropTypes.bool,
|
||||
pressed: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
activeStyle: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
overlay: PropTypes.bool,
|
||||
tabIndex: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -26,6 +28,7 @@ export default class IconButton extends React.PureComponent {
|
||||
disabled: false,
|
||||
animate: false,
|
||||
overlay: false,
|
||||
tabIndex: '0',
|
||||
};
|
||||
|
||||
handleClick = (e) => {
|
||||
@ -73,10 +76,12 @@ export default class IconButton extends React.PureComponent {
|
||||
{({ rotate }) =>
|
||||
<button
|
||||
aria-label={this.props.title}
|
||||
aria-pressed={this.props.pressed}
|
||||
title={this.props.title}
|
||||
className={classes.join(' ')}
|
||||
onClick={this.handleClick}
|
||||
style={style}
|
||||
tabIndex={this.props.tabIndex}
|
||||
>
|
||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
|
||||
</button>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,8 @@ export default class Status extends ImmutablePureComponent {
|
||||
autoPlayGif: PropTypes.bool,
|
||||
muted: PropTypes.bool,
|
||||
intersectionObserverWrapper: PropTypes.object,
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -59,6 +61,7 @@ export default class Status extends ImmutablePureComponent {
|
||||
'boostModal',
|
||||
'autoPlayGif',
|
||||
'muted',
|
||||
'listLength',
|
||||
]
|
||||
|
||||
updateOnStates = ['isExpanded']
|
||||
@ -67,8 +70,8 @@ export default class Status extends ImmutablePureComponent {
|
||||
if (!nextState.isIntersecting && nextState.isHidden) {
|
||||
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
|
||||
// that either "isIntersecting" or "isHidden" matter, and then they're
|
||||
// the only things that matter.
|
||||
return this.state.isIntersecting || !this.state.isHidden;
|
||||
// the only things that matter (and updated ARIA attributes).
|
||||
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
|
||||
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
|
||||
// If we're going from a non-intersecting state to an intersecting state,
|
||||
// (i.e. offscreen to onscreen), then we definitely need to re-render
|
||||
@ -107,17 +110,12 @@ export default class Status extends ImmutablePureComponent {
|
||||
this.height = getRectFromEntry(entry).height;
|
||||
}
|
||||
|
||||
// Edge 15 doesn't support isIntersecting, but we can infer it
|
||||
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
|
||||
// https://github.com/WICG/IntersectionObserver/issues/211
|
||||
const isIntersecting = (typeof entry.isIntersecting === 'boolean') ?
|
||||
entry.isIntersecting : entry.intersectionRect.height > 0;
|
||||
this.setState((prevState) => {
|
||||
if (prevState.isIntersecting && !isIntersecting) {
|
||||
if (prevState.isIntersecting && !entry.isIntersecting) {
|
||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
||||
}
|
||||
return {
|
||||
isIntersecting: isIntersecting,
|
||||
isIntersecting: entry.isIntersecting,
|
||||
isHidden: false,
|
||||
};
|
||||
});
|
||||
@ -174,7 +172,7 @@ export default class Status extends ImmutablePureComponent {
|
||||
|
||||
// Exclude intersectionObserverWrapper from `other` variable
|
||||
// because intersection is managed in here.
|
||||
const { status, account, intersectionObserverWrapper, ...other } = this.props;
|
||||
const { status, account, intersectionObserverWrapper, index, listLength, ...other } = this.props;
|
||||
const { isExpanded, isIntersecting, isHidden } = this.state;
|
||||
|
||||
if (status === null) {
|
||||
@ -183,10 +181,10 @@ export default class Status extends ImmutablePureComponent {
|
||||
|
||||
if (!isIntersecting && isHidden) {
|
||||
return (
|
||||
<div ref={this.handleRef} data-id={status.get('id')} style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
|
||||
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
|
||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||
{status.get('content')}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@ -200,14 +198,14 @@ export default class Status extends ImmutablePureComponent {
|
||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||
|
||||
return (
|
||||
<div className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} >
|
||||
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
|
||||
</div>
|
||||
|
||||
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@ -236,7 +234,7 @@ export default class Status extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
|
||||
<article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex='0' ref={this.handleRef}>
|
||||
<div className='status__info'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
|
||||
@ -254,7 +252,7 @@ export default class Status extends ImmutablePureComponent {
|
||||
{media}
|
||||
|
||||
<StatusActionBar {...this.props} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from './icon_button';
|
||||
import DropdownMenu from './dropdown_menu';
|
||||
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
@ -156,7 +156,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||
{shareButton}
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
|
||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -104,6 +104,32 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
this.props.onScrollToBottom();
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) {
|
||||
const article = (() => {
|
||||
switch (e.key) {
|
||||
case 'PageDown':
|
||||
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].nextElementSibling;
|
||||
case 'PageUp':
|
||||
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].previousElementSibling;
|
||||
case 'End':
|
||||
return this.node.querySelector('[role="feed"] > article:last-of-type');
|
||||
case 'Home':
|
||||
return this.node.querySelector('[role="feed"] > article:first-of-type');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
if (article) {
|
||||
e.preventDefault();
|
||||
article.focus();
|
||||
article.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
|
||||
|
||||
@ -113,11 +139,11 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
if (isLoading || statusIds.size > 0 || !emptyMessage) {
|
||||
scrollableArea = (
|
||||
<div className='scrollable' ref={this.setRef}>
|
||||
<div className='status-list'>
|
||||
<div role='feed' className='status-list' onKeyDown={this.handleKeyDown}>
|
||||
{prepend}
|
||||
|
||||
{statusIds.map((statusId) => {
|
||||
return <StatusContainer key={statusId} id={statusId} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
|
||||
{statusIds.map((statusId, index) => {
|
||||
return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
|
||||
})}
|
||||
|
||||
{loadMore}
|
||||
|
@ -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);
|
@ -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'>
|
||||
|
@ -52,9 +52,10 @@ class Avatar extends ImmutablePureComponent {
|
||||
return (
|
||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ radius }) =>
|
||||
<a // eslint-disable-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href={account.get('url')}
|
||||
className='account__header__avatar'
|
||||
role='presentation'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
|
||||
@ -62,7 +63,9 @@ class Avatar extends ImmutablePureComponent {
|
||||
onMouseOut={this.handleMouseOut}
|
||||
onFocus={this.handleMouseOver}
|
||||
onBlur={this.handleMouseOut}
|
||||
/>
|
||||
>
|
||||
<span style={{ display: 'none' }}>{account.get('acct')}</span>
|
||||
</a>
|
||||
}
|
||||
</Motion>
|
||||
);
|
||||
|
@ -159,7 +159,10 @@ export default class ComposeForm extends ImmutablePureComponent {
|
||||
<div className='compose-form'>
|
||||
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
|
||||
<div className='spoiler-input'>
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' />
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' />
|
||||
</label>
|
||||
</div>
|
||||
</Collapsable>
|
||||
|
||||
|
@ -65,6 +65,22 @@ export default class EmojiPickerDropdown extends React.PureComponent {
|
||||
this.setState({ active: false });
|
||||
}
|
||||
|
||||
onToggle = (e) => {
|
||||
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
|
||||
if (this.state.active) {
|
||||
this.onHideDropdown();
|
||||
} else {
|
||||
this.onShowDropdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEmojiPickerKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.onHideDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
|
||||
@ -104,10 +120,11 @@ export default class EmojiPickerDropdown extends React.PureComponent {
|
||||
};
|
||||
|
||||
const { active, loading } = this.state;
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
|
||||
return (
|
||||
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
|
||||
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)}>
|
||||
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
|
||||
<DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-pressed={active} role='button' onKeyDown={this.onToggle} tabIndex={0} >
|
||||
<img
|
||||
className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
|
||||
alt='🙂'
|
||||
@ -118,7 +135,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
|
||||
<DropdownContent className='dropdown__left'>
|
||||
{
|
||||
this.state.active && !this.state.loading &&
|
||||
(<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} categories={categories} search />)
|
||||
(<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} onKeyDown={this.onEmojiPickerKeyDown} categories={categories} search />)
|
||||
}
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
|
@ -18,6 +18,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
||||
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
||||
<Avatar src={this.props.account.get('avatar')} animate size={40} />
|
||||
</Permalink>
|
||||
|
||||
|
@ -24,6 +24,10 @@ const iconStyle = {
|
||||
export default class PrivacyDropdown extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
@ -34,14 +38,36 @@ export default class PrivacyDropdown extends React.PureComponent {
|
||||
};
|
||||
|
||||
handleToggle = () => {
|
||||
this.setState({ open: !this.state.open });
|
||||
if (this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
}
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
e.preventDefault();
|
||||
this.setState({ open: false });
|
||||
this.props.onChange(value);
|
||||
if (e.key === 'Escape') {
|
||||
this.setState({ open: false });
|
||||
} else if (!e.key || e.key === 'Enter') {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
e.preventDefault();
|
||||
this.setState({ open: false });
|
||||
this.props.onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
onGlobalClick = (e) => {
|
||||
@ -50,6 +76,17 @@ export default class PrivacyDropdown extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('click', this.onGlobalClick);
|
||||
window.addEventListener('touchstart', this.onGlobalClick);
|
||||
@ -68,25 +105,18 @@ export default class PrivacyDropdown extends React.PureComponent {
|
||||
const { value, intl } = this.props;
|
||||
const { open } = this.state;
|
||||
|
||||
const options = [
|
||||
{ icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) },
|
||||
{ icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) },
|
||||
];
|
||||
|
||||
const valueOption = options.find(item => item.value === value);
|
||||
const valueOption = this.options.find(item => item.value === value);
|
||||
|
||||
return (
|
||||
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
|
||||
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} pressed={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||
<div className='privacy-dropdown__dropdown'>
|
||||
{open && options.map(item =>
|
||||
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
{open && this.options.map(item =>
|
||||
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.shortText}</strong>
|
||||
{item.longText}
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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);
|
||||
|
@ -65,21 +65,21 @@ export default class Compose extends React.PureComponent {
|
||||
const { columns } = this.props;
|
||||
header = (
|
||||
<nav className='drawer__header'>
|
||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link>
|
||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link>
|
||||
{!columns.some(column => column.get('id') === 'HOME') && (
|
||||
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link>
|
||||
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
|
||||
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link>
|
||||
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'COMMUNITY') && (
|
||||
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link>
|
||||
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'PUBLIC') && (
|
||||
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link>
|
||||
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link>
|
||||
)}
|
||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a>
|
||||
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a>
|
||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a>
|
||||
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -5,6 +5,7 @@ import spring from 'react-motion/lib/spring';
|
||||
import BundleContainer from '../containers/bundle_container';
|
||||
import BundleModalError from './bundle_modal_error';
|
||||
import ModalLoading from './modal_loading';
|
||||
import ActionsModal from '../components/actions_modal';
|
||||
import {
|
||||
MediaModal,
|
||||
OnboardingModal,
|
||||
@ -21,6 +22,7 @@ const MODAL_COMPONENTS = {
|
||||
'BOOST': BoostModal,
|
||||
'CONFIRM': ConfirmationModal,
|
||||
'REPORT': ReportModal,
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
@ -42,10 +44,34 @@ export default class ModalRoot extends React.PureComponent {
|
||||
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!!nextProps.type && !this.props.type) {
|
||||
this.activeElement = document.activeElement;
|
||||
|
||||
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (!this.type && !!prevProps.type) {
|
||||
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
|
||||
this.activeElement.focus();
|
||||
this.activeElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
getSiblings = () => {
|
||||
return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
|
||||
}
|
||||
|
||||
setRef = ref => {
|
||||
this.node = ref;
|
||||
}
|
||||
|
||||
willEnter () {
|
||||
return { opacity: 0, scale: 0.98 };
|
||||
}
|
||||
@ -84,11 +110,11 @@ export default class ModalRoot extends React.PureComponent {
|
||||
willLeave={this.willLeave}
|
||||
>
|
||||
{interpolatedStyles =>
|
||||
<div className='modal-root'>
|
||||
<div className='modal-root' ref={this.setRef}>
|
||||
{interpolatedStyles.map(({ key, data: { type, props }, style }) => (
|
||||
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
||||
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
|
||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||
<div role='dialog' className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
|
||||
</BundleContainer>
|
||||
|
@ -49,6 +49,10 @@ const mapStateToProps = state => ({
|
||||
@connect(mapStateToProps)
|
||||
export default class UI extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
@ -123,6 +127,14 @@ export default class UI extends React.PureComponent {
|
||||
this.setState({ draggingOver: false });
|
||||
}
|
||||
|
||||
handleServiceWorkerPostMessage = ({ data }) => {
|
||||
if (data.type === 'navigate') {
|
||||
this.context.router.history.push(data.path);
|
||||
} else {
|
||||
console.warn('Unknown message type:', data.type); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||
@ -131,6 +143,10 @@ export default class UI extends React.PureComponent {
|
||||
document.addEventListener('dragleave', this.handleDragLeave, false);
|
||||
document.addEventListener('dragend', this.handleDragEnd, false);
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
||||
}
|
||||
|
||||
this.props.dispatch(refreshHomeTimeline());
|
||||
this.props.dispatch(refreshNotifications());
|
||||
}
|
||||
|
@ -103,9 +103,9 @@ export function ReportModal () {
|
||||
}
|
||||
|
||||
export function MediaGallery () {
|
||||
return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery');
|
||||
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
|
||||
}
|
||||
|
||||
export function VideoPlayer () {
|
||||
return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player');
|
||||
return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
|
||||
}
|
||||
|
@ -5,6 +5,15 @@ export function isMobile(width) {
|
||||
};
|
||||
|
||||
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
let userTouching = false;
|
||||
|
||||
window.addEventListener('touchstart', () => {
|
||||
userTouching = true;
|
||||
}, { once: true });
|
||||
|
||||
export function isUserTouching() {
|
||||
return userTouching;
|
||||
}
|
||||
|
||||
export function isIOS() {
|
||||
return iOS;
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "المشاركات",
|
||||
"account.report": "أبلغ عن @{name}",
|
||||
"account.requested": "في انتظار الموافقة",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "إلغاء الحظر عن @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "إلغاء المتابعة",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "الخيط العام الموحد",
|
||||
"column_back_button.label": "العودة",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Публикации",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "В очакване на одобрение",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Не блокирай",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Не следвай",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Публичен канал",
|
||||
"column_back_button.label": "Назад",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Publicacions",
|
||||
"account.report": "Informe @{name}",
|
||||
"account.requested": "Esperant aprovació",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Desbloquejar @{name}",
|
||||
"account.unblock_domain": "Mostra {domain}",
|
||||
"account.unfollow": "Deixar de seguir",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Línia de temps federada",
|
||||
"column_back_button.label": "Enrere",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Beiträge",
|
||||
"account.report": "@{name} melden",
|
||||
"account.requested": "Warte auf Erlaubnis",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "@{name} entblocken",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Entfolgen",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Gesamtes bekanntes Netz",
|
||||
"column_back_button.label": "Zurück",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -61,6 +61,14 @@
|
||||
"defaultMessage": "Hide settings",
|
||||
"id": "column_header.hide_settings"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Move column to the left",
|
||||
"id": "column_header.moveLeft_settings"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Move column to the right",
|
||||
"id": "column_header.moveRight_settings"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Unpin",
|
||||
"id": "column_header.unpin"
|
||||
@ -366,6 +374,10 @@
|
||||
"defaultMessage": "Report @{name}",
|
||||
"id": "account.report"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Share @{name}'s profile",
|
||||
"id": "account.share"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Media",
|
||||
"id": "account.media"
|
||||
@ -1019,6 +1031,10 @@
|
||||
{
|
||||
"defaultMessage": "Report @{name}",
|
||||
"id": "status.report"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Share",
|
||||
"id": "status.share"
|
||||
}
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/status/components/action_bar.json"
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Posts",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Awaiting approval",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Unblock @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Unfollow",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Federated timeline",
|
||||
"column_back_button.label": "Back",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Mesaĝoj",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Atendas aprobon",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Malbloki @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Malsekvi",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Fratara tempolinio",
|
||||
"column_back_button.label": "Reveni",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Publicaciones",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Esperando aprobación",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Desbloquear",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Dejar de seguir",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Historia federada",
|
||||
"column_back_button.label": "Atrás",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "نوشتهها",
|
||||
"account.report": "گزارش @{name}",
|
||||
"account.requested": "در انتظار پذیرش",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "رفع انسداد @{name}",
|
||||
"account.unblock_domain": "رفع پنهانسازی از {domain}",
|
||||
"account.unfollow": "پایان پیگیری",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "نوشتههای همهجا",
|
||||
"column_back_button.label": "بازگشت",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Postit",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Odottaa hyväksyntää",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Salli @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Lopeta seuraaminen",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Yleinen aikajana",
|
||||
"column_back_button.label": "Takaisin",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Statuts",
|
||||
"account.report": "Signaler",
|
||||
"account.requested": "Invitation envoyée",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Débloquer",
|
||||
"account.unblock_domain": "Ne plus masquer {domain}",
|
||||
"account.unfollow": "Ne plus suivre",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Fil public global",
|
||||
"column_back_button.label": "Retour",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Épingler",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Retirer",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "הודעות",
|
||||
"account.report": "לדווח על @{name}",
|
||||
"account.requested": "בהמתנה לאישור",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "הסרת חסימה מעל @{name}",
|
||||
"account.unblock_domain": "הסר חסימה מקהילת {domain}",
|
||||
"account.unfollow": "הפסקת מעקב",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "בפרהסיה",
|
||||
"column_back_button.label": "חזרה",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Postovi",
|
||||
"account.report": "Prijavi @{name}",
|
||||
"account.requested": "Čeka pristanak",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Deblokiraj @{name}",
|
||||
"account.unblock_domain": "Otkrij {domain}",
|
||||
"account.unfollow": "Prestani slijediti",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Federalni timeline",
|
||||
"column_back_button.label": "Natrag",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Posts",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Awaiting approval",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Blokkolás levétele",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Követés abbahagyása",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Nyilvános",
|
||||
"column_back_button.label": "Vissza",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Postingan",
|
||||
"account.report": "Laporkan @{name}",
|
||||
"account.requested": "Menunggu persetujuan",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Hapus blokir @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Berhenti mengikuti",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Linimasa gabunggan",
|
||||
"column_back_button.label": "Kembali",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Mesaji",
|
||||
"account.report": "Denuncar @{name}",
|
||||
"account.requested": "Vartante aprobo",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Desblokusar @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Ne plus sequar",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Federata tempolineo",
|
||||
"column_back_button.label": "Retro",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Posts",
|
||||
"account.report": "Segnala @{name}",
|
||||
"account.requested": "In attesa di approvazione",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Sblocca @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Non seguire",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Timeline federata",
|
||||
"column_back_button.label": "Indietro",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "投稿",
|
||||
"account.report": "通報",
|
||||
"account.requested": "承認待ち",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "ブロック解除",
|
||||
"account.unblock_domain": "{domain}を表示",
|
||||
"account.unfollow": "フォロー解除",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "連合タイムライン",
|
||||
"column_back_button.label": "戻る",
|
||||
"column_header.hide_settings": "設定を隠す",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "ピン留めする",
|
||||
"column_header.show_settings": "設定を表示",
|
||||
"column_header.unpin": "ピン留めを外す",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "포스트",
|
||||
"account.report": "신고",
|
||||
"account.requested": "승인 대기 중",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "차단 해제",
|
||||
"account.unblock_domain": "{domain} 숨김 해제",
|
||||
"account.unfollow": "팔로우 해제",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "연합 타임라인",
|
||||
"column_back_button.label": "돌아가기",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "고정하기",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "고정 해제",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Toots",
|
||||
"account.report": "Rapporteer @{name}",
|
||||
"account.requested": "Wacht op goedkeuring",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Deblokkeer @{name}",
|
||||
"account.unblock_domain": "{domain} niet meer negeren",
|
||||
"account.unfollow": "Ontvolgen",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Globale tijdlijn",
|
||||
"column_back_button.label": "terug",
|
||||
"column_header.hide_settings": "Instellingen verbergen",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Vastmaken",
|
||||
"column_header.show_settings": "Instellingen tonen",
|
||||
"column_header.unpin": "Losmaken",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Innlegg",
|
||||
"account.report": "Rapportér @{name}",
|
||||
"account.requested": "Venter på godkjennelse",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Avblokker @{name}",
|
||||
"account.unblock_domain": "Vis {domain}",
|
||||
"account.unfollow": "Avfølg",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Felles tidslinje",
|
||||
"column_back_button.label": "Tilbake",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"account.block": "Blocar @{name}",
|
||||
"account.block_domain": "Tot amagar del domeni {domain}",
|
||||
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
|
||||
"account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incompletas.",
|
||||
"account.edit_profile": "Modificar lo perfil",
|
||||
"account.follow": "Sègre",
|
||||
"account.followers": "Seguidors",
|
||||
@ -13,18 +13,19 @@
|
||||
"account.posts": "Estatuts",
|
||||
"account.report": "Senhalar @{name}",
|
||||
"account.requested": "Invitacion mandada",
|
||||
"account.share": "Partejar lo perfil a @{name}",
|
||||
"account.unblock": "Desblocar @{name}",
|
||||
"account.unblock_domain": "Desblocar {domain}",
|
||||
"account.unfollow": "Quitar de sègre",
|
||||
"account.unmute": "Quitar de rescondre @{name}",
|
||||
"account.view_full_profile": "View full profile",
|
||||
"account.view_full_profile": "Veire lo perfil complet",
|
||||
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
|
||||
"bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.",
|
||||
"bundle_column_error.retry": "Tornar ensejar",
|
||||
"bundle_column_error.retry": "Tornar ensajar",
|
||||
"bundle_column_error.title": "Error de ret",
|
||||
"bundle_modal_error.close": "Tampar",
|
||||
"bundle_modal_error.message": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.",
|
||||
"bundle_modal_error.retry": "Tornar ensejar",
|
||||
"bundle_modal_error.message": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
|
||||
"bundle_modal_error.retry": "Tornar ensajar",
|
||||
"column.blocks": "Personas blocadas",
|
||||
"column.community": "Flux public local",
|
||||
"column.favourites": "Favorits",
|
||||
@ -34,9 +35,11 @@
|
||||
"column.notifications": "Notificacions",
|
||||
"column.public": "Flux public global",
|
||||
"column_back_button.label": "Tornar",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.hide_settings": "Amagar los paramètres",
|
||||
"column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
|
||||
"column_header.moveRight_settings": "Desplaçar la colomna a man esquèrra",
|
||||
"column_header.pin": "Penjar",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.show_settings": "Mostrar los paramètres",
|
||||
"column_header.unpin": "Despenjar",
|
||||
"column_subheading.navigation": "Navigacion",
|
||||
"column_subheading.settings": "Paramètres",
|
||||
@ -48,35 +51,35 @@
|
||||
"compose_form.publish_loud": "{publish} !",
|
||||
"compose_form.sensitive": "Marcar lo mèdia coma sensible",
|
||||
"compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
|
||||
"compose_form.spoiler_placeholder": "Avertiment",
|
||||
"compose_form.spoiler_placeholder": "Escrivètz l’avertiment aquí",
|
||||
"confirmation_modal.cancel": "Anullar",
|
||||
"confirmations.block.confirm": "Blocar",
|
||||
"confirmations.block.message": "Sètz segur de voler blocar {name} ?",
|
||||
"confirmations.delete.confirm": "Suprimir",
|
||||
"confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?",
|
||||
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
|
||||
"confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
|
||||
"confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
|
||||
"confirmations.mute.confirm": "Metre en silenci",
|
||||
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
|
||||
"confirmations.unfollow.confirm": "Quitar de sègre",
|
||||
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
|
||||
"emoji_button.activity": "Activitat",
|
||||
"emoji_button.activity": "Activitats",
|
||||
"emoji_button.flags": "Drapèus",
|
||||
"emoji_button.food": "Beure e manjar",
|
||||
"emoji_button.label": "Inserir un emoji",
|
||||
"emoji_button.nature": "Natura",
|
||||
"emoji_button.objects": "Objèctes",
|
||||
"emoji_button.people": "Gents",
|
||||
"emoji_button.search": "Cercar...",
|
||||
"emoji_button.search": "Cercar…",
|
||||
"emoji_button.symbols": "Simbòls",
|
||||
"emoji_button.travel": "Viatges & lòcs",
|
||||
"empty_column.community": "Lo flux public local es void. Escribètz quicòm per lo garnir !",
|
||||
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
|
||||
"empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
|
||||
"empty_column.home": "Pel moment segètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
|
||||
"empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
|
||||
"empty_column.home.inactivity": "Vòstra pagina d’acuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
|
||||
"empty_column.home.public_timeline": "lo flux public",
|
||||
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
|
||||
"empty_column.public": "I a pas res aquí ! Escribètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
|
||||
"empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
|
||||
"follow_request.authorize": "Autorizar",
|
||||
"follow_request.reject": "Regetar",
|
||||
"getting_started.appsshort": "Apps",
|
||||
@ -105,11 +108,11 @@
|
||||
"navigation_bar.preferences": "Preferéncias",
|
||||
"navigation_bar.public_timeline": "Flux public global",
|
||||
"notification.favourite": "{name} a ajustat a sos favorits :",
|
||||
"notification.follow": "{name} vos sèc.",
|
||||
"notification.follow": "{name} vos sèc",
|
||||
"notification.mention": "{name} vos a mencionat :",
|
||||
"notification.reblog": "{name} a partejat vòstre estatut :",
|
||||
"notifications.clear": "Levar",
|
||||
"notifications.clear_confirmation": "Volètz vertadièrament levar totas vòstras las notificacions ?",
|
||||
"notifications.clear": "Escafar",
|
||||
"notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
|
||||
"notifications.column_settings.alert": "Notificacions localas",
|
||||
"notifications.column_settings.favourite": "Favorits :",
|
||||
"notifications.column_settings.follow": "Nòus seguidors :",
|
||||
@ -121,15 +124,15 @@
|
||||
"notifications.column_settings.sound": "Emetre un son",
|
||||
"onboarding.done": "Fach",
|
||||
"onboarding.next": "Seguent",
|
||||
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra intància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
|
||||
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
|
||||
"onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
|
||||
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un enteragís amb vos",
|
||||
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
|
||||
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.",
|
||||
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
|
||||
"onboarding.page_one.welcome": "Benvengut a Mastodon !",
|
||||
"onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.",
|
||||
"onboarding.page_six.almost_done": "Gaireben acabat…",
|
||||
"onboarding.page_six.appetoot": "Bon Appetoot!",
|
||||
"onboarding.page_six.appetoot": "Bon Appetut!",
|
||||
"onboarding.page_six.apps_available": "I a d’aplicacions per mobil per iOS, Android e mai.",
|
||||
"onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.",
|
||||
"onboarding.page_six.guidelines": "guida de la comunitat",
|
||||
@ -170,7 +173,7 @@
|
||||
"status.report": "Senhalar @{name}",
|
||||
"status.sensitive_toggle": "Clicar per mostrar",
|
||||
"status.sensitive_warning": "Contengut sensible",
|
||||
"status.share": "Share",
|
||||
"status.share": "Partejar",
|
||||
"status.show_less": "Tornar plegar",
|
||||
"status.show_more": "Desplegar",
|
||||
"status.unmute_conversation": "Conversacions amb silenci levat",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Posty",
|
||||
"account.report": "Zgłoś @{name}",
|
||||
"account.requested": "Oczekująca prośba",
|
||||
"account.share": "Udostępnij profil @{name}",
|
||||
"account.unblock": "Odblokuj @{name}",
|
||||
"account.unblock_domain": "Odblokuj domenę {domain}",
|
||||
"account.unfollow": "Przestań śledzić",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Globalna oś czasu",
|
||||
"column_back_button.label": "Wróć",
|
||||
"column_header.hide_settings": "Ukryj ustawienia",
|
||||
"column_header.moveLeft_settings": "Przesuń kolumnę w lewo",
|
||||
"column_header.moveRight_settings": "Przesuń kolumnę w prawo",
|
||||
"column_header.pin": "Przypnij",
|
||||
"column_header.show_settings": "Pokaż ustawienia",
|
||||
"column_header.unpin": "Cofnij przypięcie",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Posts",
|
||||
"account.report": "Denunciar @{name}",
|
||||
"account.requested": "A aguardar aprovação",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Não bloquear @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Deixar de seguir",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Global",
|
||||
"column_back_button.label": "Voltar",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Posts",
|
||||
"account.report": "Denunciar @{name}",
|
||||
"account.requested": "A aguardar aprovação",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Não bloquear @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Deixar de seguir",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Global",
|
||||
"column_back_button.label": "Voltar",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Посты",
|
||||
"account.report": "Пожаловаться",
|
||||
"account.requested": "Ожидает подтверждения",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Разблокировать",
|
||||
"account.unblock_domain": "Разблокировать {domain}",
|
||||
"account.unfollow": "Отписаться",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Глобальная лента",
|
||||
"column_back_button.label": "Назад",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Закрепить",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Открепить",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Posts",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Awaiting approval",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Unblock @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Unfollow",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Federated timeline",
|
||||
"column_back_button.label": "Back",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Gönderiler",
|
||||
"account.report": "Rapor et @{name}",
|
||||
"account.requested": "Onay bekleniyor",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Engeli kaldır @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Takipten vazgeç",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Federe zaman tüneli",
|
||||
"column_back_button.label": "Geri",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "Пости",
|
||||
"account.report": "Поскаржитися",
|
||||
"account.requested": "Очікує підтвердження",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "Розблокувати",
|
||||
"account.unblock_domain": "Розблокувати {domain}",
|
||||
"account.unfollow": "Відписатися",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "Глобальна стрічка",
|
||||
"column_back_button.label": "Назад",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "嘟文",
|
||||
"account.report": "举报 @{name}",
|
||||
"account.requested": "等待审批",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "解除对 @{name} 的屏蔽",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "取消关注",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "跨站公共时间轴",
|
||||
"column_back_button.label": "Back",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "文章",
|
||||
"account.report": "舉報 @{name}",
|
||||
"account.requested": "等候審批",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "解除對 @{name} 的封鎖",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "取消關注",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "跨站時間軸",
|
||||
"column_back_button.label": "返回",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"account.posts": "貼文",
|
||||
"account.report": "檢舉 @{name}",
|
||||
"account.requested": "正在等待許可",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.unblock": "取消封鎖 @{name}",
|
||||
"account.unblock_domain": "不再隱藏 {domain}",
|
||||
"account.unfollow": "取消關注",
|
||||
@ -35,6 +36,8 @@
|
||||
"column.public": "聯盟時間軸",
|
||||
"column_back_button.label": "上一頁",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
|
@ -1,3 +1,45 @@
|
||||
const MAX_NOTIFICATIONS = 5;
|
||||
const GROUP_TAG = 'tag';
|
||||
|
||||
// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker
|
||||
const formatGroupTitle = (message, count) => message.replace('%{count}', count);
|
||||
|
||||
const notify = options =>
|
||||
self.registration.getNotifications().then(notifications => {
|
||||
if (notifications.length === MAX_NOTIFICATIONS) {
|
||||
// Reached the maximum number of notifications, proceed with grouping
|
||||
const group = {
|
||||
title: formatGroupTitle(notifications[0].data.message, notifications.length + 1),
|
||||
body: notifications
|
||||
.sort((n1, n2) => n1.timestamp < n2.timestamp)
|
||||
.map(notification => notification.title).join('\n'),
|
||||
badge: '/badge.png',
|
||||
icon: '/android-chrome-192x192.png',
|
||||
tag: GROUP_TAG,
|
||||
data: {
|
||||
url: (new URL('/web/notifications', self.location)).href,
|
||||
count: notifications.length + 1,
|
||||
message: notifications[0].data.message,
|
||||
},
|
||||
};
|
||||
|
||||
notifications.forEach(notification => notification.close());
|
||||
|
||||
return self.registration.showNotification(group.title, group);
|
||||
} else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) {
|
||||
// Already grouped, proceed with appending the notification to the group
|
||||
const group = cloneNotification(notifications[0]);
|
||||
|
||||
group.title = formatGroupTitle(group.data.message, group.data.count + 1);
|
||||
group.body = `${options.title}\n${group.body}`;
|
||||
group.data = { ...group.data, count: group.data.count + 1 };
|
||||
|
||||
return self.registration.showNotification(group.title, group);
|
||||
}
|
||||
|
||||
return self.registration.showNotification(options.title, options);
|
||||
});
|
||||
|
||||
const handlePush = (event) => {
|
||||
const options = event.data.json();
|
||||
|
||||
@ -17,7 +59,7 @@ const handlePush = (event) => {
|
||||
options.actions = options.data.actions;
|
||||
}
|
||||
|
||||
event.waitUntil(self.registration.showNotification(options.title, options));
|
||||
event.waitUntil(notify(options));
|
||||
};
|
||||
|
||||
const cloneNotification = (notification) => {
|
||||
@ -50,22 +92,37 @@ const makeRequest = (notification, action) =>
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const findBestClient = clients => {
|
||||
const focusedClient = clients.find(client => client.focused);
|
||||
const visibleClient = clients.find(client => client.visibilityState === 'visible');
|
||||
|
||||
return focusedClient || visibleClient || clients[0];
|
||||
};
|
||||
|
||||
const openUrl = url =>
|
||||
self.clients.matchAll({ type: 'window' }).then(clientList => {
|
||||
if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
|
||||
const webClients = clientList
|
||||
.filter(client => /\/web\//.test(client.url))
|
||||
.sort(client => client !== 'visible');
|
||||
if (clientList.length !== 0) {
|
||||
const webClients = clientList.filter(client => /\/web\//.test(client.url));
|
||||
|
||||
const visibleClient = clientList.find(client => client.visibilityState === 'visible');
|
||||
const focusedClient = clientList.find(client => client.focused);
|
||||
if (webClients.length !== 0) {
|
||||
const client = findBestClient(webClients);
|
||||
|
||||
const client = webClients[0] || visibleClient || focusedClient || clientList[0];
|
||||
const { pathname } = new URL(url);
|
||||
|
||||
return client.navigate(url).then(client => client.focus());
|
||||
} else {
|
||||
return self.clients.openWindow(url);
|
||||
if (pathname.startsWith('/web/')) {
|
||||
return client.focus().then(client => client.postMessage({
|
||||
type: 'navigate',
|
||||
path: pathname.slice('/web/'.length - 1),
|
||||
}));
|
||||
}
|
||||
} else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
|
||||
const client = findBestClient(clientList);
|
||||
|
||||
return client.navigate(url).then(client => client.focus());
|
||||
}
|
||||
}
|
||||
|
||||
return self.clients.openWindow(url);
|
||||
});
|
||||
|
||||
const removeActionFromNotification = (notification, action) => {
|
||||
|
@ -121,7 +121,7 @@
|
||||
|
||||
.information-board {
|
||||
background: darken($ui-base-color, 4%);
|
||||
padding: 40px 0;
|
||||
padding: 20px 0;
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
@ -162,13 +162,14 @@
|
||||
.information-board-sections {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.section {
|
||||
flex: 1 0 0;
|
||||
font: 16px/28px 'mastodon-font-sans-serif', sans-serif;
|
||||
text-align: right;
|
||||
padding: 0 15px;
|
||||
padding: 10px 15px;
|
||||
|
||||
span,
|
||||
strong {
|
||||
@ -190,14 +191,6 @@
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
flex-direction: column;
|
||||
|
||||
.section {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.owner {
|
||||
@ -547,6 +540,7 @@
|
||||
}
|
||||
|
||||
#mastodon-timeline {
|
||||
display: flex;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
font-family: 'mastodon-font-sans-serif', sans-serif;
|
||||
@ -561,11 +555,20 @@
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 6px rgba($black, 0.1);
|
||||
|
||||
.column-header {
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
font-weight: inherit;
|
||||
margin: 0;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.column {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
@ -651,16 +654,12 @@
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.information-board {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.information-board .container {
|
||||
padding-right: 20px;
|
||||
|
||||
.panel {
|
||||
position: static;
|
||||
margin-top: 30px;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
|
||||
|
@ -214,16 +214,18 @@
|
||||
}
|
||||
|
||||
.dropdown--active::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 4.5px 7.8px;
|
||||
border-color: transparent transparent $ui-secondary-color;
|
||||
bottom: 8px;
|
||||
right: 104px;
|
||||
@media screen and (min-width: 1025px) {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 4.5px 7.8px;
|
||||
border-color: transparent transparent $ui-secondary-color;
|
||||
bottom: 8px;
|
||||
right: 104px;
|
||||
}
|
||||
}
|
||||
|
||||
.invisible {
|
||||
@ -3402,7 +3404,8 @@ button.icon-button.active i.fa-retweet {
|
||||
|
||||
.boost-modal,
|
||||
.confirmation-modal,
|
||||
.report-modal {
|
||||
.report-modal,
|
||||
.actions-modal {
|
||||
background: lighten($ui-secondary-color, 8%);
|
||||
color: $ui-base-color;
|
||||
border-radius: 8px;
|
||||
@ -3427,6 +3430,15 @@ button.icon-button.active i.fa-retweet {
|
||||
}
|
||||
}
|
||||
|
||||
.actions-modal {
|
||||
.status {
|
||||
background: $white;
|
||||
border-bottom-color: $ui-secondary-color;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.boost-modal__container {
|
||||
overflow-x: scroll;
|
||||
padding: 10px;
|
||||
@ -3493,6 +3505,47 @@ button.icon-button.active i.fa-retweet {
|
||||
}
|
||||
}
|
||||
|
||||
.actions-modal {
|
||||
.status {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
|
||||
ul {
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
|
||||
li:empty {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li:not(:empty) {
|
||||
a {
|
||||
color: $ui-base-color;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
|
||||
&.active {
|
||||
&,
|
||||
button {
|
||||
background: $ui-highlight-color;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
button:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-modal__action-bar {
|
||||
.confirmation-modal__cancel-button {
|
||||
background-color: transparent;
|
||||
|
@ -53,6 +53,7 @@ class Web::PushSubscription < ApplicationRecord
|
||||
url: url,
|
||||
actions: actions,
|
||||
access_token: access_token,
|
||||
message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker
|
||||
}
|
||||
),
|
||||
endpoint: endpoint,
|
||||
@ -117,7 +118,7 @@ class Web::PushSubscription < ApplicationRecord
|
||||
when :mention then [
|
||||
{
|
||||
title: translate('push_notifications.mention.action_favourite'),
|
||||
icon: full_asset_url('emoji/2764.png', skip_pipeline: true),
|
||||
icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true),
|
||||
todo: 'request',
|
||||
method: 'POST',
|
||||
action: "/api/v1/statuses/#{notification.target_status.id}/favourite",
|
||||
@ -130,11 +131,11 @@ class Web::PushSubscription < ApplicationRecord
|
||||
can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden?
|
||||
|
||||
if should_hide
|
||||
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('emoji/1f441.png'), todo: 'expand', action: 'expand')
|
||||
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), todo: 'expand', action: 'expand')
|
||||
end
|
||||
|
||||
if can_boost
|
||||
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
|
||||
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('web-push-icon_boost.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
|
||||
end
|
||||
|
||||
actions
|
||||
|
@ -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
|
||||
|
@ -19,10 +19,10 @@
|
||||
%td
|
||||
%samp= session.ip
|
||||
%td
|
||||
- if request.session['auth_id'] == session.session_id
|
||||
- if current_session.session_id == session.session_id
|
||||
= t 'sessions.current_session'
|
||||
- else
|
||||
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)
|
||||
%td
|
||||
- if request.session['auth_id'] != session.session_id
|
||||
- if current_session.session_id != session.session_id
|
||||
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete
|
||||
|
@ -3,7 +3,7 @@
|
||||
class Pubsubhubbub::SubscribeWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'push', retry: 10, unique: :until_executed
|
||||
sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false
|
||||
|
||||
sidekiq_retry_in do |count|
|
||||
case count
|
||||
@ -18,6 +18,12 @@ class Pubsubhubbub::SubscribeWorker
|
||||
end
|
||||
end
|
||||
|
||||
sidekiq_retries_exhausted do |msg, _e|
|
||||
account = Account.find(msg['args'].first)
|
||||
logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing"
|
||||
::UnsubscribeService.new.call(account)
|
||||
end
|
||||
|
||||
def perform(account_id)
|
||||
account = Account.find(account_id)
|
||||
logger.debug "PuSH re-subscribing to #{account.acct}"
|
||||
|
@ -7,18 +7,19 @@ class WebPushNotificationWorker
|
||||
|
||||
def perform(session_activation_id, notification_id)
|
||||
session_activation = SessionActivation.find(session_activation_id)
|
||||
notification = Notification.find(notification_id)
|
||||
notification = Notification.find(notification_id)
|
||||
|
||||
return if session_activation.nil? || notification.nil?
|
||||
return if session_activation.web_push_subscription.nil? || notification.activity.nil?
|
||||
|
||||
begin
|
||||
session_activation.web_push_subscription.push(notification)
|
||||
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription => e
|
||||
# Subscription expiration is not currently implemented in any browser
|
||||
session_activation.web_push_subscription.destroy!
|
||||
session_activation.update!(web_push_subscription: nil)
|
||||
session_activation.web_push_subscription.push(notification)
|
||||
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
|
||||
# Subscription expiration is not currently implemented in any browser
|
||||
|
||||
raise e
|
||||
end
|
||||
session_activation.web_push_subscription.destroy!
|
||||
session_activation.update!(web_push_subscription: nil)
|
||||
|
||||
true
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
run.config:
|
||||
engine: ruby
|
||||
engine.config:
|
||||
runtime: ruby-2.4.1
|
||||
runtime: ruby-2.4
|
||||
|
||||
extra_packages:
|
||||
# basic servers:
|
||||
@ -20,6 +20,9 @@ run.config:
|
||||
# for node-gyp, used in the asset compilation process:
|
||||
- python-2
|
||||
|
||||
# i18n:
|
||||
- libidn
|
||||
|
||||
cache_dirs:
|
||||
- node_modules
|
||||
|
||||
@ -35,10 +38,6 @@ run.config:
|
||||
|
||||
extra_steps:
|
||||
- envsubst < .env.nanobox > .env
|
||||
- gem install bundler
|
||||
- bundle config build.nokogiri --with-iconv-dir=/data/ --with-zlib-dir=/data/
|
||||
- bundle config build.nokogumbo --with-iconv-dir=/data/ --with-zlib-dir=/data/
|
||||
- bundle install --clean
|
||||
- yarn
|
||||
|
||||
fs_watch: true
|
||||
|
@ -348,6 +348,8 @@ en:
|
||||
title: "%{name} favourited your status"
|
||||
follow:
|
||||
title: "%{name} is now following you"
|
||||
group:
|
||||
title: "%{count} notifications"
|
||||
mention:
|
||||
action_boost: Boost
|
||||
action_expand: Show more
|
||||
|
@ -1,19 +1,38 @@
|
||||
---
|
||||
oc:
|
||||
about:
|
||||
about_mastodon_html: Mastodon es un malhum social <em>liure e open-source</em>. Una alternativa <em>descentralizada</em> a las plat-formas comercialas, aquò evita qu’una sola companhiá monopolize vòstra comunicacion. Causissètz un servidor que vos fisatz, quina que siasque vòstra causida, podètz interagir amb tot lo mond. Qual que siasque pòt aver son instància Mastodon e participar al <em>malhum social</em> sens cap de problèmas.
|
||||
about_mastodon_html: Mastodon es un malhum social bastit amb de protocòls liures e gratuits. Es descentralizat coma los corrièls.
|
||||
about_this: A prepaus d’aquesta instància
|
||||
closed_registrations: Las inscripcions son clavadas pel moment sus aquesta instància.
|
||||
contact: Contacte
|
||||
contact_missing: Pas parametrat
|
||||
contact_unavailable: Pas disponible
|
||||
description_headline: Qué es %{domain} ?
|
||||
domain_count_after: autras instàncias
|
||||
domain_count_before: Connectat a
|
||||
other_instances: Autras instàncias
|
||||
extended_description_html: |
|
||||
<h3>Una bona plaça per las règlas</h3>
|
||||
<p>La descripcion longa es pas estada causida pel moment.</p>
|
||||
features:
|
||||
humane_approach_body: Amb l’experiéncia dels fracasses d’autres malhums, Mastodon ten per objectiu de lutar contra los abuses dels malhums socials en far de causidas eticas.
|
||||
humane_approach_title: Un biais mai uman
|
||||
not_a_product_body: Mastodon es pas un malhum comercial. Pas cap de reclama, d’utilizacion de vòstras donadas o d’òrt daurat clavat. I a pas cap d’autoritat centrala.
|
||||
not_a_product_title: Sètz una persona, non pas un produit
|
||||
real_conversation_body: Amb 500 caractèrs a vòstra disposicion e un nivèl de confidencialitat per cada publicacion, podètz vos exprimir coma volètz.
|
||||
real_conversation_title: Fach per de conversacions vertadièras
|
||||
within_reach_body: Multiplas aplicacion per iOS, Android, e autras plataformas mercés a un entorn API de bon utilizar, vos permet de gardar lo contacte pertot.
|
||||
within_reach_title: Totjorn al costat
|
||||
find_another_instance: Trobar mai instàncias
|
||||
generic_description: "%{domain} es un dels servidors del malhum"
|
||||
hosted_on: Mastodon albergat sus %{domain}
|
||||
learn_more: Ne saber mai
|
||||
other_instances: Lista d’instàncias
|
||||
source_code: Còdi font
|
||||
status_count_after: estatuts
|
||||
status_count_before: qu’an escrich
|
||||
user_count_after: personas
|
||||
user_count_before: Ostal de
|
||||
what_is_mastodon: Qu’es Mastodon ?
|
||||
accounts:
|
||||
follow: Sègre
|
||||
followers: Seguidors
|
||||
@ -23,6 +42,7 @@ oc:
|
||||
people_who_follow: Lo mond que sègon %{name}
|
||||
posts: Estatuts
|
||||
remote_follow: Sègre a distància
|
||||
reserved_username: Aqueste nom d’utilizaire es reservat
|
||||
unfollow: Quitar de sègre
|
||||
admin:
|
||||
accounts:
|
||||
@ -60,8 +80,10 @@ oc:
|
||||
profile_url: URL del perfil
|
||||
public: Public
|
||||
push_subscription_expires: Fin de l’abonament PuSH
|
||||
redownload: Actualizar los avatars
|
||||
reset: Reïnicializar
|
||||
reset_password: Reïnicializar lo senhal
|
||||
resubscribe: Se tornar abonar
|
||||
salmon_url: URL Salmon
|
||||
search: Cercar
|
||||
show:
|
||||
@ -70,13 +92,14 @@ oc:
|
||||
targeted_reports: Rapòrts faches tocant aqueste compte
|
||||
silence: Silenci
|
||||
statuses: Estatuts
|
||||
subscribe: S’abonar
|
||||
title: Comptes
|
||||
undo_silenced: Levar lo silenci
|
||||
undo_suspension: Levar la suspension
|
||||
username: Nom d’utilizaire
|
||||
web: Web
|
||||
domain_blocks:
|
||||
add_new: Ajustar un nòu
|
||||
add_new: N’ajustar un nòu
|
||||
created_msg: Domeni blocat es a èsser tractat
|
||||
destroyed_msg: Lo blocatge del domeni es estat levat
|
||||
domain: Domeni
|
||||
@ -85,12 +108,14 @@ oc:
|
||||
hint: Lo blocatge empacharà pas la creacion de compte dins la basa de donadas, mai aplicarà la moderacion sus aquestes comptes.
|
||||
severity:
|
||||
desc_html: "<strong>Silenci</strong> farà venir invisibles los estatuts del compte al mond que son pas de seguidors. <strong>Suspendre</strong> levarà tot lo contengut del compte, los mèdias e las donadas de perfil."
|
||||
noop: Cap
|
||||
silence: Silenci
|
||||
suspend: Suspendre
|
||||
title: Nòu blocatge domeni
|
||||
reject_media: Regetar los fichièrs mèdias
|
||||
reject_media_hint: Lèva los fichièrs gardats localament e regèta las demandas de telecargament dins lo futur. Servís pas a res per las suspensions
|
||||
severities:
|
||||
noop: Cap
|
||||
silence: Silenci
|
||||
suspend: Suspendre
|
||||
severity: Severitat
|
||||
@ -110,6 +135,7 @@ oc:
|
||||
domain_name: Domeni
|
||||
title: Instàncias conegudas
|
||||
reports:
|
||||
action_taken_by: Accion menada per
|
||||
are_you_sure: Es segur ?
|
||||
comment:
|
||||
label: Comentari
|
||||
@ -147,7 +173,7 @@ oc:
|
||||
desc_html: Autorizar lo monde a se marcar
|
||||
title: Inscripcions
|
||||
site_description:
|
||||
desc_html: Afichada jos la forma de paragrafe sus la pagina d’acuèlh e utilizada coma balisa meta.<br> Podètz utilizar de balisas HTML, coma <code><a></code> e <code><em></code>.
|
||||
desc_html: Afichada jos la forma de paragraf sus la pagina d’acuèlh e utilizada coma balisa meta.<br> Podètz utilizar de balisas HTML, coma <code><a></code> e <code><em></code>.
|
||||
title: Descripcion del site
|
||||
site_description_extended:
|
||||
desc_html: Afichada sus la pagina d’informacion complementària del site<br>Podètz utilizar de balisas HTML
|
||||
@ -183,6 +209,10 @@ oc:
|
||||
title: WebSub
|
||||
topic: Subjècte
|
||||
title: Administracion
|
||||
admin_mailer:
|
||||
new_report:
|
||||
body: "%{reporter} a senhalat %{target}"
|
||||
subject: Novèl senhalament per %{instance} (#%{id})
|
||||
application_mailer:
|
||||
settings: 'Cambiar las preferéncias de corrièl : %{link}'
|
||||
signature: Notificacion de Mastodon sus %{instance}
|
||||
@ -190,7 +220,8 @@ oc:
|
||||
applications:
|
||||
invalid_url: L’URL donada es invalida
|
||||
auth:
|
||||
change_password: Cambiar lo senhal
|
||||
agreement_html: En vos marcar acceptatz <a href="%{rules_path}">nòstres tèrmes de servici</a> e <a href="%{terms_path}">politica de confidencialitat</a>.
|
||||
change_password: Seguretat
|
||||
delete_account: Suprimir lo compte
|
||||
delete_account_html: Se volètz suprimir vòstre compte, podètz <a href="%{path}">o far aquí</a>. Vos demandarem que confirmetz.
|
||||
didnt_get_confirmation: Avètz pas recebut las instruccions de confirmacion ?
|
||||
@ -204,6 +235,12 @@ oc:
|
||||
authorize_follow:
|
||||
error: O planhèm, i a agut una error al moment de cercar lo compte
|
||||
follow: Sègre
|
||||
follow_request: 'Avètz demandat de sègre :'
|
||||
following: 'Felicitacion ! Seguètz ara :'
|
||||
post_follow:
|
||||
close: O podètz tampar aquesta fenèstra.
|
||||
return: Tornar al perfil
|
||||
web: Tornar a l’interfàcia Web
|
||||
prompt_html: 'Avètz (<strong>%{self}</strong>) demandat de sègre :'
|
||||
title: Sègre %{acct}
|
||||
date:
|
||||
@ -288,12 +325,14 @@ oc:
|
||||
warning_html: La supression del contengut d’aquesta instància es sola assegurada. Lo contengut fòrça partejat daissarà probablament de traças. Los servidors fòra-linha e los que vos sègon pas mai auràn pas la mesa a jorn de lor basa de donada.
|
||||
warning_title: Disponibilitat del contengut difusat
|
||||
errors:
|
||||
'403': Avètz pas l’autorizacion de veire aquesta pagina.
|
||||
'404': La pagina que recercatz existís pas.
|
||||
'410': La pagina que cercatz existís pas mai.
|
||||
'422':
|
||||
content: Verificacion de seguretat fracassada. Blocatz los cookies ?
|
||||
title: Verificacion de seguretat fracassada
|
||||
'429': Lo servidor mòla (subrecargada)
|
||||
noscript: Per utilizar l’aplicacion web de Mastodon, mercés d’activar JavaScript. O podètz utilizar una aplicacion per vòstra plataforma coma alernativa.
|
||||
exports:
|
||||
blocks: Personas que blocatz
|
||||
csv: CSV
|
||||
@ -327,7 +366,7 @@ oc:
|
||||
following: Lista de mond que seguètz
|
||||
muting: Lista de mond que volètz pas legir
|
||||
upload: Importar
|
||||
landing_strip_html: "<strong>%{name}</strong> es un utilizaire de %{link_to_root_path}. Podètz lo/la sègre o interagir amb el o ela s’avètz un compte ont que siasque sul fediverse."
|
||||
landing_strip_html: "<strong>%{name}</strong> utiliza %{link_to_root_path}. Podètz lo/la sègre o interagir amb el o ela s’avètz un compte ont que siasque sul fediverse."
|
||||
landing_strip_signup_html: S’es pas lo cas, podètz <a href="%{sign_up_path}">vos marcar aquí</a>.
|
||||
media_attachments:
|
||||
validations:
|
||||
@ -362,6 +401,23 @@ oc:
|
||||
next: Seguent
|
||||
prev: Precedent
|
||||
truncate: "…"
|
||||
push_notifications:
|
||||
favourite:
|
||||
title: "%{name} a mes vòstre estatut en favorit"
|
||||
follow:
|
||||
title: "%{name} vos sèc ara"
|
||||
group:
|
||||
title: "%{count} notificacions"
|
||||
mention:
|
||||
action_boost: Partejar
|
||||
action_expand: Ne veire mai
|
||||
action_favourite: Ajustar als favorits
|
||||
title: "%{name} vos a mencionat"
|
||||
reblog:
|
||||
title: "%{name} a partejat vòstre estatut"
|
||||
subscribed:
|
||||
body: Podètz ara recebre las notificacions push.
|
||||
title: Abonament enregistrat !
|
||||
remote_follow:
|
||||
acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire
|
||||
missing_resource: URL de redireccion pas trobada
|
||||
@ -438,7 +494,7 @@ oc:
|
||||
|
||||
<h3 id="collect">Quinas informacions collectem ?</h3>
|
||||
|
||||
<p>Collectem informacions sus vos quand vos marcatz sus nòstre site e juntem las donadas quand participatz a nòstre forum ne legissent, escrivent e notant lo contengut partejat aquí.</p>
|
||||
<p>Collectem informacions sus vos quand vos marcatz sus nòstre site e juntem las donadas quand participatz a nòstre forum en legissent, escrivent e notant lo contengut partejat aquí.</p>
|
||||
|
||||
<p>Pendent l’inscripcion podèm vos demandar vòstre nom e adreça de corrièl. Podètz çaquelà visitar nòstre site sens vos marcar. Verificarem vòstra adreça amb un messatge donant un ligam unic. Se clicatz sul ligam sauprem qu’avètz lo contraròtle de l’adreça.</p>
|
||||
|
||||
@ -472,13 +528,13 @@ oc:
|
||||
|
||||
<p>Òc-ben. Los cookies son de pichons fichièrs qu’un site o sos forneires de servicis plaçan dins lo disc dur de vòstre ordenador via lo navigator Web (Se los acceptatz). Aqueles cookies permeton al site de reconéisser vòstre navigator e se tenètz un compte enregistrat de l’associar a vòstre compte.</p>
|
||||
|
||||
<p>Empleguem de cookies per comprendre e enregistrar vòstras preferéncias per vòstras visitas venentas, per recampar de donadas sul trafic del site e las interaccions per fin que posquem ofrir una melhora experiéncia del site e de las aisinas pel futur. Pòt arribar que contractèssem amb de forneires de servicis tèrces per nos ajudar a comprendre melhor nòstres visitors. Aqueles forneires an pas lo drech que d’utilizar las donadas collectadas per nos ajudar a menar e melhorar nòstre afar.</p>
|
||||
<p>Empleguem de cookies per comprendre e enregistrar vòstras preferéncias per vòstras visitas venentas, per recampar de donadas sul trafic del site e las interaccions per dire que posquem ofrir una melhora experiéncia del site e de las aisinas pel futur. Pòt arribar que contractèssem amb de forneires de servicis tèrces per nos ajudar a comprendre melhor nòstres visitors. Aqueles forneires an pas lo drech que d’utilizar las donadas collectadas per nos ajudar a menar e melhorar nòstre afar.</p>
|
||||
|
||||
<h3 id="disclose">Divulguem d’informacions a de partits exteriors ?</h3>
|
||||
<h3 id="disclose">Divulguem d’informacions a de tèrces ?</h3>
|
||||
|
||||
<p>Vendèm pas, comercem o qualque transferiment que siasque a de partits exteriors vòstras informacions personalas identificablas. Aquò inclutz pas los tèrces partits de confisança que nos assiston a menar nòstre site, menar nòstre afar o vos servir, baste que son d’acòrd per gardar aquelas informacions confidencialas. Pòt tanben arribar que liberèssem vòstras informacions quand cresèm qu’es apropriat d’o far per se sometre a la lei, per refortir nòstras politicas, o per protegir los dreches, proprietats o seguritat de qualqu’un o de nosautres. Pasmens es possible que mandèssem d’informacions non-personalas e identificablas de nòstres visitors a d’autres partits per d’utilizacion en marketing, publicitat o un emplec mai.</p>
|
||||
<p>Vendèm pas, comercem o qualque transferiment que siasque a de tèrces vòstras informacions personalas identificablas. Aquò inclutz pas los tèrces partits de confisança que nos assiston a menar nòstre site, menar nòstre afar o vos servir, baste que son d’acòrd per gardar aquelas informacions confidencialas. Pòt tanben arribar que liberèssem vòstras informacions quand cresèm qu’es apropriat d’o far per se sometre a la lei, per refortir nòstras politicas, o per protegir los dreches, proprietats o seguritat de qualqu’un o de nosautres. Pasmens es possible que mandèssem d’informacions non-personalas e identificablas de nòstres visitors a d’autres partits per d’utilizacion en marketing, publicitat o un emplec mai.</p>
|
||||
|
||||
<h3 id="third-party">Ligams de tèrces partits</h3>
|
||||
<h3 id="third-party">Ligams de tèrces</h3>
|
||||
|
||||
<p>Pòt arribar, a nòstra discrecion, qu’incluguèssem o ofriguèssem de produches o servicis de tèrces partits sus nòstre site. Aqueles sites tèrces an de politicas de confidencialitats separadas e independentas. En consequéncia avèm pas cap de responsabilitat pel contengut e las activitats d’aqueles sites ligats. Pasmens cerquem de protegir l’integritat de nòstre site e aculhèm los comentaris tocant aqueles sites.</p>
|
||||
|
||||
@ -515,6 +571,7 @@ oc:
|
||||
instructions_html: "<strong>Escanatz aqueste còdi QR amb Google Authenticator o una aplicacion similària sus vòstre mobil</strong>. A partir d’ara, aquesta aplicacion generarà un geton que vos caldrà picar per vos connectar."
|
||||
lost_recovery_codes: Los còdi de recuperacion vos permeton d’accedir a vòstre compte se perdètz vòstre mobil. S’avètz perdut vòstres còdis de recuperacion los podètz tornar generar aquí. Los ancians còdis seràn pas mai valides.
|
||||
manual_instructions: 'Se podètz pas numerizar lo còdi QR e que vos cal picar lo còdi a la man, vaquí lo còdi en clar :'
|
||||
recovery_codes: Salvar los còdis de recuperacion
|
||||
recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar
|
||||
recovery_instructions_html: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants.
|
||||
setup: Paramètres
|
||||
|
@ -350,6 +350,8 @@ pl:
|
||||
title: "%{name} dodał Twój status do ulubionych"
|
||||
follow:
|
||||
title: "%{name} zaczął Cię śledzić"
|
||||
group:
|
||||
title: "%{count} powiadomień"
|
||||
mention:
|
||||
action_boost: Podbij
|
||||
action_expand: Pokaż więcej
|
||||
|
@ -267,6 +267,21 @@ ru:
|
||||
next: След
|
||||
prev: Пред
|
||||
truncate: "…"
|
||||
push_notifications:
|
||||
favourite:
|
||||
title: "Ваш статус понравился %{name}"
|
||||
follow:
|
||||
title: "%{name} теперь подписан(а) на Вас"
|
||||
mention:
|
||||
action_boost: Продвинуть
|
||||
action_expand: Развернуть
|
||||
action_favourite: Нравится
|
||||
title: "Вас упомянул(а) %{name}"
|
||||
reblog:
|
||||
title: "%{name} продвинул(а) Ваш статус"
|
||||
subscribed:
|
||||
body: Теперь Вы можете получать push-уведомления.
|
||||
title: Подписка зарегистрирована!
|
||||
remote_follow:
|
||||
acct: Введите username@domain, откуда Вы хотите подписаться
|
||||
missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей
|
||||
@ -335,6 +350,8 @@ ru:
|
||||
click_to_show: Показать
|
||||
reblogged: продвинул(а)
|
||||
sensitive_content: Чувствительный контент
|
||||
terms:
|
||||
title: "Условия обслуживания и политика конфиденциальности %{instance}"
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
@ -16,6 +16,7 @@ ru:
|
||||
many: Осталось <span class="name-counter">%{count}</span> символов
|
||||
one: Остался <span class="name-counter">1</span> символ
|
||||
other: Осталось <span class="name-counter">%{count}</span> символов
|
||||
setting_noindex: Относится к Вашему публичному профилю и страницам статусов
|
||||
imports:
|
||||
data: Файл CSV, экспортированный с другого узла Mastodon
|
||||
sessions:
|
||||
@ -42,7 +43,11 @@ ru:
|
||||
setting_auto_play_gif: Автоматически проигрывать анимированные GIF
|
||||
setting_boost_modal: Показывать диалог подтверждения перед продвижением
|
||||
setting_default_privacy: Видимость постов
|
||||
setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный
|
||||
setting_delete_modal: Показывать диалог подтверждения перед удалением
|
||||
setting_noindex: Отказаться от индексации в поисковых машинах
|
||||
setting_system_font_ui: Использовать шрифт системы по умолчанию
|
||||
setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта
|
||||
severity: Строгость
|
||||
type: Тип импорта
|
||||
username: Имя пользователя
|
||||
|
@ -3,6 +3,8 @@
|
||||
require 'sidekiq/web'
|
||||
require 'sidekiq-scheduler/web'
|
||||
|
||||
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
|
||||
|
||||
Rails.application.routes.draw do
|
||||
mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development?
|
||||
|
||||
|
@ -17,9 +17,11 @@ defaults: &defaults
|
||||
closed_registrations_message: ''
|
||||
open_deletion: true
|
||||
timeline_preview: true
|
||||
default_sensitive: false
|
||||
unfollow_modal: false
|
||||
boost_modal: false
|
||||
auto_play_gif: false
|
||||
delete_modal: true
|
||||
auto_play_gif: false
|
||||
system_font_ui: false
|
||||
noindex: false
|
||||
notification_emails:
|
||||
|
@ -10,7 +10,11 @@ const { publicPath } = require('./configuration.js');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
output: { filename: '[name]-[chunkhash].js' },
|
||||
output: {
|
||||
filename: '[name]-[chunkhash].js',
|
||||
chunkFilename: '[name]-[chunkhash].js',
|
||||
},
|
||||
|
||||
devtool: 'source-map', // separate sourcemap file, suitable for production
|
||||
stats: 'normal',
|
||||
|
||||
@ -48,7 +52,7 @@ module.exports = merge(sharedConfig, {
|
||||
ServiceWorker: {
|
||||
entry: path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'),
|
||||
cacheName: 'mastodon',
|
||||
output: '../sw.js',
|
||||
output: '../assets/sw.js',
|
||||
publicPath: '/sw.js',
|
||||
minify: true,
|
||||
},
|
||||
|
@ -33,7 +33,7 @@ module.exports = {
|
||||
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name]-[chunkhash].js',
|
||||
chunkFilename: '[name].js',
|
||||
path: output.path,
|
||||
publicPath: output.publicPath,
|
||||
},
|
||||
|
@ -21,7 +21,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def flags
|
||||
'rc1'
|
||||
'rc2'
|
||||
end
|
||||
|
||||
def to_a
|
||||
|
15
package.json
15
package.json
@ -7,9 +7,8 @@
|
||||
"build:production": "cross-env RAILS_ENV=production ./bin/webpack",
|
||||
"manage:translations": "node ./config/webpack/translationRunner.js",
|
||||
"start": "node ./streaming/index.js",
|
||||
"storybook": "cross-env NODE_ENV=test start-storybook -s ./public -p 9001 -c storybook",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ storybook/ streaming/",
|
||||
"test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ streaming/",
|
||||
"test:mocha": "cross-env NODE_ENV=test mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.js",
|
||||
"postinstall": "npm rebuild node-sass"
|
||||
},
|
||||
@ -57,7 +56,7 @@
|
||||
"glob": "^7.1.1",
|
||||
"http-link-header": "^0.8.0",
|
||||
"immutable": "^3.8.1",
|
||||
"intersection-observer": "^0.3.2",
|
||||
"intersection-observer": "^0.4.0",
|
||||
"intl": "^1.2.5",
|
||||
"intl-relativeformat": "^2.0.0",
|
||||
"is-nan": "^1.2.1",
|
||||
@ -113,15 +112,13 @@
|
||||
"tiny-queue": "^0.2.1",
|
||||
"uuid": "^3.1.0",
|
||||
"uws": "^8.14.0",
|
||||
"webpack": "^3.0.0",
|
||||
"webpack-bundle-analyzer": "^2.8.2",
|
||||
"webpack-manifest-plugin": "^1.1.2",
|
||||
"webpack": "^3.4.1",
|
||||
"webpack-bundle-analyzer": "^2.8.3",
|
||||
"webpack-manifest-plugin": "^1.2.1",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"websocket.js": "^0.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^3.1.8",
|
||||
"@storybook/react": "^3.1.8",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"chai": "^4.1.0",
|
||||
"chai-enzyme": "^0.8.0",
|
||||
@ -134,7 +131,7 @@
|
||||
"react-intl-translations-manager": "^5.0.0",
|
||||
"react-test-renderer": "^15.6.1",
|
||||
"sinon": "^2.3.7",
|
||||
"webpack-dev-server": "^2.5.1",
|
||||
"webpack-dev-server": "^2.6.1",
|
||||
"yargs": "^8.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
1
public/sw.js
Symbolic link
1
public/sw.js
Symbolic link
@ -0,0 +1 @@
|
||||
assets/sw.js
|
BIN
public/web-push-icon_expand.png
Normal file
BIN
public/web-push-icon_expand.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
public/web-push-icon_favourite.png
Normal file
BIN
public/web-push-icon_favourite.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
public/web-push-icon_reblog.png
Normal file
BIN
public/web-push-icon_reblog.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 851 B |
@ -4,7 +4,7 @@ describe Api::V1::Accounts::CredentialsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
|
@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowerAccountsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||
|
||||
before do
|
||||
Fabricate(:follow, target_account: user.account)
|
||||
|
@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowingAccountsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||
|
||||
before do
|
||||
Fabricate(:follow, account: user.account)
|
||||
|
@ -4,7 +4,7 @@ describe Api::V1::Accounts::RelationshipsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
|
@ -4,7 +4,7 @@ RSpec.describe Api::V1::Accounts::SearchController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
|
@ -4,7 +4,7 @@ describe Api::V1::Accounts::StatusesController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
|
@ -4,7 +4,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow read') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
|
@ -4,7 +4,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
|
||||
|
||||
before do
|
||||
Fabricate(:block, account: user.account)
|
||||
|
@ -4,7 +4,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
|
||||
|
||||
before do
|
||||
user.account.block_domain!('example.com')
|
||||
|
@ -3,19 +3,77 @@ require 'rails_helper'
|
||||
RSpec.describe Api::V1::FavouritesController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
|
||||
before do
|
||||
Fabricate(:favourite, account: user.account)
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
end
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'returns http success' do
|
||||
get :index, params: { limit: 1 }
|
||||
context 'without token' do
|
||||
it 'returns http unauthorized' do
|
||||
get :index
|
||||
expect(response).to have_http_status :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
context 'with token' do
|
||||
context 'without read scope' do
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) do
|
||||
Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: '')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
get :index
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
context 'without valid resource owner' do
|
||||
before do
|
||||
token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read')
|
||||
user.destroy!
|
||||
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
end
|
||||
|
||||
it 'returns http unprocessable entity' do
|
||||
get :index
|
||||
expect(response).to have_http_status :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
context 'with read scope and valid resource owner' do
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) do
|
||||
Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read')
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows favourites owned by the user' do
|
||||
favourite_by_user = Fabricate(:favourite, account: user.account)
|
||||
favourite_by_others = Fabricate(:favourite)
|
||||
|
||||
get :index
|
||||
|
||||
expect(assigns(:statuses)).to match_array [favourite_by_user.status]
|
||||
end
|
||||
|
||||
it 'adds pagination headers if necessary' do
|
||||
favourite = Fabricate(:favourite, account: user.account)
|
||||
|
||||
get :index, params: { limit: 1 }
|
||||
|
||||
expect(response.headers['Link'].find_link(['rel', 'next']).href).to eq "http://test.host/api/v1/favourites?limit=1&max_id=#{favourite.id}"
|
||||
expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/favourites?limit=1&since_id=#{favourite.id}"
|
||||
end
|
||||
|
||||
it 'does not add pagination headers if not necessary' do
|
||||
get :index
|
||||
|
||||
expect(response.headers['Link'].find_link(['rel', 'next'])).to eq nil
|
||||
expect(response.headers['Link'].find_link(['rel', 'prev'])).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice', locked: true)) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
|
||||
let(:follower) { Fabricate(:account, username: 'bob') }
|
||||
|
||||
before do
|
||||
|
@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user