Merge tag 'v1.5.0rc3' into kosmos

This commit is contained in:
Basti 2017-08-01 12:56:22 +02:00
commit 1388d73e0c
85 changed files with 333 additions and 91 deletions

View File

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

View File

@ -1,3 +1,4 @@
libpq-dev
protobuf-compiler
libprotobuf-dev
ffmpeg

View File

@ -9,16 +9,19 @@ export default class ColumnBackButton extends React.PureComponent {
};
handleClick = () => {
if (window.history && window.history.length === 1) this.context.router.history.push('/');
else this.context.router.history.goBack();
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
}
}
render () {
return (
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
<button onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</button>
);
}

View File

@ -134,7 +134,7 @@ export default class DropdownMenu extends React.PureComponent {
return (
<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}>
<DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-expanded={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</DropdownTrigger>

View File

@ -13,6 +13,7 @@ export default class IconButton extends React.PureComponent {
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
@ -77,6 +78,7 @@ export default class IconButton extends React.PureComponent {
<button
aria-label={this.props.title}
aria-pressed={this.props.pressed}
aria-expanded={this.props.expanded}
title={this.props.title}
className={classes.join(' ')}
onClick={this.handleClick}

View File

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

View File

@ -172,7 +172,7 @@ export default class Status extends ImmutablePureComponent {
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status, account, intersectionObserverWrapper, index, listLength, ...other } = this.props;
const { status, account, intersectionObserverWrapper, index, listLength, wrapped, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
if (status === null) {
@ -234,7 +234,7 @@ export default class Status extends ImmutablePureComponent {
}
return (
<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}>
<article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex={wrapped ? null : '0'} ref={this.handleRef}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>

View File

@ -151,8 +151,8 @@ export default class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton}
<div className='status__action-bar-dropdown'>

View File

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

View File

@ -109,9 +109,9 @@ export default class StatusList extends ImmutablePureComponent {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].nextElementSibling;
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
case 'PageUp':
return e.nativeEvent.path[0].nodeName === 'ARTICLE' && e.nativeEvent.path[0].previousElementSibling;
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':

View File

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

View File

@ -18,6 +18,7 @@ import WarningContainer from '../containers/warning_container';
import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@ -145,9 +146,9 @@ export default class ComposeForm extends ImmutablePureComponent {
render () {
const { intl, onPaste, showSearch } = this.props;
const disabled = this.props.is_submitting;
const text = [this.props.spoiler_text, this.props.text].join('');
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
let publishText = '';
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
@ -203,7 +204,7 @@ export default class ComposeForm extends ImmutablePureComponent {
<div className='compose-form__publish'>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !==0 && text.trim().length === 0)} block /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
</div>
</div>
</div>

View File

@ -124,7 +124,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
return (
<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} >
<DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} >
<img
className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
alt='🙂'

View File

@ -109,7 +109,7 @@ export default class PrivacyDropdown extends React.PureComponent {
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} pressed={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} expanded={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__dropdown'>
{open && 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' : ''}`}>

View File

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

View File

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

View File

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

View File

@ -53,7 +53,7 @@ export default class ModalRoot extends React.PureComponent {
}
componentDidUpdate (prevProps) {
if (!this.type && !!prevProps.type) {
if (!this.props.type && !!prevProps.type) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
this.activeElement.focus();
this.activeElement = null;

View File

@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import NavLink from 'react-router-dom/NavLink';
import { FormattedMessage, injectIntl } from 'react-intl';
import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile';
export const links = [
<NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
@ -25,16 +27,56 @@ export function getLink (index) {
@injectIntl
export default class TabsBar extends React.Component {
static contextTypes = {
router: PropTypes.object.isRequired,
}
static propTypes = {
intl: PropTypes.object.isRequired,
}
setRef = ref => {
this.node = ref;
}
handleClick = (e) => {
// Only apply optimization for touch devices, which we assume are slower
// We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
if (isUserTouching()) {
e.preventDefault();
e.persist();
requestAnimationFrame(() => {
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
const currentTab = tabs.find(tab => tab.classList.contains('active'));
const nextTab = tabs.find(tab => tab.contains(e.target));
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
if (currentTab !== nextTab) {
if (currentTab) {
currentTab.classList.remove('active');
}
const listener = debounce(() => {
nextTab.removeEventListener('transitionend', listener);
this.context.router.history.push(to);
}, 50);
nextTab.addEventListener('transitionend', listener);
nextTab.classList.add('active');
}
});
}
}
render () {
const { intl: { formatMessage } } = this.props;
return (
<nav className='tabs-bar'>
{links.map(link => React.cloneElement(link, { key: link.props.to, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
<nav className='tabs-bar' ref={this.setRef}>
{links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
</nav>
);
}

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "عرض الردود",
"home.settings": "إعدادات العمود",
"lightbox.close": "إغلاق",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "تحميل ...",
"media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "تعذر العثور عليه",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Затвори",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Зареждане...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar respostes",
"home.settings": "Ajustos de columna",
"lightbox.close": "Tancar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregant...",
"media_gallery.toggle_visible": "Alternar visibilitat",
"missing_indicator.label": "No trobat",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Antworten anzeigen",
"home.settings": "Spalteneinstellungen",
"lightbox.close": "Schließen",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Lade…",
"media_gallery.toggle_visible": "Sichtbarkeit einstellen",
"missing_indicator.label": "Nicht gefunden",

View File

@ -1113,6 +1113,14 @@
{
"defaultMessage": "Close",
"id": "lightbox.close"
},
{
"defaultMessage": "Previous",
"id": "lightbox.previous"
},
{
"defaultMessage": "Next",
"id": "lightbox.next"
}
],
"path": "app/javascript/mastodon/features/ui/components/media_modal.json"

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Fermi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ŝarĝanta...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Cerrar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "نمایش پاسخ‌ها",
"home.settings": "تنظیمات ستون",
"lightbox.close": "بستن",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "بارگیری...",
"media_gallery.toggle_visible": "تغییر پیدایی",
"missing_indicator.label": "پیدا نشد",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Sulje",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ladataan...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Afficher les réponses",
"home.settings": "Paramètres de la colonne",
"lightbox.close": "Fermer",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Chargement…",
"media_gallery.toggle_visible": "Modifier la visibilité",
"missing_indicator.label": "Non trouvé",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "הצגת תגובות",
"home.settings": "הגדרות טור",
"lightbox.close": "סגירה",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "טוען...",
"media_gallery.toggle_visible": "נראה\\בלתי נראה",
"missing_indicator.label": "לא נמצא",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Pokaži odgovore",
"home.settings": "Postavke Stupca",
"lightbox.close": "Zatvori",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Učitavam...",
"media_gallery.toggle_visible": "Preklopi vidljivost",
"missing_indicator.label": "Nije nađen",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Bezárás",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Betöltés...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Tampilkan balasan",
"home.settings": "Pengaturan kolom",
"lightbox.close": "Tutup",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Tunggu sebentar...",
"media_gallery.toggle_visible": "Tampil/Sembunyikan",
"missing_indicator.label": "Tidak ditemukan",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Montrar respondi",
"home.settings": "Aranji di la kolumno",
"lightbox.close": "Klozar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Kargante...",
"media_gallery.toggle_visible": "Chanjar videbleso",
"missing_indicator.label": "Ne trovita",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Mostra risposte",
"home.settings": "Impostazioni colonna",
"lightbox.close": "Chiudi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carico...",
"media_gallery.toggle_visible": "Imposta visibilità",
"missing_indicator.label": "Non trovato",

View File

@ -13,7 +13,7 @@
"account.posts": "投稿",
"account.report": "通報",
"account.requested": "承認待ち",
"account.share": "Share @{name}'s profile",
"account.share": "@{name} のプロフィールを共有する",
"account.unblock": "ブロック解除",
"account.unblock_domain": "{domain}を表示",
"account.unfollow": "フォロー解除",
@ -36,8 +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.moveLeft_settings": "カラムを左に移動する",
"column_header.moveRight_settings": "カラムを右に移動する",
"column_header.pin": "ピン留めする",
"column_header.show_settings": "設定を表示",
"column_header.unpin": "ピン留めを外す",
@ -94,6 +94,8 @@
"home.column_settings.show_replies": "返信表示",
"home.settings": "カラム設定",
"lightbox.close": "閉じる",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません",
@ -154,7 +156,7 @@
"reply_indicator.cancel": "キャンセル",
"report.placeholder": "コメント",
"report.submit": "通報する",
"report.target": "問題のユーザー",
"report.target": "{target} を通報する",
"search.placeholder": "検索",
"search_results.total": "{count, number}件の結果",
"standalone.public_title": "連合タイムライン",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "답글 표시",
"home.settings": "컬럼 설정",
"lightbox.close": "닫기",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "불러오는 중...",
"media_gallery.toggle_visible": "표시 전환",
"missing_indicator.label": "찾을 수 없습니다",

View File

@ -13,7 +13,7 @@
"account.posts": "Toots",
"account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring",
"account.share": "Share @{name}'s profile",
"account.share": "Profiel van @{name} delen",
"account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "{domain} niet meer negeren",
"account.unfollow": "Ontvolgen",
@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Reacties tonen",
"home.settings": "Kolom-instellingen",
"lightbox.close": "Sluiten",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Laden…",
"media_gallery.toggle_visible": "Media wel/niet tonen",
"missing_indicator.label": "Niet gevonden",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Vis svar",
"home.settings": "Kolonneinnstillinger",
"lightbox.close": "Lukk",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Laster...",
"media_gallery.toggle_visible": "Veksle synlighet",
"missing_indicator.label": "Ikke funnet",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar las responsas",
"home.settings": "Paramètres de la colomna",
"lightbox.close": "Tampar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Cargament…",
"media_gallery.toggle_visible": "Modificar la visibilitat",
"missing_indicator.label": "Pas trobat",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Pokazuj odpowiedzi",
"home.settings": "Ustawienia kolumny",
"lightbox.close": "Zamknij",
"lightbox.next": "Następne",
"lightbox.previous": "Poprzednie",
"loading_indicator.label": "Ładowanie...",
"media_gallery.toggle_visible": "Przełącz widoczność",
"missing_indicator.label": "Nie znaleziono",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem",
"lightbox.close": "Fechar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem",
"lightbox.close": "Fechar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Показывать ответы",
"home.settings": "Настройки колонки",
"lightbox.close": "Закрыть",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Загрузка...",
"media_gallery.toggle_visible": "Показать/скрыть",
"missing_indicator.label": "Не найдено",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Cevapları göster",
"home.settings": "Kolon ayarları",
"lightbox.close": "Kapat",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Yükleniyor...",
"media_gallery.toggle_visible": "Görünürlüğü değiştir",
"missing_indicator.label": "Bulunamadı",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "Показувати відповіді",
"home.settings": "Налаштування колонок",
"lightbox.close": "Закрити",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Завантаження...",
"media_gallery.toggle_visible": "Показати/приховати",
"missing_indicator.label": "Не знайдено",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "显示回应嘟文",
"home.settings": "字段设置",
"lightbox.close": "关闭",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "加载中……",
"media_gallery.toggle_visible": "打开或关上",
"missing_indicator.label": "找不到内容",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "顯示回應文章",
"home.settings": "欄位設定",
"lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容",

View File

@ -94,6 +94,8 @@
"home.column_settings.show_replies": "顯示回應",
"home.settings": "欄位設定",
"lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到",

View File

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

View File

@ -147,10 +147,15 @@
white-space: nowrap;
overflow: hidden;
a,
span {
font-weight: 400;
color: lighten($ui-base-color, 34%);
}
a {
text-decoration: none;
}
}
}
@ -309,6 +314,10 @@
margin-bottom: 0;
}
hr {
border-color: rgba($ui-base-lighter-color, .6);
}
.header {
line-height: 30px;
overflow: hidden;

View File

@ -148,13 +148,17 @@
color: $ui-base-lighter-color;
}
&.active {
color: $ui-highlight-color;
}
&.disabled {
color: $ui-primary-color;
}
&.active {
color: $ui-highlight-color;
&.disabled {
color: lighten($ui-highlight-color, 13%);
}
}
}
&.overlayed {
@ -462,6 +466,10 @@
overflow: hidden;
white-space: pre-wrap;
&:focus {
outline: rgba($ui-highlight-color, 0.7) solid 2px;
}
.emojione {
width: 18px;
height: 18px;
@ -563,6 +571,12 @@
}
}
&:focus,
&.status-direct:focus {
outline: 0;
background-color: lighten($ui-base-color, 10%);
}
&.light {
.status__relative-time {
color: $ui-primary-color;
@ -1585,6 +1599,8 @@
cursor: pointer;
flex: 0 0 auto;
font-size: 16px;
border: 0;
text-align: start;
padding: 15px;
z-index: 3;
@ -2315,6 +2331,8 @@ button.icon-button.active i.fa-retweet {
cursor: pointer;
display: flex;
flex-direction: column;
border: 0;
width: 100%;
height: 100%;
justify-content: center;
position: relative;
@ -2388,6 +2406,7 @@ button.icon-button.active i.fa-retweet {
align-items: center;
background: rgba($base-overlay-background, 0.5);
box-sizing: border-box;
border: 0;
color: $primary-text-color;
cursor: pointer;
display: flex;
@ -3480,7 +3499,7 @@ button.icon-button.active i.fa-retweet {
}
.confirmation-modal {
max-width: 280px;
max-width: 85vw;
@media screen and (min-width: 480px) {
max-width: 380px;

View File

@ -6,7 +6,7 @@ class Emoji
include Singleton
def initialize
data = Oj.load(File.open(File.join(Rails.root, 'lib', 'assets', 'emoji.json')))
data = Oj.load(File.open(Rails.root.join('lib', 'assets', 'emoji.json')))
@map = {}

View File

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

View File

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

View File

@ -135,7 +135,7 @@ class Web::PushSubscription < ApplicationRecord
end
if can_boost
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" }
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
end
actions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<%= display_name(@me) %>,
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('admin_mailer.new_report.body', target: @report.target_account.acct, reporter: @report.account.acct) %>

View File

@ -5,8 +5,8 @@
= render 'shared/error_messages', object: resource
= f.input :reset_password_token, as: :hidden
= f.input :password, autofocus: true, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password') }
= f.input :password_confirmation, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password') }
= f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
.actions
= f.button :button, t('auth.set_new_password'), type: :submit

View File

@ -5,9 +5,9 @@
= render 'shared/error_messages', object: resource
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
= f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password') }
= f.input :password_confirmation, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password') }
= f.input :current_password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password') }
= f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
= f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View File

@ -11,8 +11,8 @@
= "@#{site_hostname}"
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
= f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
= f.input :password_confirmation, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
.actions
= f.button :button, t('auth.register'), type: :submit

View File

@ -3,7 +3,7 @@
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
= f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
.actions
= f.button :button, t('auth.login'), type: :submit

View File

@ -3,7 +3,7 @@
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
= f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'),
input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt') }, required: true, autofocus: true, autocomplete: 'off',
input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true,
hint: t('simple_form.hints.sessions.otp')
.actions

View File

@ -1,4 +1,4 @@
<%= display_name(@me) %>,
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.digest.body', since: l(@since), instance: root_url) %>
<% @notifications.each do |notification| %>

View File

@ -1,4 +1,4 @@
<%= display_name(@me) %>,
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.favourite.body', name: @account.acct) %>

View File

@ -1,4 +1,4 @@
<%= display_name(@me) %>,
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.follow.body', name: @account.acct) %>

View File

@ -1,4 +1,4 @@
<%= display_name(@me) %>,
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.follow_request.body', name: @account.acct) %>

View File

@ -1,4 +1,4 @@
<%= display_name(@me) %>,
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.mention.body', name: @status.account.acct) %>

View File

@ -1,4 +1,4 @@
<%= display_name(@me) %>,
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.reblog.body', name: @account.acct) %>

View File

@ -10,7 +10,7 @@
%p.hint= t('deletes.description_html')
= f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password') }, hint: t('deletes.confirm_password')
= f.input :password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, hint: t('deletes.confirm_password')
.actions
= f.button :button, t('deletes.proceed'), type: :submit, class: 'negative'

View File

@ -11,7 +11,7 @@
%p.hint= t('two_factor_authentication.manual_instructions')
%samp.qr-alternative__code= current_user.otp_secret.scan(/.{4}/).join(' ')
= f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt')
= f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }
.actions
= f.button :button, t('two_factor_authentication.enable'), type: :submit

View File

@ -10,7 +10,7 @@
%hr/
= simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
= f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt')
= f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }
.actions
= f.button :button, t('two_factor_authentication.disable'), type: :submit

View File

@ -14,7 +14,7 @@ class Pubsubhubbub::DistributionWorker
@subscriptions = active_subscriptions.to_a
distribute_public!(stream_entries.reject(&:hidden?))
distribute_hidden!(stream_entries.reject { |s| !s.hidden? })
distribute_hidden!(stream_entries.select(&:hidden?))
end
private
@ -35,7 +35,7 @@ class Pubsubhubbub::DistributionWorker
@payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries))
@domains = @account.followers.domains
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url, s.domain) }) do |subscription|
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.select { |s| allowed_to_receive?(s.callback_url, s.domain) }) do |subscription|
[subscription.id, @payload]
end
end

View File

@ -215,6 +215,7 @@ en:
body: "%{reporter} has reported %{target}"
subject: New report for %{instance} (#%{id})
application_mailer:
salutation: '%{name},'
settings: 'Change e-mail preferences: %{link}'
signature: Mastodon notifications from %{instance}
view: 'View:'

View File

@ -5,9 +5,14 @@ ja:
about_this: 詳細情報
closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。しかし、他のインスタンスにアカウントを作成しても全く同じネットワークに参加することができます。
contact: 連絡先
contact_missing: 未設定
contact_unavailable: N/A
description_headline: "%{domain} とは?"
domain_count_after: 個のインスタンス
domain_count_before: 接続中
extended_description_html: |
<h3>ルールを書くのに適した場所</h3>
<p>詳細説明が設定されていません。</p>
features:
humane_approach_body: 他の SNS の失敗から学び、Mastodon はソーシャルメディアが誤った使い方をされることの無いように倫理的な設計を目指しています。
humane_approach_title: より思いやりのある設計
@ -104,12 +109,14 @@ ja:
hint: ドメインブロックはデータベース中のアカウント項目の作成を妨げませんが、遡って自動的に指定されたモデレーションをそれらのアカウントに適用します。
severity:
desc_html: "<strong>サイレンス</strong>はアカウントのトゥートをフォローしていない人から隠します。<strong>停止</strong>はそのアカウントのコンテンツ、メディア、プロフィールデータをすべて削除します。"
noop: なし
silence: サイレンス
suspend: 停止
title: 新規ドメインブロック
reject_media: メディアファイルを拒否
reject_media_hint: ローカルに保存されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。
severities:
noop: なし
silence: サイレンス
suspend: 停止
severity: 深刻度
@ -341,6 +348,8 @@ ja:
title: あなたのトゥートが %{name} さんにお気に入り登録されました
follow:
title: "%{name} さんにフォローされました"
group:
title: "%{count} 件の通知"
mention:
action_boost: ブースト
action_expand: もっと見る
@ -392,6 +401,8 @@ ja:
windows: Windows
windows_mobile: Windows Mobile
windows_phone: Windows Phone
revoke: 削除
revoke_success: セッションを削除しました
title: セッション
settings:
authorized_apps: 認証済みアプリ

View File

@ -37,8 +37,8 @@ nl:
type: Importtype
username: gebruikersnaam
interactions:
must_be_follower: Blokkeermeldingen van mensen die jou niet volgen
must_be_following: Blokkeermeldingen van mensen die jij niet volgt
must_be_follower: Blokkeer meldingen van mensen die jou niet volgen
must_be_following: Blokkeer meldingen van mensen die jij niet volgt
notification_emails:
digest: Verstuur periodiek e-mails met een samenvatting
favourite: Verstuur een e-mail wanneer iemand jouw toot als favoriet markeert

View File

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

View File

@ -9,7 +9,7 @@
"start": "node ./streaming/index.js",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ streaming/",
"test:mocha": "cross-env NODE_ENV=test mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.js",
"test:mocha": "cross-env NODE_ENV=test mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/**/*.test.js",
"postinstall": "npm rebuild node-sass"
},
"repository": {

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Status, type: :model do
let(:alice) { Fabricate(:account, username: 'alice') }
let(:bob) { Fabricate(:account, username: 'bob') }
let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!')}
let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!') }
subject { Fabricate(:status, account: alice) }

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'rails_helper'
describe StatusLengthValidator do
describe '#validate' do
it 'does not add errors onto remote statuses'
it 'does not add errors onto local reblogs'
it 'adds an error when content warning is over 500 characters' do
status = double(spoiler_text: 'a' * 520, text: '', errors: double(add: nil), local?: true, reblog?: false)
subject.validate(status)
expect(status.errors).to have_received(:add)
end
it 'adds an error when text is over 500 characters' do
status = double(spoiler_text: '', text: 'a' * 520, errors: double(add: nil), local?: true, reblog?: false)
subject.validate(status)
expect(status.errors).to have_received(:add)
end
it 'adds an error when text and content warning are over 500 characters total' do
status = double(spoiler_text: 'a' * 250, text: 'b' * 251, errors: double(add: nil), local?: true, reblog?: false)
subject.validate(status)
expect(status.errors).to have_received(:add)
end
it 'counts URLs as 23 characters flat' do
text = ('a' * 476) + " http://#{'b' * 30}.com/example"
status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false)
subject.validate(status)
expect(status.errors).to_not have_received(:add)
end
it 'counts only the front part of remote usernames' do
text = ('a' * 475) + " @alice@#{'b' * 30}.com"
status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false)
subject.validate(status)
expect(status.errors).to_not have_received(:add)
end
end
end