~cytrogen/masto-fe

fe00f7a7e4469d18b43be2159fa9d953d2050f46 — ThibG 6 years ago d4d4e84 + 14d855c
Merge pull request #1043 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
44 files changed, 693 insertions(+), 141 deletions(-)

M .env.production.sample
M CHANGELOG.md
M Gemfile
M Gemfile.lock
M app/controllers/statuses_controller.rb
M app/controllers/stream_entries_controller.rb
M app/javascript/flavours/glitch/containers/status_container.js
M app/javascript/flavours/glitch/features/ui/components/boost_modal.js
M app/javascript/flavours/glitch/styles/contrast/diff.scss
M app/javascript/flavours/glitch/styles/polls.scss
M app/javascript/mastodon/actions/compose.js
A app/javascript/mastodon/components/autosuggest_input.js
M app/javascript/mastodon/components/autosuggest_textarea.js
M app/javascript/mastodon/containers/status_container.js
M app/javascript/mastodon/features/account_gallery/components/media_item.js
M app/javascript/mastodon/features/compose/components/compose_form.js
M app/javascript/mastodon/features/compose/components/poll_form.js
M app/javascript/mastodon/features/compose/containers/compose_form_container.js
M app/javascript/mastodon/features/compose/containers/poll_form_container.js
M app/javascript/mastodon/features/ui/components/boost_modal.js
M app/javascript/mastodon/locales/ca.json
M app/javascript/mastodon/locales/defaultMessages.json
M app/javascript/mastodon/locales/en.json
M app/javascript/mastodon/locales/ja.json
M app/javascript/mastodon/locales/nl.json
M app/javascript/mastodon/reducers/compose.js
M app/javascript/styles/contrast/diff.scss
M app/javascript/styles/mastodon/components.scss
M app/javascript/styles/mastodon/polls.scss
M app/lib/activitypub/tag_manager.rb
M app/models/form/status_batch.rb
M app/models/tombstone.rb
M config/locales/ca.yml
M config/locales/devise.ja.yml
M config/locales/fr.yml
M config/locales/ja.yml
M config/locales/simple_form.fr.yml
M config/locales/sk.yml
A db/migrate/20190509164208_add_by_moderator_to_tombstone.rb
M db/schema.rb
M lib/mastodon/domains_cli.rb
M lib/mastodon/version.rb
M spec/lib/activitypub/tag_manager_spec.rb
M spec/services/process_mentions_service_spec.rb
M .env.production.sample => .env.production.sample +1 -0
@@ 10,6 10,7 @@ DB_NAME=postgres
DB_PASS=
DB_PORT=5432
# Optional ElasticSearch configuration
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
# ES_ENABLED=true
# ES_HOST=es
# ES_PORT=9200

M CHANGELOG.md => CHANGELOG.md +12 -0
@@ 3,6 3,18 @@ Changelog

All notable changes to this project will be documented in this file.

## [2.8.2] - 2019-05-05
### Added

- Add `SOURCE_TAG` environment variable ([ushitora-anqou](https://github.com/tootsuite/mastodon/pull/10698))

### Fixed

- Fix cropped hero image on frontpage ([BaptisteGelez](https://github.com/tootsuite/mastodon/pull/10702))
- Fix blurhash gem not compiling on some operating systems ([Gargron](https://github.com/tootsuite/mastodon/pull/10700))
- Fix unexpected CSS animations in some browsers ([ThibG](https://github.com/tootsuite/mastodon/pull/10699))
- Fix closing video modal scrolling timelines to top ([ThibG](https://github.com/tootsuite/mastodon/pull/10695))

## [2.8.1] - 2019-05-04
### Added


M Gemfile => Gemfile +1 -1
@@ 117,7 117,7 @@ group :test do
  gem 'rspec-sidekiq', '~> 3.0'
  gem 'simplecov', '~> 0.16', require: false
  gem 'webmock', '~> 3.5'
  gem 'parallel_tests', '~> 2.28'
  gem 'parallel_tests', '~> 2.29'
end

group :development do

M Gemfile.lock => Gemfile.lock +3 -3
@@ 395,7 395,7 @@ GEM
      av (~> 0.9.0)
      paperclip (>= 2.5.2)
    parallel (1.17.0)
    parallel_tests (2.28.0)
    parallel_tests (2.29.0)
      parallel
    parser (2.6.3.0)
      ast (~> 2.4.0)


@@ 480,7 480,7 @@ GEM
      link_header (~> 0.0, >= 0.0.8)
    rdf-normalize (0.3.3)
      rdf (>= 2.2, < 4.0)
    redis (4.1.0)
    redis (4.1.1)
    redis-actionpack (5.0.2)
      actionpack (>= 4.0, < 6)
      redis-rack (>= 1, < 3)


@@ 727,7 727,7 @@ DEPENDENCIES
  ox (~> 2.10)
  paperclip (~> 6.0)
  paperclip-av-transcoder (~> 0.6)
  parallel_tests (~> 2.28)
  parallel_tests (~> 2.29)
  pg (~> 1.1)
  pghero (~> 2.2)
  pkg-config (~> 1.3)

M app/controllers/statuses_controller.rb => app/controllers/statuses_controller.rb +5 -1
@@ 28,7 28,11 @@ class StatusesController < ApplicationController
    respond_to do |format|
      format.html do
        use_pack 'public'
        mark_cacheable! unless user_signed_in?

        unless user_signed_in?
          skip_session!
          expires_in 10.seconds, public: true
        end

        @body_classes = 'with-modals'


M app/controllers/stream_entries_controller.rb => app/controllers/stream_entries_controller.rb +6 -0
@@ 16,6 16,12 @@ class StreamEntriesController < ApplicationController
    respond_to do |format|
      format.html do
        use_pack 'public'

        unless user_signed_in?
          skip_session!
          expires_in 5.minutes, public: true
        end

        redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
      end


M app/javascript/flavours/glitch/containers/status_container.js => app/javascript/flavours/glitch/containers/status_container.js +8 -8
@@ 88,18 88,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
  },

  onModalReblog (status) {
    dispatch(reblog(status));
    if (status.get('reblogged')) {
      dispatch(unreblog(status));
    } else {
      dispatch(reblog(status));
    }
  },

  onReblog (status, e) {
    if (status.get('reblogged')) {
      dispatch(unreblog(status));
    if (e.shiftKey || !boostModal) {
      this.onModalReblog(status);
    } else {
      if (e.shiftKey || !boostModal) {
        this.onModalReblog(status);
      } else {
        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
      }
      dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
    }
  },


M app/javascript/flavours/glitch/features/ui/components/boost_modal.js => app/javascript/flavours/glitch/features/ui/components/boost_modal.js +3 -1
@@ 10,6 10,7 @@ import DisplayName from 'flavours/glitch/components/display_name';
import ImmutablePureComponent from 'react-immutable-pure-component';

const messages = defineMessages({
  cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
});



@@ 52,6 53,7 @@ export default class BoostModal extends ImmutablePureComponent {

  render () {
    const { status, intl } = this.props;
    const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;

    return (
      <div className='modal-root__modal boost-modal'>


@@ 77,7 79,7 @@ export default class BoostModal extends ImmutablePureComponent {

        <div className='boost-modal__action-bar'>
          <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div>
          <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} />
          <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
        </div>
      </div>
    );

M app/javascript/flavours/glitch/styles/contrast/diff.scss => app/javascript/flavours/glitch/styles/contrast/diff.scss +8 -0
@@ 67,3 67,11 @@
    text-decoration: none;
  }
}

.nothing-here {
  color: $darker-text-color;
}

.public-layout .public-account-header__tabs__tabs .counter.active::after {
  border-bottom: 4px solid $ui-highlight-color;
}

M app/javascript/flavours/glitch/styles/polls.scss => app/javascript/flavours/glitch/styles/polls.scss +6 -3
@@ 118,11 118,14 @@
    text-decoration: underline;
    font-size: inherit;

    &:hover,
    &:focus,
    &:active {
    &:hover {
      text-decoration: none;
    }

    &:active,
    &:focus {
      background-color: rgba($dark-text-color, .1);
    }
  }

  .button {

M app/javascript/mastodon/actions/compose.js => app/javascript/mastodon/actions/compose.js +2 -1
@@ 383,7 383,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
  };
};

export function selectComposeSuggestion(position, token, suggestion) {
export function selectComposeSuggestion(position, token, suggestion, path) {
  return (dispatch, getState) => {
    let completion, startPosition;



@@ 405,6 405,7 @@ export function selectComposeSuggestion(position, token, suggestion) {
      position: startPosition,
      token,
      completion,
      path,
    });
  };
};

A app/javascript/mastodon/components/autosuggest_input.js => app/javascript/mastodon/components/autosuggest_input.js +229 -0
@@ 0,0 1,229 @@
import React from 'react';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';

const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
  let word;

  let left  = str.slice(0, caretPosition).search(/\S+$/);
  let right = str.slice(caretPosition).search(/\s/);

  if (right < 0) {
    word = str.slice(left);
  } else {
    word = str.slice(left, right + caretPosition);
  }

  if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) {
    return [null, null];
  }

  word = word.trim().toLowerCase();

  if (word.length > 0) {
    return [left + 1, word];
  } else {
    return [null, null];
  }
};

export default class AutosuggestInput extends ImmutablePureComponent {

  static propTypes = {
    value: PropTypes.string,
    suggestions: ImmutablePropTypes.list,
    disabled: PropTypes.bool,
    placeholder: PropTypes.string,
    onSuggestionSelected: PropTypes.func.isRequired,
    onSuggestionsClearRequested: PropTypes.func.isRequired,
    onSuggestionsFetchRequested: PropTypes.func.isRequired,
    onChange: PropTypes.func.isRequired,
    onKeyUp: PropTypes.func,
    onKeyDown: PropTypes.func,
    autoFocus: PropTypes.bool,
    className: PropTypes.string,
    id: PropTypes.string,
    searchTokens: PropTypes.list,
    maxLength: PropTypes.number,
  };

  static defaultProps = {
    autoFocus: true,
    searchTokens: ImmutableList(['@', ':', '#']),
  };

  state = {
    suggestionsHidden: true,
    focused: false,
    selectedSuggestion: 0,
    lastToken: null,
    tokenStart: 0,
  };

  onChange = (e) => {
    const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);

    if (token !== null && this.state.lastToken !== token) {
      this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
      this.props.onSuggestionsFetchRequested(token);
    } else if (token === null) {
      this.setState({ lastToken: null });
      this.props.onSuggestionsClearRequested();
    }

    this.props.onChange(e);
  }

  onKeyDown = (e) => {
    const { suggestions, disabled } = this.props;
    const { selectedSuggestion, suggestionsHidden } = this.state;

    if (disabled) {
      e.preventDefault();
      return;
    }

    if (e.which === 229 || e.isComposing) {
      // Ignore key events during text composition
      // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
      return;
    }

    switch(e.key) {
    case 'Escape':
      if (suggestions.size === 0 || suggestionsHidden) {
        document.querySelector('.ui').parentElement.focus();
      } else {
        e.preventDefault();
        this.setState({ suggestionsHidden: true });
      }

      break;
    case 'ArrowDown':
      if (suggestions.size > 0 && !suggestionsHidden) {
        e.preventDefault();
        this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
      }

      break;
    case 'ArrowUp':
      if (suggestions.size > 0 && !suggestionsHidden) {
        e.preventDefault();
        this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
      }

      break;
    case 'Enter':
    case 'Tab':
      // Select suggestion
      if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
        e.preventDefault();
        e.stopPropagation();
        this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
      }

      break;
    }

    if (e.defaultPrevented || !this.props.onKeyDown) {
      return;
    }

    this.props.onKeyDown(e);
  }

  onBlur = () => {
    this.setState({ suggestionsHidden: true, focused: false });
  }

  onFocus = () => {
    this.setState({ focused: true });
  }

  onSuggestionClick = (e) => {
    const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
    e.preventDefault();
    this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
    this.input.focus();
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
      this.setState({ suggestionsHidden: false });
    }
  }

  setInput = (c) => {
    this.input = c;
  }

  renderSuggestion = (suggestion, i) => {
    const { selectedSuggestion } = this.state;
    let inner, key;

    if (typeof suggestion === 'object') {
      inner = <AutosuggestEmoji emoji={suggestion} />;
      key   = suggestion.id;
    } else if (suggestion[0] === '#') {
      inner = suggestion;
      key   = suggestion;
    } else {
      inner = <AutosuggestAccountContainer id={suggestion} />;
      key   = suggestion;
    }

    return (
      <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
        {inner}
      </div>
    );
  }

  render () {
    const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
    const { suggestionsHidden } = this.state;
    const style = { direction: 'ltr' };

    if (isRtl(value)) {
      style.direction = 'rtl';
    }

    return (
      <div className='autosuggest-input'>
        <label>
          <span style={{ display: 'none' }}>{placeholder}</span>

          <input
            type='text'
            ref={this.setInput}
            disabled={disabled}
            placeholder={placeholder}
            autoFocus={autoFocus}
            value={value}
            onChange={this.onChange}
            onKeyDown={this.onKeyDown}
            onKeyUp={onKeyUp}
            onFocus={this.onFocus}
            onBlur={this.onBlur}
            style={style}
            aria-autocomplete='list'
            id={id}
            className={className}
            maxLength={maxLength}
          />
        </label>

        <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
          {suggestions.map(this.renderSuggestion)}
        </div>
      </div>
    );
  }

}

M app/javascript/mastodon/components/autosuggest_textarea.js => app/javascript/mastodon/components/autosuggest_textarea.js +9 -3
@@ 55,7 55,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
  };

  state = {
    suggestionsHidden: false,
    suggestionsHidden: true,
    focused: false,
    selectedSuggestion: 0,
    lastToken: null,
    tokenStart: 0,


@@ 134,7 135,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
  }

  onBlur = () => {
    this.setState({ suggestionsHidden: true });
    this.setState({ suggestionsHidden: true, focused: false });
  }

  onFocus = () => {
    this.setState({ focused: true });
  }

  onSuggestionClick = (e) => {


@@ 145,7 150,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
      this.setState({ suggestionsHidden: false });
    }
  }


@@ 207,6 212,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
            onChange={this.onChange}
            onKeyDown={this.onKeyDown}
            onKeyUp={onKeyUp}
            onFocus={this.onFocus}
            onBlur={this.onBlur}
            onPaste={this.onPaste}
            style={style}

M app/javascript/mastodon/containers/status_container.js => app/javascript/mastodon/containers/status_container.js +8 -8
@@ 69,18 69,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
  },

  onModalReblog (status) {
    dispatch(reblog(status));
    if (status.get('reblogged')) {
      dispatch(unreblog(status));
    } else {
      dispatch(reblog(status));
    }
  },

  onReblog (status, e) {
    if (status.get('reblogged')) {
      dispatch(unreblog(status));
    if (e.shiftKey || !boostModal) {
      this.onModalReblog(status);
    } else {
      if (e.shiftKey || !boostModal) {
        this.onModalReblog(status);
      } else {
        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
      }
      dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
    }
  },


M app/javascript/mastodon/features/account_gallery/components/media_item.js => app/javascript/mastodon/features/account_gallery/components/media_item.js +13 -1
@@ 2,6 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
import classNames from 'classnames';
import { decode } from 'blurhash';


@@ 88,8 89,10 @@ export default class MediaItem extends ImmutablePureComponent {
    const width  = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
    const height = width;
    const status = attachment.get('status');
    const title = status.get('spoiler_text') || attachment.get('description');

    let thumbnail = '';
    let icon;

    if (attachment.get('type') === 'unknown') {
      // Skip


@@ 131,11 134,20 @@ export default class MediaItem extends ImmutablePureComponent {
      );
    }

    if (!visible) {
      icon = (
        <span className='account-gallery__item__icons'>
          <Icon id='eye-slash' />
        </span>
      );
    }

    return (
      <div className='account-gallery__item' style={{ width, height }}>
        <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}>
        <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
          <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
          {visible && thumbnail}
          {!visible && icon}
        </a>
      </div>
    );

M app/javascript/mastodon/features/compose/components/compose_form.js => app/javascript/mastodon/features/compose/components/compose_form.js +22 -6
@@ 5,6 5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import AutosuggestInput from '../../../components/autosuggest_input';
import PollButtonContainer from '../containers/poll_button_container';
import UploadButtonContainer from '../containers/upload_button_container';
import { defineMessages, injectIntl } from 'react-intl';


@@ 103,7 104,11 @@ class ComposeForm extends ImmutablePureComponent {
  }

  onSuggestionSelected = (tokenStart, token, value) => {
    this.props.onSuggestionSelected(tokenStart, token, value);
    this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
  }

  onSpoilerSuggestionSelected = (tokenStart, token, value) => {
    this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
  }

  handleChangeSpoilerText = (e) => {


@@ 136,7 141,7 @@ class ComposeForm extends ImmutablePureComponent {
      this.autosuggestTextarea.textarea.focus();
    } else if (this.props.spoiler !== prevProps.spoiler) {
      if (this.props.spoiler) {
        this.spoilerText.focus();
        this.spoilerText.input.focus();
      } else {
        this.autosuggestTextarea.textarea.focus();
      }


@@ 179,10 184,21 @@ class ComposeForm extends ImmutablePureComponent {
        <ReplyIndicatorContainer />

        <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
          <label>
            <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
            <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input'  id='cw-spoiler-input' ref={this.setSpoilerText} />
          </label>
          <AutosuggestInput
            placeholder={intl.formatMessage(messages.spoiler_placeholder)}
            value={this.props.spoilerText}
            onChange={this.handleChangeSpoilerText}
            onKeyDown={this.handleKeyDown}
            disabled={!this.props.spoiler}
            ref={this.setSpoilerText}
            suggestions={this.props.suggestions}
            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
            onSuggestionSelected={this.onSpoilerSuggestionSelected}
            searchTokens={[':']}
            id='cw-spoiler-input'
            className='spoiler-input__input'
          />
        </div>

        <div className='compose-form__autosuggest-wrapper'>

M app/javascript/mastodon/features/compose/components/poll_form.js => app/javascript/mastodon/features/compose/components/poll_form.js +30 -4
@@ 5,6 5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon';
import AutosuggestInput from 'mastodon/components/autosuggest_input';
import classNames from 'classnames';

const messages = defineMessages({


@@ 27,6 28,10 @@ class Option extends React.PureComponent {
    onChange: PropTypes.func.isRequired,
    onRemove: PropTypes.func.isRequired,
    onToggleMultiple: PropTypes.func.isRequired,
    suggestions: ImmutablePropTypes.list,
    onClearSuggestions: PropTypes.func.isRequired,
    onFetchSuggestions: PropTypes.func.isRequired,
    onSuggestionSelected: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };



@@ 38,12 43,25 @@ class Option extends React.PureComponent {
    this.props.onRemove(this.props.index);
  };


  handleToggleMultiple = e => {
    this.props.onToggleMultiple();
    e.preventDefault();
    e.stopPropagation();
  };

  onSuggestionsClearRequested = () => {
    this.props.onClearSuggestions();
  }

  onSuggestionsFetchRequested = (token) => {
    this.props.onFetchSuggestions(token);
  }

  onSuggestionSelected = (tokenStart, token, value) => {
    this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
  }

  render () {
    const { isPollMultiple, title, index, intl } = this.props;



@@ 57,12 75,16 @@ class Option extends React.PureComponent {
            tabIndex='0'
          />

          <input
            type='text'
          <AutosuggestInput
            placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
            maxLength={25}
            value={title}
            onChange={this.handleOptionTitleChange}
            suggestions={this.props.suggestions}
            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
            onSuggestionSelected={this.onSuggestionSelected}
            searchTokens={[':']}
          />
        </label>



@@ 87,6 109,10 @@ class PollForm extends ImmutablePureComponent {
    onAddOption: PropTypes.func.isRequired,
    onRemoveOption: PropTypes.func.isRequired,
    onChangeSettings: PropTypes.func.isRequired,
    suggestions: ImmutablePropTypes.list,
    onClearSuggestions: PropTypes.func.isRequired,
    onFetchSuggestions: PropTypes.func.isRequired,
    onSuggestionSelected: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };



@@ 103,7 129,7 @@ class PollForm extends ImmutablePureComponent {
  };

  render () {
    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;

    if (!options) {
      return null;


@@ 112,7 138,7 @@ class PollForm extends ImmutablePureComponent {
    return (
      <div className='compose-form__poll-wrapper'>
        <ul>
          {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)}
          {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)}
        </ul>

        <div className='poll__footer'>

M app/javascript/mastodon/features/compose/containers/compose_form_container.js => app/javascript/mastodon/features/compose/containers/compose_form_container.js +2 -2
@@ 45,8 45,8 @@ const mapDispatchToProps = (dispatch) => ({
    dispatch(fetchComposeSuggestions(token));
  },

  onSuggestionSelected (position, token, suggestion) {
    dispatch(selectComposeSuggestion(position, token, suggestion));
  onSuggestionSelected (position, token, suggestion, path) {
    dispatch(selectComposeSuggestion(position, token, suggestion, path));
  },

  onChangeSpoilerText (checked) {

M app/javascript/mastodon/features/compose/containers/poll_form_container.js => app/javascript/mastodon/features/compose/containers/poll_form_container.js +19 -0
@@ 1,8 1,14 @@
import { connect } from 'react-redux';
import PollForm from '../components/poll_form';
import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
import {
  clearComposeSuggestions,
  fetchComposeSuggestions,
  selectComposeSuggestion,
} from '../../../actions/compose';

const mapStateToProps = state => ({
  suggestions: state.getIn(['compose', 'suggestions']),
  options: state.getIn(['compose', 'poll', 'options']),
  expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
  isMultiple: state.getIn(['compose', 'poll', 'multiple']),


@@ 24,6 30,19 @@ const mapDispatchToProps = dispatch => ({
  onChangeSettings(expiresIn, isMultiple) {
    dispatch(changePollSettings(expiresIn, isMultiple));
  },

  onClearSuggestions () {
    dispatch(clearComposeSuggestions());
  },

  onFetchSuggestions (token) {
    dispatch(fetchComposeSuggestions(token));
  },

  onSuggestionSelected (position, token, accountId, path) {
    dispatch(selectComposeSuggestion(position, token, accountId, path));
  },

});

export default connect(mapStateToProps, mapDispatchToProps)(PollForm);

M app/javascript/mastodon/features/ui/components/boost_modal.js => app/javascript/mastodon/features/ui/components/boost_modal.js +3 -1
@@ 11,6 11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';

const messages = defineMessages({
  cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
});



@@ 51,6 52,7 @@ class BoostModal extends ImmutablePureComponent {

  render () {
    const { status, intl } = this.props;
    const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;

    return (
      <div className='modal-root__modal boost-modal'>


@@ 76,7 78,7 @@ class BoostModal extends ImmutablePureComponent {

        <div className='boost-modal__action-bar'>
          <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div>
          <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} />
          <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
        </div>
      </div>
    );

M app/javascript/mastodon/locales/ca.json => app/javascript/mastodon/locales/ca.json +5 -4
@@ 17,7 17,7 @@
  "account.hide_reblogs": "Amaga els impulsos de @{name}",
  "account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}",
  "account.locked_info": "Aquest estat de privadesa del compte està definit com a bloquejat. El propietari revisa manualment qui pot seguir-lo.",
  "account.media": "Media",
  "account.media": "Mèdia",
  "account.mention": "Esmentar @{name}",
  "account.moved_to": "{name} s'ha mogut a:",
  "account.mute": "Silencia @{name}",


@@ 77,6 77,7 @@
  "compose_form.poll.remove_option": "Elimina aquesta opció",
  "compose_form.publish": "Toot",
  "compose_form.publish_loud": "{publish}!",
  "compose_form.sensitive.hide": "Marcar mèdia com a sensible",
  "compose_form.sensitive.marked": "Mèdia marcat com a sensible",
  "compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible",
  "compose_form.spoiler.marked": "Text es ocult sota l'avís",


@@ 85,7 86,7 @@
  "confirmation_modal.cancel": "Cancel·la",
  "confirmations.block.block_and_report": "Block & Report",
  "confirmations.block.confirm": "Bloca",
  "confirmations.block.message": "Estàs segur que vols blocar {name}?",
  "confirmations.block.message": "Estàs segur que vols bloquejar a {name}?",
  "confirmations.delete.confirm": "Suprimeix",
  "confirmations.delete.message": "Estàs segur que vols suprimir aquest estat?",
  "confirmations.delete_list.confirm": "Suprimeix",


@@ 125,7 126,7 @@
  "empty_column.favourited_statuses": "Encara no tens cap toot favorit. Quan en tinguis, apareixerà aquí.",
  "empty_column.favourites": "Encara ningú ha marcat aquest toot com a favorit. Quan algú ho faci, apareixera aquí.",
  "empty_column.follow_requests": "Encara no teniu cap petició de seguiment. Quan rebeu una, apareixerà aquí.",
  "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
  "empty_column.hashtag": "Encara no hi ha res en aquesta etiqueta.",
  "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
  "empty_column.home.public_timeline": "la línia de temps pública",
  "empty_column.list": "Encara no hi ha res en aquesta llista. Quan els membres d'aquesta llista publiquin nous estats, apareixeran aquí.",


@@ 209,6 210,7 @@
  "lightbox.close": "Tancar",
  "lightbox.next": "Següent",
  "lightbox.previous": "Anterior",
  "lightbox.view_context": "Veure el context",
  "lists.account.add": "Afegir a la llista",
  "lists.account.remove": "Treure de la llista",
  "lists.delete": "Delete list",


@@ 340,7 342,6 @@
  "status.reply": "Respondre",
  "status.replyAll": "Respondre al tema",
  "status.report": "Informar sobre @{name}",
  "status.sensitive_toggle": "Clic per veure",
  "status.sensitive_warning": "Contingut sensible",
  "status.share": "Compartir",
  "status.show_less": "Mostra menys",

M app/javascript/mastodon/locales/defaultMessages.json => app/javascript/mastodon/locales/defaultMessages.json +17 -8
@@ 180,10 180,6 @@
      {
        "defaultMessage": "Media hidden",
        "id": "status.media_hidden"
      },
      {
        "defaultMessage": "Click to view",
        "id": "status.sensitive_toggle"
      }
    ],
    "path": "app/javascript/mastodon/components/media_gallery.json"


@@ 1096,6 1092,10 @@
      {
        "defaultMessage": "Media is not marked as sensitive",
        "id": "compose_form.sensitive.unmarked"
      },
      {
        "defaultMessage": "Mark media as sensitive",
        "id": "compose_form.sensitive.hide"
      }
    ],
    "path": "app/javascript/mastodon/features/compose/containers/sensitive_button_container.json"


@@ 2262,6 2262,10 @@
      {
        "defaultMessage": "Next",
        "id": "lightbox.next"
      },
      {
        "defaultMessage": "View context",
        "id": "lightbox.view_context"
      }
    ],
    "path": "app/javascript/mastodon/features/ui/components/media_modal.json"


@@ 2357,6 2361,15 @@
  {
    "descriptors": [
      {
        "defaultMessage": "View context",
        "id": "lightbox.view_context"
      }
    ],
    "path": "app/javascript/mastodon/features/ui/components/video_modal.json"
  },
  {
    "descriptors": [
      {
        "defaultMessage": "Your draft will be lost if you leave Mastodon.",
        "id": "ui.beforeunload"
      }


@@ 2408,10 2421,6 @@
      {
        "defaultMessage": "Media hidden",
        "id": "status.media_hidden"
      },
      {
        "defaultMessage": "Click to view",
        "id": "status.sensitive_toggle"
      }
    ],
    "path": "app/javascript/mastodon/features/video/index.json"

M app/javascript/mastodon/locales/en.json => app/javascript/mastodon/locales/en.json +2 -1
@@ 81,6 81,7 @@
  "compose_form.poll.remove_option": "Remove this choice",
  "compose_form.publish": "Toot",
  "compose_form.publish_loud": "{publish}!",
  "compose_form.sensitive.hide": "Mark media as sensitive",
  "compose_form.sensitive.marked": "Media is marked as sensitive",
  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
  "compose_form.spoiler.marked": "Text is hidden behind warning",


@@ 213,6 214,7 @@
  "lightbox.close": "Close",
  "lightbox.next": "Next",
  "lightbox.previous": "Previous",
  "lightbox.view_context": "View context",
  "lists.account.add": "Add to list",
  "lists.account.remove": "Remove from list",
  "lists.delete": "Delete list",


@@ 345,7 347,6 @@
  "status.reply": "Reply",
  "status.replyAll": "Reply to thread",
  "status.report": "Report @{name}",
  "status.sensitive_toggle": "Click to view",
  "status.sensitive_warning": "Sensitive content",
  "status.share": "Share",
  "status.show_less": "Show less",

M app/javascript/mastodon/locales/ja.json => app/javascript/mastodon/locales/ja.json +2 -1
@@ 81,6 81,7 @@
  "compose_form.poll.remove_option": "この項目を削除",
  "compose_form.publish": "トゥート",
  "compose_form.publish_loud": "{publish}!",
  "compose_form.sensitive.hide": "メディアを閲覧注意にする",
  "compose_form.sensitive.marked": "メディアに閲覧注意が設定されています",
  "compose_form.sensitive.unmarked": "メディアに閲覧注意が設定されていません",
  "compose_form.spoiler.marked": "閲覧注意が設定されています",


@@ 213,6 214,7 @@
  "lightbox.close": "閉じる",
  "lightbox.next": "次",
  "lightbox.previous": "前",
  "lightbox.view_context": "トゥートを表示",
  "lists.account.add": "リストに追加",
  "lists.account.remove": "リストから外す",
  "lists.delete": "リストを削除",


@@ 345,7 347,6 @@
  "status.reply": "返信",
  "status.replyAll": "全員に返信",
  "status.report": "@{name}さんを通報",
  "status.sensitive_toggle": "クリックして表示",
  "status.sensitive_warning": "閲覧注意",
  "status.share": "共有",
  "status.show_less": "隠す",

M app/javascript/mastodon/locales/nl.json => app/javascript/mastodon/locales/nl.json +1 -0
@@ 77,6 77,7 @@
  "compose_form.poll.remove_option": "Deze keuze verwijderen",
  "compose_form.publish": "Toot",
  "compose_form.publish_loud": "{publish}!",
  "compose_form.sensitive.hide": "Media als gevoelig markeren",
  "compose_form.sensitive.marked": "Media is als gevoelig gemarkeerd",
  "compose_form.sensitive.unmarked": "Media is niet als gevoelig gemarkeerd",
  "compose_form.spoiler.marked": "Tekst is achter een waarschuwing verborgen",

M app/javascript/mastodon/reducers/compose.js => app/javascript/mastodon/reducers/compose.js +8 -6
@@ 131,13 131,15 @@ function removeMedia(state, mediaId) {
  });
};

const insertSuggestion = (state, position, token, completion) => {
const insertSuggestion = (state, position, token, completion, path) => {
  return state.withMutations(map => {
    map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
    map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
    map.set('suggestion_token', null);
    map.update('suggestions', ImmutableList(), list => list.clear());
    map.set('focusDate', new Date());
    map.set('caretPosition', position + completion.length + 1);
    map.set('suggestions', ImmutableList());
    if (path.length === 1 && path[0] === 'text') {
      map.set('focusDate', new Date());
      map.set('caretPosition', position + completion.length + 1);
    }
    map.set('idempotencyKey', uuid());
  });
};


@@ 304,7 306,7 @@ export default function compose(state = initialState, action) {
  case COMPOSE_SUGGESTIONS_READY:
    return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
  case COMPOSE_SUGGESTION_SELECT:
    return insertSuggestion(state, action.position, action.token, action.completion);
    return insertSuggestion(state, action.position, action.token, action.completion, action.path);
  case COMPOSE_SUGGESTION_TAGS_UPDATE:
    return updateSuggestionTags(state, action.token);
  case COMPOSE_TAG_HISTORY_UPDATE:

M app/javascript/styles/contrast/diff.scss => app/javascript/styles/contrast/diff.scss +8 -0
@@ 67,3 67,11 @@
    text-decoration: none;
  }
}

.nothing-here {
  color: $darker-text-color;
}

.public-layout .public-account-header__tabs__tabs .counter.active::after {
  border-bottom: 4px solid $ui-highlight-color;
}

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +9 -0
@@ 319,6 319,7 @@
  }

  .autosuggest-textarea,
  .autosuggest-input,
  .spoiler-input {
    position: relative;
  }


@@ 4829,6 4830,14 @@ a.status-card.compact:hover {
  border-radius: 4px;
  overflow: hidden;
  margin: 2px;

  &__icons {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 24px;
  }
}

.notification__filter-bar,

M app/javascript/styles/mastodon/polls.scss => app/javascript/styles/mastodon/polls.scss +12 -5
@@ 37,11 37,14 @@
      display: none;
    }

    .autossugest-input {
      flex: 1 1 auto;
    }

    input[type=text] {
      display: block;
      box-sizing: border-box;
      flex: 1 1 auto;
      width: 20px;
      width: 100%;
      font-size: 14px;
      color: $inverted-text-color;
      display: block;


@@ 64,6 67,7 @@
    &.editable {
      display: flex;
      align-items: center;
      overflow: visible;
    }
  }



@@ 114,11 118,14 @@
    text-decoration: underline;
    font-size: inherit;

    &:hover,
    &:focus,
    &:active {
    &:hover {
      text-decoration: none;
    }

    &:active,
    &:focus {
      background-color: rgba($dark-text-color, .1);
    }
  }

  .button {

M app/lib/activitypub/tag_manager.rb => app/lib/activitypub/tag_manager.rb +18 -2
@@ 65,7 65,14 @@ class ActivityPub::TagManager
    when 'unlisted', 'private'
      [account_followers_url(status.account)]
    when 'direct', 'limited'
      status.active_mentions.map { |mention| uri_for(mention.account) }
      if status.account.silenced?
        # Only notify followers if the account is locally silenced
        account_ids = status.active_mentions.pluck(:account_id)
        to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) }
        to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
      else
        status.active_mentions.map { |mention| uri_for(mention.account) }
      end
    end
  end



@@ 86,7 93,16 @@ class ActivityPub::TagManager
      cc << COLLECTIONS[:public]
    end

    cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) unless status.direct_visibility? || status.limited_visibility?
    unless status.direct_visibility? || status.limited_visibility?
      if status.account.silenced?
        # Only notify followers if the account is locally silenced
        account_ids = status.active_mentions.pluck(:account_id)
        cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) })
        cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
      else
        cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) })
      end
    end

    cc
  end

M app/models/form/status_batch.rb => app/models/form/status_batch.rb +1 -0
@@ 35,6 35,7 @@ class Form::StatusBatch
  def delete_statuses
    Status.where(id: status_ids).reorder(nil).find_each do |status|
      RemovalWorker.perform_async(status.id)
      Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true)
      log_action :destroy, status
    end


M app/models/tombstone.rb => app/models/tombstone.rb +6 -5
@@ 4,11 4,12 @@
#
# Table name: tombstones
#
#  id         :bigint(8)        not null, primary key
#  account_id :bigint(8)
#  uri        :string           not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  id           :bigint(8)        not null, primary key
#  account_id   :bigint(8)
#  uri          :string           not null
#  created_at   :datetime         not null
#  updated_at   :datetime         not null
#  by_moderator :boolean
#

class Tombstone < ApplicationRecord

M config/locales/ca.yml => config/locales/ca.yml +5 -4
@@ 269,6 269,7 @@ ca:
      created_msg: El bloqueig de domini ara s'està processant
      destroyed_msg: El bloqueig de domini s'ha desfet
      domain: Domini
      existing_domain_block_html: Ja has imposat uns limits més estrictes a %{name}, l'hauries de <a href="%{unblock_url}">desbloquejar-lo</a> primer.
      new:
        create: Crea un bloqueig
        hint: El bloqueig de domini no impedirà la creació de nous comptes en la base de dades, però s'aplicaran de manera retroactiva mètodes de moderació específics sobre aquests comptes.


@@ 655,7 656,7 @@ ca:
        invalid_token: Els tokens de Keybase són hashs de signatures i han de tenir 66 caràcters hexadecimals
        verification_failed: Keybase no reconeix aquest token com a signatura del usuari de Keybase %{kb_username}. Si us plau prova des de Keybase.
      wrong_user: No es pot crear una prova per a %{proving} mentre es connectava com a %{current}. Inicia sessió com a %{proving} i prova de nou.
    explanation_html: Aquí pots connectar criptogràficament les teves altres identitats com ara el teu perfil de Keybase. Això permet que altres persones t'envïin missatges xifrats i continguts de confiança que els hi enviess.
    explanation_html: Aquí pots connectar criptogràficament les teves altres identitats com ara el teu perfil de Keybase. Això permet que altres persones t'envïin missatges xifrats i confiar en el contingut que els hi envies.
    i_am_html: Sóc %{username} a %{service}.
    identity: Identitat
    inactive: Inactiu


@@ 675,7 676,7 @@ ca:
      blocking: Llista de blocats
      domain_blocking: Llistat de dominis bloquejats
      following: Llista de seguits
      muting: Llista d'apagats
      muting: Llista de silenciats
    upload: Carregar
  in_memoriam_html: En Memòria.
  invites:


@@ 778,7 779,7 @@ ca:
  preferences:
    languages: Llengues
    other: Altre
    publishing: Publicació
    publishing: Publicant
    web: Web
  relationships:
    activity: Activitat del compte


@@ 922,7 923,7 @@ ca:
    sensitive_content: Contingut sensible
  terms:
    body_html: |
      <h2>Privacy Policy</h2>
      <h2>Política de Privacitat</h2>
      <h3 id="collect">Quina informació recollim?</h3>

      <ul>

M config/locales/devise.ja.yml => config/locales/devise.ja.yml +1 -1
@@ 12,7 12,7 @@ ja:
      last_attempt: あと1回失敗するとアカウントがロックされます。
      locked: アカウントはロックされました。
      not_found_in_database: "%{authentication_keys}かパスワードが誤っています。"
      pending: あなたのアカウントはまだ審査中です。
      pending: あなたのアカウントはまだ承認待ちです。
      timeout: セッションの有効期限が切れました。続行するには再度ログインしてください。
      unauthenticated: 続行するにはログインするか、アカウントを作成してください。
      unconfirmed: 続行するにはメールアドレスを確認する必要があります。

M config/locales/fr.yml => config/locales/fr.yml +99 -24
@@ 4,17 4,25 @@ fr:
    about_hashtag_html: Figurent ci-dessous les pouets tagués avec <strong>#%{hashtag}</strong>. Vous pouvez interagir avec eux si vous avez un compte n’importe où dans le Fediverse.
    about_mastodon_html: Mastodon est un réseau social utilisant des formats ouverts et des logiciels libres. Comme le courriel, il est décentralisé.
    about_this: À propos
    active_count_after: actif·ve·s
    active_footnote: Utilisateur·rice·s actif·ve·s mensuels (MAU)
    administered_by: 'Administrée par :'
    api: API
    apps: Applications mobiles
    apps_platforms: Utilisez Mastodon depuis iOS, Android et d’autres plates-formes
    browse_directory: Parcourir l’annuaire des profils et filtrer par centres d’intérêt
    browse_public_posts: Parcourir un flux en direct de messages publics sur Mastodon
    contact: Contact
    contact_missing: Manquant
    contact_unavailable: Non disponible
    discover_users: Découvrez des utilisateur·rice·s
    documentation: Documentation
    extended_description_html: |
      <h3>Un bon endroit pour les règles</h3>
      <p>La description étendue n’a pas été remplie.</p>
    federation_hint_html: Avec un compte sur %{instance}, vous pourrez suivre les gens sur n’importe quel serveur Mastodon et au-delà.
    generic_description: "%{domain} est seulement un serveur du réseau"
    get_apps: Essayez une application mobile
    hosted_on: Serveur Mastodon hébergée par %{domain}
    learn_more: En savoir plus
    privacy_policy: Politique de vie privée


@@ 23,7 31,8 @@ fr:
      one: Statut
      other: Statuts
    status_count_before: Ayant publié
    terms: Conditions d'utilisation
    tagline: Suivez vos ami·e·s et découvrez en de nouveaux·elles
    terms: Conditions d’utilisation
    user_count_after:
      one: utilisateur
      other: utilisateurs


@@ 113,15 122,18 @@ fr:
      moderation:
        active: Actif
        all: Tous
        pending: En cours de traitement
        silenced: Masqués
        suspended: Suspendus
        title: Modération
      moderation_notes: Notes de modération
      most_recent_activity: Dernière activité
      most_recent_ip: Adresse IP la plus récente
      no_account_selected: Aucun compte n’a été modifié, car aucun n’a été sélectionné
      no_limits_imposed: Aucune limite imposée
      not_subscribed: Non abonné
      outbox_url: URL de sortie
      pending: En attente d’approbation
      perform_full_suspension: Suspendre
      profile_url: URL du profil
      promote: Promouvoir


@@ 129,8 141,10 @@ fr:
      public: Publique
      push_subscription_expires: Expiration de l’abonnement PuSH
      redownload: Rafraîchir le profil
      reject: Rejeter
      reject_all: Tout rejeter
      remove_avatar: Supprimer l’avatar
      remove_header: Supprimer l'entête
      remove_header: Supprimer l’entête
      resend_confirmation:
        already_confirmed: Cet·te utilisateur·ice est déjà confirmé·e
        send: Renvoyer un courriel de confirmation


@@ 149,7 163,7 @@ fr:
      shared_inbox_url: URL de la boite de réception partagée
      show:
        created_reports: Signalements faits
        targeted_reports: Signalés par d'autres
        targeted_reports: Signalés par d’autres
      silence: Masquer
      silenced: Silencié
      statuses: Statuts


@@ 173,7 187,7 @@ fr:
        create_domain_block: "%{name} a bloqué le domaine %{target}"
        create_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste noire"
        demote_user: "%{name} a rétrogradé l’utilisateur·ice %{target}"
        destroy_custom_emoji: "%{name} a détruit l'émoticône %{target}"
        destroy_custom_emoji: "%{name} a détruit l’émoticône %{target}"
        destroy_domain_block: "%{name} a débloqué le domaine %{target}"
        destroy_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste blanche"
        destroy_status: "%{name} a enlevé le statut de %{target}"


@@ 230,6 244,7 @@ fr:
      feature_profile_directory: Annuaire des profils
      feature_registrations: Inscriptions
      feature_relay: Relais de fédération
      feature_timeline_preview: Aperçu du fil public
      features: Fonctionnalités
      hidden_service: Fédération avec des services cachés
      open_reports: signalements non résolus


@@ 249,6 264,7 @@ fr:
      created_msg: Le blocage de domaine est désormais activé
      destroyed_msg: Le blocage de domaine a été désactivé
      domain: Domaine
      existing_domain_block_html: Vous avez déjà imposé des limites plus strictes à %{name}, vous devez d’abord le <a href="%{unblock_url}">débloquer</a>.
      new:
        create: Créer le blocage
        hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes.


@@ 314,6 330,8 @@ fr:
        expired: Expiré
        title: Filtre
      title: Invitations
    pending_accounts:
      title: Comptes en attente (%{count})
    relays:
      add_new: Ajouter un nouveau relais
      delete: Effacer


@@ 324,7 342,7 @@ fr:
      enable_hint: Une fois activé, votre serveur souscrira à tous les pouets publics présents sur ce relais et y enverra ses propres pouets publics.
      enabled: Activé
      inbox_url: URL de relais
      pending: En attente de l'approbation du relai
      pending: En attente de l’approbation du relai
      save_and_enable: Sauvegarder et activer
      setup: Paramétrer une connexion de relais
      status: Statut


@@ 373,13 391,13 @@ fr:
        email: Entrez une adresse courriel publique
        username: Entrez un nom d’utilisateur⋅ice
      custom_css:
        desc_html: Modifier l'apparence avec une CSS chargée sur chaque page
        desc_html: Modifier l’apparence avec une CSS chargée sur chaque page
        title: CSS personnalisé
      hero:
        desc_html: Affichée sur la page d’accueil. Au moins 600x100px recommandé. Lorsqu’elle n’est pas définie, se rabat sur la vignette du serveur
        title: Image d’en-tête
      mascot:
        desc_html: Affiché sur plusieurs pages. Au moins 293×205px recommandé. Lorsqu'il n'est pas défini, retombe à la mascotte par défaut
        desc_html: Affiché sur plusieurs pages. Au moins 293×205px recommandé. Lorsqu’il n’est pas défini, retombe à la mascotte par défaut
        title: Image de la mascotte
      peers_api_enabled:
        desc_html: Noms des domaines que ce serveur a découvert dans le fediverse


@@ 388,8 406,8 @@ fr:
        desc_html: Les liens de prévisualisation sur les autres sites web afficheront une vignette même si le média est sensible
        title: Afficher les médias sensibles dans les prévisualisations OpenGraph
      profile_directory:
        desc_html: Permettre aux utilisateurs d'être découverts
        title: Activer l'annuaire des profils
        desc_html: Permettre aux utilisateurs d’être découverts
        title: Activer l’annuaire des profils
      registrations:
        closed_message:
          desc_html: Affiché sur la page d’accueil lorsque les inscriptions sont fermées<br>Vous pouvez utiliser des balises HTML


@@ 400,6 418,12 @@ fr:
        min_invite_role:
          disabled: Personne
          title: Autoriser les invitations par
      registrations_mode:
        modes:
          approved: Approbation requise pour s’inscrire
          none: Personne ne peut s’inscrire
          open: N’importe qui peut s’inscrire
        title: Mode d’enregistrement
      show_known_fediverse_at_about_page:
        desc_html: Lorsque l’option est activée, les pouets provenant de toutes les serveurs connues sont affichés dans la prévisualisation. Sinon, seuls les pouets locaux sont affichés.
        title: Afficher le fediverse connu dans la prévisualisation du fil


@@ 410,7 434,7 @@ fr:
        desc_html: Paragraphe introductif sur la page d’accueil. Décrivez ce qui rend spécifique ce serveur Mastodon et toute autre chose importante. Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
        title: Description du serveur
      site_description_extended:
        desc_html: L'endroit idéal pour afficher votre code de conduite, les règles, les guides et autres choses qui rendent votre serveur différent. Vous pouvez utiliser des balises HTML
        desc_html: L’endroit idéal pour afficher votre code de conduite, les règles, les guides et autres choses qui rendent votre serveur différent. Vous pouvez utiliser des balises HTML
        title: Description étendue du serveur
      site_short_description:
        desc_html: Affichée dans la barre latérale et dans les méta-tags. Décrivez ce qui rend spécifique ce serveur Mastodon en un seul paragraphe. Si laissée vide, la description du serveur sera affiché par défaut.


@@ 449,19 473,22 @@ fr:
    tags:
      accounts: Comptes
      hidden: Masqué
      hide: Masquer dans l'annuaire
      hide: Masquer dans l’annuaire
      name: Hashtag
      title: Hashtags
      unhide: Afficher dans l'annuaire
      unhide: Afficher dans l’annuaire
      visible: Visible
    title: Administration
    warning_presets:
      add_new: Ajouter un nouveau
      delete: Effacer
      edit: Éditer
      edit_preset: Éditer la présélection d'attention
      title: Gérer les présélections d'attention
      edit_preset: Éditer la présélection d’avertissement
      title: Gérer les présélections d’avertissement
  admin_mailer:
    new_pending_account:
      body: Les détails du nouveau compte se trouvent ci-dessous. Vous pouvez approuver ou rejeter cette demande.
      subject: Nouveau compte à examiner sur %{instance} (%{username})
    new_report:
      body: "%{reporter} a signalé %{target}"
      body_remote: Quelqu’un de %{domain} a signalé %{target}


@@ 482,7 509,9 @@ fr:
    warning: Soyez prudent⋅e avec ces données. Ne les partagez pas !
    your_token: Votre jeton d’accès
  auth:
    apply_for_account: Demander une invitation
    change_password: Mot de passe
    checkbox_agreement_html: J’accepte les <a href="%{rules_path}" target="_blank">règles du serveur</a> et les <a href="%{terms_path}" target="_blank">conditions de service</a>
    confirm_email: Confirmer mon adresse mail
    delete_account: Supprimer le compte
    delete_account_html: Si vous désirez supprimer votre compte, vous pouvez <a href="%{path}">cliquer ici</a>. Il vous sera demandé de confirmer cette action.


@@ 498,10 527,12 @@ fr:
      cas: CAS
      saml: SAML
    register: S’inscrire
    registration_closed: "%{instance} n’accepte pas de nouveaux membres"
    resend_confirmation: Envoyer à nouveau les consignes de confirmation
    reset_password: Réinitialiser le mot de passe
    security: Sécurité
    set_new_password: Définir le nouveau mot de passe
    trouble_logging_in: Vous avez un problème pour vous connecter ?
  authorize_follow:
    already_following: Vous suivez déjà ce compte
    error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant


@@ 537,11 568,11 @@ fr:
    warning_title: Disponibilité du contenu disséminé
  directories:
    directory: Annuaire des profils
    enabled: Vous êtes actuellement listé dans l'annuaire.
    enabled_but_waiting: Vous avez choisi d'être listé dans l'annuaire, mais vous n'avez pas encore le nombre minimum de suiveurs (%{min_followers}) pour y être inscrit.
    explanation: Découvrir des utilisateurs en se basant sur leurs centres d'intérêt
    enabled: Vous êtes actuellement listé dans l’annuaire.
    enabled_but_waiting: Vous avez choisi d’être listé dans l’annuaire, mais vous n’avez pas encore le nombre minimum de suiveurs (%{min_followers}) pour y être inscrit.
    explanation: Découvrir des utilisateurs en se basant sur leurs centres d’intérêt
    explore_mastodon: Explorer %{title}
    how_to_enable: Vous n'êtes pas encore inscrit dans l'annuaire. Vous pouvez vous inscrire ci-dessous. Utilisez des hashtags dans votre texte biographique pour être listé sous des hashtags spécifiques !
    how_to_enable: Vous n’êtes pas encore inscrit dans l’annuaire. Vous pouvez vous inscrire ci-dessous. Utilisez des hashtags dans votre texte biographique pour être listé sous des hashtags spécifiques !
    people:
      one: "%{count} personne"
      other: "%{count} personne"


@@ 557,6 588,9 @@ fr:
      content: Nous sommes désolé·e·s, mais quelque chose s’est mal passé de notre côté.
      title: Cette page n’est pas correcte
    noscript_html: Pour utiliser Mastodon, veuillez activer JavaScript. Sinon, essayez l’une des <a href="%{apps_path}">applications natives</a> pour Mastodon pour votre plate-forme.
  existing_username_validator:
    not_found: n’a pas trouvé d’utilisateur·rice local·e avec ce nom
    not_found_multiple: n’a pas trouvé %{usernames}
  exports:
    archive_takeout:
      date: Date


@@ 597,19 631,41 @@ fr:
    more: Davantage…
    resources: Ressources
  generic:
    all: Tous
    changes_saved_msg: Les modifications ont été enregistrées avec succès !
    copy: Copier
    order_by: Classer par
    save_changes: Enregistrer les modifications
    validation_errors:
      one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous
      other: Certaines choses ne vont pas ! Vérifiez les %{count} erreurs ci-dessous
  html_validator:
    invalid_markup: 'contient un balisage HTML invalide: %{error}'
  identity_proofs:
    active: Actif
    authorize: Oui, autoriser
    authorize_connection_prompt: Autoriser cette connexion chiffrée ?
    errors:
      failed: La connexion chiffrée a échoué. Veuillez réessayer à partir de %{provider}.
      keybase:
        invalid_token: Les jetons Keybase sont des hachages de signatures et doivent comporter 66 caractères hexadécimaux
        verification_failed: Keybase ne reconnaît pas ce jeton comme une signature de l’utilisateur Keybase %{kb_username}. Veuillez réessayer à partir de Keybase.
      wrong_user: Impossible de créer une preuve pour %{proving} lorsque vous êtes connecté en tant que %{current}. Connectez-vous en tant que %{proving} et réessayez.
    explanation_html: Ici, vous pouvez connecter de manière chiffrée vos autres identités, par exemple un profil Keybase. Cela permet à d’autres personnes de vous envoyer des messages chiffrés et de faire confiance au contenu que vous leur envoyez.
    i_am_html: Je suis %{username} sur %{service}.
    identity: Identité
    inactive: Inactif
    publicize_checkbox: 'Et le poueter:'
    publicize_toot: 'C’est prouvé ! Je suis %{username} sur %{service}: %{url}'
    status: Statut de vérification
    view_proof: Voir la preuve
  imports:
    modes:
      merge: Fusionner
      merge_long: Garder les enregistrements existants et ajouter les nouveaux
      overwrite: Réécrire
      overwrite_long: Remplacer les enregistrements actuels par les nouveaux
    preface: Vous pouvez importer certaines données que vous avez exporté d'un autre serveur, comme une liste des personnes que vous suivez ou bloquez sur votre compte.
    preface: Vous pouvez importer certaines données que vous avez exporté d’un autre serveur, comme une liste des personnes que vous suivez ou bloquez sur votre compte.
    success: Vos données ont été importées avec succès et seront traitées en temps et en heure
    types:
      blocking: Liste d’utilisateur⋅ice⋅s bloqué⋅e⋅s


@@ 713,13 769,26 @@ fr:
      duration_too_short: est trop tôt
      expired: Ce sondage est déjà terminé
      over_character_limit: ne peuvent être plus long que %{max} caractères chacun
      too_few_options: doit avoir plus qu'une proposition
      too_few_options: doit avoir plus qu’une proposition
      too_many_options: ne peut contenir plus que %{max} propositions
  preferences:
    languages: Langues
    other: Autre
    publishing: Publication
    web: Web
  relationships:
    activity: Activité du compte
    dormant: Dormant
    last_active: Dernière activité
    most_recent: Plus récent
    moved: Déménagé
    mutual: Mutuel
    primary: Primaire
    relationship: Relation
    remove_selected_domains: Supprimer tous les abonné·e·s des domaines sélectionnés
    remove_selected_followers: Supprimer les abonné·e·s sélectionnés
    remove_selected_follows: Cesser de suivre les utilisateur·rice·s sélectionné·e·s
    status: Statut du compte
  remote_follow:
    acct: Entrez l’adresse profil@serveur depuis laquelle vous voulez vous abonner
    missing_resource: L’URL de redirection n’a pas pu être trouvée


@@ 729,7 798,7 @@ fr:
    reason_html: "<strong>Pourquoi cette étape est-elle nécessaire?</strong> <code>%{instance}</code> pourrait ne pas être le serveur où vous vous êtes inscrit, et nous devons donc vous rediriger vers votre serveur de base en premier."
  remote_interaction:
    favourite:
      proceed: Confirmer l'ajout aux favoris
      proceed: Confirmer l’ajout aux favoris
      prompt: 'Vous souhaitez mettre ce pouet en favori :'
    reblog:
      proceed: Confirmer le repartage


@@ 787,6 856,9 @@ fr:
    revoke_success: Session révoquée avec succès
    title: Sessions
  settings:
    account: Compte
    account_settings: Paramètres du compte
    appearance: Apparence
    authorized_apps: Applications autorisées
    back: Retour vers Mastodon
    delete: Suppression de compte


@@ 794,10 866,13 @@ fr:
    edit_profile: Modifier le profil
    export: Export de données
    featured_tags: Hashtags mis en avant
    identity_proofs: Preuves d’identité
    import: Import de données
    migrate: Migration de compte
    notifications: Notifications
    preferences: Préférences
    profile: Profil
    relationships: Abonnements et abonné·e·s
    two_factor_authentication: Identification à deux facteurs
  statuses:
    attached:


@@ 954,8 1029,8 @@ fr:
      title: Récupération de l’archive
    warning:
      explanation:
        disable: Lorsque votre compte est gelé, les données de votre compte demeurent intactes, mais vous ne pouvez effectuer aucune action jusqu'à ce qu'il soit débloqué.
        silence: Lorsque votre compte est limité, seulement les utilisateurs qui vous suivent déjà verront vos pouets sur ce serveur, et vous pourriez être exclu de plusieurs listes publiques. Néanmoins, d'autres utilisateurs peuvent vous suivre manuellement.
        disable: Lorsque votre compte est gelé, les données de votre compte demeurent intactes, mais vous ne pouvez effectuer aucune action jusqu’à ce qu’il soit débloqué.
        silence: Lorsque votre compte est limité, seulement les utilisateurs qui vous suivent déjà verront vos pouets sur ce serveur, et vous pourriez être exclu de plusieurs listes publiques. Néanmoins, d’autres utilisateurs peuvent vous suivre manuellement.
        suspend: Votre compte a été suspendu, et tous vos pouets et vos fichiers multimédia téléversés ont été supprimés irréversiblement de ce serveur, et des serveurs où vous aviez des abonné⋅e⋅s.
      review_server_policies: Passer en revue les politiques du serveur
      subject:


@@ 993,5 1068,5 @@ fr:
    seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles.
    signed_in_as: 'Connecté·e en tant que :'
  verification:
    explanation_html: 'Vous pouvez <strong>vérifier vous-même que vous êtes le propriétaire des liens dans les métadonnées de votre profil</strong>. Pour cela, le site Web lié doit contenir un lien vers votre profil Mastodon. Le lien de retour <strong>doit</strong>avoir un attribut <code>rel="me"</code>. Le contenu textuel du lien n''a pas d''importance. En voici un exemple :'
    explanation_html: 'Vous pouvez <strong>vérifier vous-même que vous êtes le propriétaire des liens dans les métadonnées de votre profil</strong>. Pour cela, le site Web lié doit contenir un lien vers votre profil Mastodon. Le lien de retour <strong>doit</strong>avoir un attribut <code>rel="me"</code>. Le contenu textuel du lien n’a pas d’importance. En voici un exemple :'
    verification: Vérification

M config/locales/ja.yml => config/locales/ja.yml +13 -12
@@ 20,7 20,7 @@ ja:
    extended_description_html: |
      <h3>ルールを書くのに適した場所</h3>
      <p>詳細説明が設定されていません。</p>
    federation_hint_html: "%{instance} にアカウントがあればどの互換性のあるサーバーのユーザーでもフォローできるでしょう。"
    federation_hint_html: "%{instance} のアカウントひとつでどんなMastodon互換サーバーのユーザーでもフォローできるでしょう。"
    generic_description: "%{domain} は、Mastodon サーバーの一つです"
    get_apps: モバイルアプリを試す
    hosted_on: Mastodon hosted on %{domain}


@@ 269,6 269,7 @@ ja:
      created_msg: ドメインブロック処理を完了しました
      destroyed_msg: ドメインブロックを外しました
      domain: ドメイン
      existing_domain_block_html: 既に%{name}に対しより厳しい制限を課しています 。まずは<a href="%{unblock_url}">それを解除する</a>必要があります。
      new:
        create: ブロックを作成
        hint: ドメインブロックはデータベース中のアカウント項目の作成を妨げませんが、遡って自動的に指定されたモデレーションをそれらのアカウントに適用します。


@@ 660,7 661,7 @@ ja:
    i_am_html: I am %{username} on %{service}.
    identity: Identity
    inactive: 非アクティブ
    publicize_checkbox: 'そしてこれをトゥートしてください:'
    publicize_checkbox: 'そしてこれをトゥートします:'
    publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}'
    status: 認証状態
    view_proof: 証明を表示


@@ 1051,21 1052,21 @@ ja:
        suspend: アカウントが停止されました
    welcome:
      edit_profile_action: プロフィールを設定
      edit_profile_step: アバター画像やヘッダー画像をアップロードしたり、表示名やその他プロフィールを変更しカスタマイズすることができます。新しいフォロワーからのフォローを許可する前に検討したい場合、アカウントを承認制にすることができます。
      edit_profile_step: アイコンやヘッダーの画像をアップロードしたり、表示名を変更したりして、自分のプロフィールをカスタマイズすることができます。また、誰かからの新規フォローを許可する前にその人の様子を見ておきたい場合、アカウントを承認制にすることもできます。
      explanation: 始めるにあたってのアドバイスです
      final_action: 始めましょう
      final_step: 'さあ始めましょう! たとえフォロワーがいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどで誰かの目に止まるかもしれません。自己紹介をしたい時は #introductions ハッシュタグを使うといいかもしれません。'
      full_handle: あなたの正式なユーザー名
      full_handle_hint: これは別のサーバーからフォローしてもらったりメッセージのやり取りをする際に、友達に伝えるといいでしょう。
      final_step: 'さあ、始めましょう! たとえフォロワーがまだいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #introductions ハッシュタグが便利かもしれません。'
      full_handle: あなたの正式なユーザーID
      full_handle_hint: 別のサーバーの友達とフォローやメッセージをやり取りする際には、これを伝えることになります。
      review_preferences_action: 設定の変更
      review_preferences_step: 受け取りたいメールや投稿の公開範囲などの設定を必ず行ってください。不快でないならアニメーション GIF の自動再生を有効にすることもできます。
      review_preferences_step: 受け取りたいメールの種類や投稿のデフォルト公開範囲など、ユーザー設定を必ず済ませておきましょう。目が回らない自信があるなら、アニメーション GIF を自動再生する設定もご検討ください。
      subject: Mastodon へようこそ
      tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じサーバーの人がフォローしている人だけが含まれるので、それが全てではありません。
      tip_following: 標準では自動でサーバーの管理者をフォローしています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してください。
      tip_local_timeline: ローカルタイムラインは %{instance} にいる人々の流れを見られるものです。彼らはあなたと同じサーバーにいる隣人のようなものです!
      tip_mobile_webapp: もしモバイル端末のブラウザで Mastodon をホーム画面に追加できる場合、プッシュ通知を受け取ることができます。それはまるでネイティブアプリのように動作します!
      tip_federated_timeline: 連合タイムラインは、Mastodon ネットワークによる巨大流しそうめんです。ただし、あなたの「隣人」達がフォローしている人々だけが流れてくる場所なので、決してそこに全てがあるわけではありません。
      tip_following: 最初は、サーバーの管理者をフォローした状態になっています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してみましょう。
      tip_local_timeline: ローカルタイムラインには、%{instance} にいる人々が流しそうめんのごとく流れてきます。彼らはあなたと同じサーバーに暮らす、愛すべき隣人です!
      tip_mobile_webapp: お使いのモバイル端末で、ブラウザから Mastodon をホーム画面に追加できますか? もし追加できる場合、プッシュ通知の受け取りなど、まるで「普通の」アプリのような機能が楽しめます!
      tips: 豆知識
      title: ようこそ、%{name} !
      title: ようこそ、%{name}!
  users:
    follow_limit_reached: あなたは現在 %{limit} 人以上フォローできません
    invalid_email: メールアドレスが無効です

M config/locales/simple_form.fr.yml => config/locales/simple_form.fr.yml +14 -10
@@ 5,8 5,8 @@ fr:
      account_warning_preset:
        text: Vous pouvez utiliser la syntaxe des pouets, comme les URLs, les hashtags et les mentions
      admin_account_action:
        send_email_notification: L'utilisateur recevra une explication de ce qu'il s'est passé avec son compte
        text_html: Optionnel. Vous pouvez utilisez la syntaxe des pouets. Vous pouvez <a href="%{path}">ajouter des présélections d'attention</a> pour économiser du temps
        send_email_notification: L’utilisateur recevra une explication de ce qu’il s’est passé avec son compte
        text_html: Optionnel. Vous pouvez utilisez la syntaxe des pouets. Vous pouvez <a href="%{path}">ajouter des présélections d’attention</a> pour économiser du temps
        type_html: Choisir que faire avec <strong>%{acct}</strong>
        warning_preset_id: Optionnel. Vous pouvez toujours ajouter un texte personnalisé à la fin de la présélection
      defaults:


@@ 15,7 15,7 @@ fr:
        bot: Ce compte exécute principalement des actions automatisées et pourrait ne pas être surveillé
        context: Un ou plusieurs contextes où le filtre devrait s’appliquer
        digest: Uniquement envoyé après une longue période d’inactivité et uniquement si vous avez reçu des messages personnels pendant votre absence
        discoverable_html: L'<a href="%{path}" target="_blank">annuaire</a> permet aux gens de trouver des comptes en se basant sur les intérêts et les activités. Nécessite au moins %{min_followers} abonnés
        discoverable_html: L’<a href="%{path}" target="_blank">annuaire</a> permet aux gens de trouver des comptes en se basant sur les intérêts et les activités. Nécessite au moins %{min_followers} abonnés
        email: Vous recevrez un courriel de confirmation
        fields: Vous pouvez avoir jusqu’à 4 éléments affichés en tant que tableau sur votre profil
        header: Au format PNG, GIF ou JPG. %{size} maximum. Sera réduit à %{dimensions}px


@@ 26,21 26,23 @@ fr:
        password: Utilisez au moins 8 caractères
        phrase: Sera trouvé sans que la case ou l’avertissement de contenu du pouet soit pris en compte
        scopes: À quelles APIs l’application sera autorisée à accéder. Si vous sélectionnez un périmètre de haut-niveau, vous n’avez pas besoin de sélectionner les individuels.
        setting_aggregate_reblogs: Ne pas afficher de nouveaux repartagés pour les pouets qui ont été récemment repartagés (n'affecte que les repartagés nouvellement reçus)
        setting_aggregate_reblogs: Ne pas afficher de nouveaux repartagés pour les pouets qui ont été récemment repartagés (n’affecte que les repartagés nouvellement reçus)
        setting_default_language: La langue de vos pouets peut être détectée automatiquement, mais ça n’est pas toujours pertinent
        setting_display_media_default: Masquer les supports marqués comme sensibles
        setting_display_media_hide_all: Toujours masquer tous les médias
        setting_display_media_show_all: Toujours afficher les médias marqués comme sensibles
        setting_hide_network: Ceux que vous suivez et ceux qui vous suivent ne seront pas affichés sur votre profil
        setting_noindex: Affecte votre profil public ainsi que vos statuts
        setting_show_application: Le nom de l'application que vous utilisez afin d'envoyer des pouets sera affiché dans la vue détaillée de ceux-ci
        setting_show_application: Le nom de l’application que vous utilisez afin d’envoyer des pouets sera affiché dans la vue détaillée de ceux-ci
        setting_theme: Affecte l’apparence de Mastodon quand vous êtes connecté·e depuis n’importe quel appareil.
        username: Votre nom d’utilisateur sera unique sur %{domain}
        whole_word: Lorsque le mot-clef ou la phrase-clef est uniquement alphanumérique, ça sera uniquement appliqué s’il correspond au mot entier
      featured_tag:
        name: 'Vous pourriez utiliser l''un d''entre eux :'
        name: 'Vous pourriez vouloir utiliser l’un d’entre eux :'
      imports:
        data: Un fichier CSV généré par un autre serveur de Mastodon
      invite_request:
        text: Cela nous aidera à considérer votre demande
      sessions:
        otp: 'Entrez le code d’authentification à deux facteurs généré par l’application de votre téléphone ou utilisez un de vos codes de récupération :'
      user:


@@ 53,7 55,7 @@ fr:
      account_warning_preset:
        text: Texte de présélection
      admin_account_action:
        send_email_notification: Notifier l'utilisateur par courriel
        send_email_notification: Notifier l’utilisateur par courriel
        text: Attention personnalisée
        type: Action
        types:


@@ 61,7 63,7 @@ fr:
          none: Ne rien faire
          silence: Silence
          suspend: Suspendre et effacer les données du compte de manière irréversible
        warning_preset_id: Utiliser un modèle d'avertissement
        warning_preset_id: Utiliser un modèle d’avertissement
      defaults:
        autofollow: Invitation à suivre votre compte
        avatar: Image de profil


@@ 72,7 74,7 @@ fr:
        context: Contextes du filtre
        current_password: Mot de passe actuel
        data: Données
        discoverable: Inscrire ce compte dans l'annuaire
        discoverable: Inscrire ce compte dans l’annuaire
        display_name: Nom public
        email: Adresse courriel
        expires_in: Expire après


@@ 103,7 105,7 @@ fr:
        setting_hide_network: Cacher votre réseau
        setting_noindex: Demander aux moteurs de recherche de ne pas indexer vos informations personnelles
        setting_reduce_motion: Réduire la vitesse des animations
        setting_show_application: Dévoiler le nom de l'application utilisée pour envoyer des pouets
        setting_show_application: Dévoiler le nom de l’application utilisée pour envoyer des pouets
        setting_system_font_ui: Utiliser la police par défaut du système
        setting_theme: Thème du site
        setting_unfollow_modal: Afficher une fenêtre de confirmation avant de vous désabonner d’un compte


@@ 118,6 120,8 @@ fr:
        must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas
        must_be_following: Masquer les notifications des personnes que vous ne suivez pas
        must_be_following_dm: Bloquer les messages directs des personnes que vous ne suivez pas
      invite_request:
        text: Pourquoi voulez-vous vous inscrire ?
      notification_emails:
        digest: Envoyer des courriels récapitulatifs
        favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statuts à ses favoris

M config/locales/sk.yml => config/locales/sk.yml +12 -9
@@ 115,7 115,7 @@ sk:
      followers: Sledujúci
      followers_url: URL adresa sledujúcich
      follows: Sledovania
      header: Hlavička
      header: Záhlavie
      inbox_url: URL adresa prijatých správ
      invited_by: Pozvaný/á užívateľom
      ip: IP adresa


@@ 138,6 138,7 @@ sk:
      moderation_notes: Moderátorské poznámky
      most_recent_activity: Posledná aktivita
      most_recent_ip: Posledná IP adresa
      no_account_selected: Nedošlo k žiadnému pozmeneniu účtov, keďže žiadne neboli vybrané
      no_limits_imposed: Nie sú stanovené žiadné obmedzenia
      not_subscribed: Neodoberá
      outbox_url: URL poslaných


@@ 152,7 153,7 @@ sk:
      reject: Zamietni
      reject_all: Zamietni všetky
      remove_avatar: Vymaž avatar
      remove_header: Vymaž hlavičku
      remove_header: Vymaž záhlavie
      resend_confirmation:
        already_confirmed: Tento užívateľ je už potvrdený
        send: Odošli potvrdzovací email znovu


@@ 319,7 320,7 @@ sk:
      by_domain: Doména
      delivery_available: Je v dosahu doručovania
      known_accounts:
        few: "%{count} známe účty"
        few: "%{count} známych účtov"
        one: "%{count} známy účet"
        other: "%{count} známe účty"
      moderation:


@@ 340,18 341,20 @@ sk:
        expired: Vypršalo
        title: Filtruj
      title: Pozvánky
    pending_accounts:
      title: Čakajúcich účtov (%{count})
    relays:
      add_new: Pridaj nový federovací mostík
      delete: Vymaž
      description_html: "<strong>Federovací mostík</strong> je prechodný server ktorý obmieňa veľké množstvá verejných príspevkov medzi tými servermi ktoré na od neho odoberajú, aj doňho prispievajú. <strong>Môže to pomôcť malým a stredným instanciám objavovať federovaný obsah</strong>, čo inak vyžaduje aby miestni užívatelia ručne následovali iných ľudí zo vzdialených instancií."
      disable: Pozastav
      disabled: Zastavené
      description_html: "<strong>Federovací mostík</strong> je prechodný server, ktorý obmieňa veľké množstvá verejných príspevkov medzi tými servermi ktoré na od neho odoberajú, aj doňho prispievajú. <strong>Môže to pomôcť malým a stredným instanciám objavovať federovaný obsah</strong>, čo inak vyžaduje aby miestni užívatelia ručne následovali iných ľudí zo vzdialených instancií."
      disable: Vypni
      disabled: Vypnutý
      enable: Povoľ
      enable_hint: Ak povolíš, tvoj server bude odoberať všetky verejné príspevky z tohto mostu, a začne posielať verejné príspevky tvojho servera na tento most.
      enabled: Povolené
      inbox_url: URL adresa mostu
      pending: Čakám na povolenie od prechodného mostu
      save_and_enable: Uložiť a povoliť
      pending: Čaká sa na povolenie od prechodného mostu
      save_and_enable: Ulož a povoľ
      setup: Nastav prepojenie s mostom
      status: Stav
      title: Mosty


@@ 390,7 393,7 @@ sk:
      updated_at: Aktualizované
    settings:
      activity_api_enabled:
        desc_html: Sčítanie lokálne publikovaných príspevkov, aktívnych užívateľov, a nových registrácii, v týždenných intervaloch
        desc_html: Sčítanie miestne uverejnených príspevkov, aktívnych užívateľov, a nových registrácii, v týždenných intervaloch
        title: Vydať hromadné štatistiky o užívateľskej aktivite
      bootstrap_timeline_accounts:
        desc_html: Ak je prezývok viacero, každú oddeľte čiarkou. Možno zadať iba miestne, odomknuté účty. Pokiaľ necháte prázdne, je to pre všetkých miestnych administrátorov.

A db/migrate/20190509164208_add_by_moderator_to_tombstone.rb => db/migrate/20190509164208_add_by_moderator_to_tombstone.rb +5 -0
@@ 0,0 1,5 @@
class AddByModeratorToTombstone < ActiveRecord::Migration[5.2]
  def change
    add_column :tombstones, :by_moderator, :boolean
  end
end

M db/schema.rb => db/schema.rb +2 -1
@@ 10,7 10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_04_20_025523) do
ActiveRecord::Schema.define(version: 2019_05_09_164208) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"


@@ 688,6 688,7 @@ ActiveRecord::Schema.define(version: 2019_04_20_025523) do
    t.string "uri", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.boolean "by_moderator"
    t.index ["account_id"], name: "index_tombstones_on_account_id"
    t.index ["uri"], name: "index_tombstones_on_uri"
  end

M lib/mastodon/domains_cli.rb => lib/mastodon/domains_cli.rb +6 -1
@@ 28,10 28,15 @@ module Mastodon
        say('.', :green, false)
      end

      DomainBlock.where(domain: domain).destroy_all
      DomainBlock.where(domain: domain).destroy_all unless options[:dry_run]

      say
      say("Removed #{removed} accounts#{dry_run}", :green)

      custom_emojis = CustomEmoji.where(domain: domain)
      custom_emojis_count = custom_emojis.count
      custom_emojis.destroy_all unless options[:dry_run]
      say("Removed #{custom_emojis_count} custom emojis", :green)
    end

    option :concurrency, type: :numeric, default: 50, aliases: [:c]

M lib/mastodon/version.rb => lib/mastodon/version.rb +1 -1
@@ 13,7 13,7 @@ module Mastodon
    end

    def patch
      1
      2
    end

    def pre

M spec/lib/activitypub/tag_manager_spec.rb => spec/lib/activitypub/tag_manager_spec.rb +32 -0
@@ 41,6 41,22 @@ RSpec.describe ActivityPub::TagManager do
      status.mentions.create(account: mentioned)
      expect(subject.to(status)).to eq [subject.uri_for(mentioned)]
    end

    it "returns URIs of mentions for direct silenced author's status only if they are followers or requesting to be" do
      bob    = Fabricate(:account, username: 'bob')
      alice  = Fabricate(:account, username: 'alice')
      foo    = Fabricate(:account)
      author = Fabricate(:account, username: 'author', silenced: true)
      status = Fabricate(:status, visibility: :direct, account: author)
      bob.follow!(author)
      FollowRequest.create!(account: foo, target_account: author)
      status.mentions.create(account: alice)
      status.mentions.create(account: bob)
      status.mentions.create(account: foo)
      expect(subject.to(status)).to include(subject.uri_for(bob))
      expect(subject.to(status)).to include(subject.uri_for(foo))
      expect(subject.to(status)).to_not include(subject.uri_for(alice))
    end
  end

  describe '#cc' do


@@ 70,6 86,22 @@ RSpec.describe ActivityPub::TagManager do
      status.mentions.create(account: mentioned)
      expect(subject.cc(status)).to include(subject.uri_for(mentioned))
    end

    it "returns URIs of mentions for silenced author's non-direct status only if they are followers or requesting to be" do
      bob    = Fabricate(:account, username: 'bob')
      alice  = Fabricate(:account, username: 'alice')
      foo    = Fabricate(:account)
      author = Fabricate(:account, username: 'author', silenced: true)
      status = Fabricate(:status, visibility: :public, account: author)
      bob.follow!(author)
      FollowRequest.create!(account: foo, target_account: author)
      status.mentions.create(account: alice)
      status.mentions.create(account: bob)
      status.mentions.create(account: foo)
      expect(subject.cc(status)).to include(subject.uri_for(bob))
      expect(subject.cc(status)).to include(subject.uri_for(foo))
      expect(subject.cc(status)).to_not include(subject.uri_for(alice))
    end
  end

  describe '#local_uri?' do

M spec/services/process_mentions_service_spec.rb => spec/services/process_mentions_service_spec.rb +24 -3
@@ 1,10 1,11 @@
require 'rails_helper'

RSpec.describe ProcessMentionsService, type: :service do
  let(:account) { Fabricate(:account, username: 'alice') }
  let(:status)  { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") }
  let(:account)    { Fabricate(:account, username: 'alice') }
  let(:visibility) { :public }
  let(:status)     { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: visibility) }

  context 'OStatus' do
  context 'OStatus with public toot' do
    let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }

    subject { ProcessMentionsService.new }


@@ 23,6 24,26 @@ RSpec.describe ProcessMentionsService, type: :service do
    end
  end

  context 'OStatus with private toot' do
    let(:visibility)  { :private }
    let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }

    subject { ProcessMentionsService.new }

    before do
      stub_request(:post, remote_user.salmon_url)
      subject.call(status)
    end

    it 'does not create a mention' do
      expect(remote_user.mentions.where(status: status).count).to eq 0
    end

    it 'does not post to remote user\'s Salmon end point' do
      expect(a_request(:post, remote_user.salmon_url)).to_not have_been_made
    end
  end

  context 'ActivityPub' do
    let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }