Browse Source

Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- app/controllers/statuses_controller.rb
  minor conflict because of glitch-soc's theming system
- app/controllers/stream_entries_controller.rb
  minor conflict because of glitch-soc's theming system
master
Thibaut Girka 1 month ago
parent
commit
68629f2773
40 changed files with 668 additions and 129 deletions
  1. 1
    0
      .env.production.sample
  2. 12
    0
      CHANGELOG.md
  3. 1
    1
      Gemfile
  4. 3
    3
      Gemfile.lock
  5. 5
    1
      app/controllers/statuses_controller.rb
  6. 6
    0
      app/controllers/stream_entries_controller.rb
  7. 2
    1
      app/javascript/mastodon/actions/compose.js
  8. 229
    0
      app/javascript/mastodon/components/autosuggest_input.js
  9. 9
    3
      app/javascript/mastodon/components/autosuggest_textarea.js
  10. 8
    8
      app/javascript/mastodon/containers/status_container.js
  11. 13
    1
      app/javascript/mastodon/features/account_gallery/components/media_item.js
  12. 22
    6
      app/javascript/mastodon/features/compose/components/compose_form.js
  13. 30
    4
      app/javascript/mastodon/features/compose/components/poll_form.js
  14. 2
    2
      app/javascript/mastodon/features/compose/containers/compose_form_container.js
  15. 19
    0
      app/javascript/mastodon/features/compose/containers/poll_form_container.js
  16. 3
    1
      app/javascript/mastodon/features/ui/components/boost_modal.js
  17. 5
    4
      app/javascript/mastodon/locales/ca.json
  18. 17
    8
      app/javascript/mastodon/locales/defaultMessages.json
  19. 2
    1
      app/javascript/mastodon/locales/en.json
  20. 2
    1
      app/javascript/mastodon/locales/ja.json
  21. 1
    0
      app/javascript/mastodon/locales/nl.json
  22. 8
    6
      app/javascript/mastodon/reducers/compose.js
  23. 8
    0
      app/javascript/styles/contrast/diff.scss
  24. 9
    0
      app/javascript/styles/mastodon/components.scss
  25. 12
    5
      app/javascript/styles/mastodon/polls.scss
  26. 18
    2
      app/lib/activitypub/tag_manager.rb
  27. 1
    0
      app/models/form/status_batch.rb
  28. 6
    5
      app/models/tombstone.rb
  29. 5
    4
      config/locales/ca.yml
  30. 1
    1
      config/locales/devise.ja.yml
  31. 99
    24
      config/locales/fr.yml
  32. 13
    12
      config/locales/ja.yml
  33. 14
    10
      config/locales/simple_form.fr.yml
  34. 12
    9
      config/locales/sk.yml
  35. 5
    0
      db/migrate/20190509164208_add_by_moderator_to_tombstone.rb
  36. 2
    1
      db/schema.rb
  37. 6
    1
      lib/mastodon/domains_cli.rb
  38. 1
    1
      lib/mastodon/version.rb
  39. 32
    0
      spec/lib/activitypub/tag_manager_spec.rb
  40. 24
    3
      spec/services/process_mentions_service_spec.rb

+ 1
- 0
.env.production.sample View File

@@ -10,6 +10,7 @@ DB_NAME=postgres
10 10
 DB_PASS=
11 11
 DB_PORT=5432
12 12
 # Optional ElasticSearch configuration
13
+# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
13 14
 # ES_ENABLED=true
14 15
 # ES_HOST=es
15 16
 # ES_PORT=9200

+ 12
- 0
CHANGELOG.md View File

@@ -3,6 +3,18 @@ Changelog
3 3
 
4 4
 All notable changes to this project will be documented in this file.
5 5
 
6
+## [2.8.2] - 2019-05-05
7
+### Added
8
+
9
+- Add `SOURCE_TAG` environment variable ([ushitora-anqou](https://github.com/tootsuite/mastodon/pull/10698))
10
+
11
+### Fixed
12
+
13
+- Fix cropped hero image on frontpage ([BaptisteGelez](https://github.com/tootsuite/mastodon/pull/10702))
14
+- Fix blurhash gem not compiling on some operating systems ([Gargron](https://github.com/tootsuite/mastodon/pull/10700))
15
+- Fix unexpected CSS animations in some browsers ([ThibG](https://github.com/tootsuite/mastodon/pull/10699))
16
+- Fix closing video modal scrolling timelines to top ([ThibG](https://github.com/tootsuite/mastodon/pull/10695))
17
+
6 18
 ## [2.8.1] - 2019-05-04
7 19
 ### Added
8 20
 

+ 1
- 1
Gemfile View File

@@ -117,7 +117,7 @@ group :test do
117 117
   gem 'rspec-sidekiq', '~> 3.0'
118 118
   gem 'simplecov', '~> 0.16', require: false
119 119
   gem 'webmock', '~> 3.5'
120
-  gem 'parallel_tests', '~> 2.28'
120
+  gem 'parallel_tests', '~> 2.29'
121 121
 end
122 122
 
123 123
 group :development do

+ 3
- 3
Gemfile.lock View File

@@ -395,7 +395,7 @@ GEM
395 395
       av (~> 0.9.0)
396 396
       paperclip (>= 2.5.2)
397 397
     parallel (1.17.0)
398
-    parallel_tests (2.28.0)
398
+    parallel_tests (2.29.0)
399 399
       parallel
400 400
     parser (2.6.3.0)
401 401
       ast (~> 2.4.0)
@@ -480,7 +480,7 @@ GEM
480 480
       link_header (~> 0.0, >= 0.0.8)
481 481
     rdf-normalize (0.3.3)
482 482
       rdf (>= 2.2, < 4.0)
483
-    redis (4.1.0)
483
+    redis (4.1.1)
484 484
     redis-actionpack (5.0.2)
485 485
       actionpack (>= 4.0, < 6)
486 486
       redis-rack (>= 1, < 3)
@@ -727,7 +727,7 @@ DEPENDENCIES
727 727
   ox (~> 2.10)
728 728
   paperclip (~> 6.0)
729 729
   paperclip-av-transcoder (~> 0.6)
730
-  parallel_tests (~> 2.28)
730
+  parallel_tests (~> 2.29)
731 731
   pg (~> 1.1)
732 732
   pghero (~> 2.2)
733 733
   pkg-config (~> 1.3)

+ 5
- 1
app/controllers/statuses_controller.rb View File

@@ -28,7 +28,11 @@ class StatusesController < ApplicationController
28 28
     respond_to do |format|
29 29
       format.html do
30 30
         use_pack 'public'
31
-        mark_cacheable! unless user_signed_in?
31
+
32
+        unless user_signed_in?
33
+          skip_session!
34
+          expires_in 10.seconds, public: true
35
+        end
32 36
 
33 37
         @body_classes = 'with-modals'
34 38
 

+ 6
- 0
app/controllers/stream_entries_controller.rb View File

@@ -16,6 +16,12 @@ class StreamEntriesController < ApplicationController
16 16
     respond_to do |format|
17 17
       format.html do
18 18
         use_pack 'public'
19
+
20
+        unless user_signed_in?
21
+          skip_session!
22
+          expires_in 5.minutes, public: true
23
+        end
24
+
19 25
         redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
20 26
       end
21 27
 

+ 2
- 1
app/javascript/mastodon/actions/compose.js View File

@@ -383,7 +383,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
383 383
   };
384 384
 };
385 385
 
386
-export function selectComposeSuggestion(position, token, suggestion) {
386
+export function selectComposeSuggestion(position, token, suggestion, path) {
387 387
   return (dispatch, getState) => {
388 388
     let completion, startPosition;
389 389
 
@@ -405,6 +405,7 @@ export function selectComposeSuggestion(position, token, suggestion) {
405 405
       position: startPosition,
406 406
       token,
407 407
       completion,
408
+      path,
408 409
     });
409 410
   };
410 411
 };

+ 229
- 0
app/javascript/mastodon/components/autosuggest_input.js View File

@@ -0,0 +1,229 @@
1
+import React from 'react';
2
+import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
3
+import AutosuggestEmoji from './autosuggest_emoji';
4
+import ImmutablePropTypes from 'react-immutable-proptypes';
5
+import PropTypes from 'prop-types';
6
+import { isRtl } from '../rtl';
7
+import ImmutablePureComponent from 'react-immutable-pure-component';
8
+import classNames from 'classnames';
9
+import { List as ImmutableList } from 'immutable';
10
+
11
+const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
12
+  let word;
13
+
14
+  let left  = str.slice(0, caretPosition).search(/\S+$/);
15
+  let right = str.slice(caretPosition).search(/\s/);
16
+
17
+  if (right < 0) {
18
+    word = str.slice(left);
19
+  } else {
20
+    word = str.slice(left, right + caretPosition);
21
+  }
22
+
23
+  if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) {
24
+    return [null, null];
25
+  }
26
+
27
+  word = word.trim().toLowerCase();
28
+
29
+  if (word.length > 0) {
30
+    return [left + 1, word];
31
+  } else {
32
+    return [null, null];
33
+  }
34
+};
35
+
36
+export default class AutosuggestInput extends ImmutablePureComponent {
37
+
38
+  static propTypes = {
39
+    value: PropTypes.string,
40
+    suggestions: ImmutablePropTypes.list,
41
+    disabled: PropTypes.bool,
42
+    placeholder: PropTypes.string,
43
+    onSuggestionSelected: PropTypes.func.isRequired,
44
+    onSuggestionsClearRequested: PropTypes.func.isRequired,
45
+    onSuggestionsFetchRequested: PropTypes.func.isRequired,
46
+    onChange: PropTypes.func.isRequired,
47
+    onKeyUp: PropTypes.func,
48
+    onKeyDown: PropTypes.func,
49
+    autoFocus: PropTypes.bool,
50
+    className: PropTypes.string,
51
+    id: PropTypes.string,
52
+    searchTokens: PropTypes.list,
53
+    maxLength: PropTypes.number,
54
+  };
55
+
56
+  static defaultProps = {
57
+    autoFocus: true,
58
+    searchTokens: ImmutableList(['@', ':', '#']),
59
+  };
60
+
61
+  state = {
62
+    suggestionsHidden: true,
63
+    focused: false,
64
+    selectedSuggestion: 0,
65
+    lastToken: null,
66
+    tokenStart: 0,
67
+  };
68
+
69
+  onChange = (e) => {
70
+    const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
71
+
72
+    if (token !== null && this.state.lastToken !== token) {
73
+      this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
74
+      this.props.onSuggestionsFetchRequested(token);
75
+    } else if (token === null) {
76
+      this.setState({ lastToken: null });
77
+      this.props.onSuggestionsClearRequested();
78
+    }
79
+
80
+    this.props.onChange(e);
81
+  }
82
+
83
+  onKeyDown = (e) => {
84
+    const { suggestions, disabled } = this.props;
85
+    const { selectedSuggestion, suggestionsHidden } = this.state;
86
+
87
+    if (disabled) {
88
+      e.preventDefault();
89
+      return;
90
+    }
91
+
92
+    if (e.which === 229 || e.isComposing) {
93
+      // Ignore key events during text composition
94
+      // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
95
+      return;
96
+    }
97
+
98
+    switch(e.key) {
99
+    case 'Escape':
100
+      if (suggestions.size === 0 || suggestionsHidden) {
101
+        document.querySelector('.ui').parentElement.focus();
102
+      } else {
103
+        e.preventDefault();
104
+        this.setState({ suggestionsHidden: true });
105
+      }
106
+
107
+      break;
108
+    case 'ArrowDown':
109
+      if (suggestions.size > 0 && !suggestionsHidden) {
110
+        e.preventDefault();
111
+        this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
112
+      }
113
+
114
+      break;
115
+    case 'ArrowUp':
116
+      if (suggestions.size > 0 && !suggestionsHidden) {
117
+        e.preventDefault();
118
+        this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
119
+      }
120
+
121
+      break;
122
+    case 'Enter':
123
+    case 'Tab':
124
+      // Select suggestion
125
+      if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
126
+        e.preventDefault();
127
+        e.stopPropagation();
128
+        this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
129
+      }
130
+
131
+      break;
132
+    }
133
+
134
+    if (e.defaultPrevented || !this.props.onKeyDown) {
135
+      return;
136
+    }
137
+
138
+    this.props.onKeyDown(e);
139
+  }
140
+
141
+  onBlur = () => {
142
+    this.setState({ suggestionsHidden: true, focused: false });
143
+  }
144
+
145
+  onFocus = () => {
146
+    this.setState({ focused: true });
147
+  }
148
+
149
+  onSuggestionClick = (e) => {
150
+    const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
151
+    e.preventDefault();
152
+    this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
153
+    this.input.focus();
154
+  }
155
+
156
+  componentWillReceiveProps (nextProps) {
157
+    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
158
+      this.setState({ suggestionsHidden: false });
159
+    }
160
+  }
161
+
162
+  setInput = (c) => {
163
+    this.input = c;
164
+  }
165
+
166
+  renderSuggestion = (suggestion, i) => {
167
+    const { selectedSuggestion } = this.state;
168
+    let inner, key;
169
+
170
+    if (typeof suggestion === 'object') {
171
+      inner = <AutosuggestEmoji emoji={suggestion} />;
172
+      key   = suggestion.id;
173
+    } else if (suggestion[0] === '#') {
174
+      inner = suggestion;
175
+      key   = suggestion;
176
+    } else {
177
+      inner = <AutosuggestAccountContainer id={suggestion} />;
178
+      key   = suggestion;
179
+    }
180
+
181
+    return (
182
+      <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
183
+        {inner}
184
+      </div>
185
+    );
186
+  }
187
+
188
+  render () {
189
+    const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
190
+    const { suggestionsHidden } = this.state;
191
+    const style = { direction: 'ltr' };
192
+
193
+    if (isRtl(value)) {
194
+      style.direction = 'rtl';
195
+    }
196
+
197
+    return (
198
+      <div className='autosuggest-input'>
199
+        <label>
200
+          <span style={{ display: 'none' }}>{placeholder}</span>
201
+
202
+          <input
203
+            type='text'
204
+            ref={this.setInput}
205
+            disabled={disabled}
206
+            placeholder={placeholder}
207
+            autoFocus={autoFocus}
208
+            value={value}
209
+            onChange={this.onChange}
210
+            onKeyDown={this.onKeyDown}
211
+            onKeyUp={onKeyUp}
212
+            onFocus={this.onFocus}
213
+            onBlur={this.onBlur}
214
+            style={style}
215
+            aria-autocomplete='list'
216
+            id={id}
217
+            className={className}
218
+            maxLength={maxLength}
219
+          />
220
+        </label>
221
+
222
+        <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
223
+          {suggestions.map(this.renderSuggestion)}
224
+        </div>
225
+      </div>
226
+    );
227
+  }
228
+
229
+}

+ 9
- 3
app/javascript/mastodon/components/autosuggest_textarea.js View File

@@ -55,7 +55,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
55 55
   };
56 56
 
57 57
   state = {
58
-    suggestionsHidden: false,
58
+    suggestionsHidden: true,
59
+    focused: false,
59 60
     selectedSuggestion: 0,
60 61
     lastToken: null,
61 62
     tokenStart: 0,
@@ -134,7 +135,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
134 135
   }
135 136
 
136 137
   onBlur = () => {
137
-    this.setState({ suggestionsHidden: true });
138
+    this.setState({ suggestionsHidden: true, focused: false });
139
+  }
140
+
141
+  onFocus = () => {
142
+    this.setState({ focused: true });
138 143
   }
139 144
 
140 145
   onSuggestionClick = (e) => {
@@ -145,7 +150,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
145 150
   }
146 151
 
147 152
   componentWillReceiveProps (nextProps) {
148
-    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
153
+    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
149 154
       this.setState({ suggestionsHidden: false });
150 155
     }
151 156
   }
@@ -207,6 +212,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
207 212
             onChange={this.onChange}
208 213
             onKeyDown={this.onKeyDown}
209 214
             onKeyUp={onKeyUp}
215
+            onFocus={this.onFocus}
210 216
             onBlur={this.onBlur}
211 217
             onPaste={this.onPaste}
212 218
             style={style}

+ 8
- 8
app/javascript/mastodon/containers/status_container.js View File

@@ -69,18 +69,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
69 69
   },
70 70
 
71 71
   onModalReblog (status) {
72
-    dispatch(reblog(status));
72
+    if (status.get('reblogged')) {
73
+      dispatch(unreblog(status));
74
+    } else {
75
+      dispatch(reblog(status));
76
+    }
73 77
   },
74 78
 
75 79
   onReblog (status, e) {
76
-    if (status.get('reblogged')) {
77
-      dispatch(unreblog(status));
80
+    if (e.shiftKey || !boostModal) {
81
+      this.onModalReblog(status);
78 82
     } else {
79
-      if (e.shiftKey || !boostModal) {
80
-        this.onModalReblog(status);
81
-      } else {
82
-        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
83
-      }
83
+      dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
84 84
     }
85 85
   },
86 86
 

+ 13
- 1
app/javascript/mastodon/features/account_gallery/components/media_item.js View File

@@ -2,6 +2,7 @@ import React from 'react';
2 2
 import PropTypes from 'prop-types';
3 3
 import ImmutablePropTypes from 'react-immutable-proptypes';
4 4
 import ImmutablePureComponent from 'react-immutable-pure-component';
5
+import Icon from 'mastodon/components/icon';
5 6
 import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
6 7
 import classNames from 'classnames';
7 8
 import { decode } from 'blurhash';
@@ -88,8 +89,10 @@ export default class MediaItem extends ImmutablePureComponent {
88 89
     const width  = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
89 90
     const height = width;
90 91
     const status = attachment.get('status');
92
+    const title = status.get('spoiler_text') || attachment.get('description');
91 93
 
92 94
     let thumbnail = '';
95
+    let icon;
93 96
 
94 97
     if (attachment.get('type') === 'unknown') {
95 98
       // Skip
@@ -131,11 +134,20 @@ export default class MediaItem extends ImmutablePureComponent {
131 134
       );
132 135
     }
133 136
 
137
+    if (!visible) {
138
+      icon = (
139
+        <span className='account-gallery__item__icons'>
140
+          <Icon id='eye-slash' />
141
+        </span>
142
+      );
143
+    }
144
+
134 145
     return (
135 146
       <div className='account-gallery__item' style={{ width, height }}>
136
-        <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}>
147
+        <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
137 148
           <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
138 149
           {visible && thumbnail}
150
+          {!visible && icon}
139 151
         </a>
140 152
       </div>
141 153
     );

+ 22
- 6
app/javascript/mastodon/features/compose/components/compose_form.js View File

@@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
5 5
 import PropTypes from 'prop-types';
6 6
 import ReplyIndicatorContainer from '../containers/reply_indicator_container';
7 7
 import AutosuggestTextarea from '../../../components/autosuggest_textarea';
8
+import AutosuggestInput from '../../../components/autosuggest_input';
8 9
 import PollButtonContainer from '../containers/poll_button_container';
9 10
 import UploadButtonContainer from '../containers/upload_button_container';
10 11
 import { defineMessages, injectIntl } from 'react-intl';
@@ -103,7 +104,11 @@ class ComposeForm extends ImmutablePureComponent {
103 104
   }
104 105
 
105 106
   onSuggestionSelected = (tokenStart, token, value) => {
106
-    this.props.onSuggestionSelected(tokenStart, token, value);
107
+    this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
108
+  }
109
+
110
+  onSpoilerSuggestionSelected = (tokenStart, token, value) => {
111
+    this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
107 112
   }
108 113
 
109 114
   handleChangeSpoilerText = (e) => {
@@ -136,7 +141,7 @@ class ComposeForm extends ImmutablePureComponent {
136 141
       this.autosuggestTextarea.textarea.focus();
137 142
     } else if (this.props.spoiler !== prevProps.spoiler) {
138 143
       if (this.props.spoiler) {
139
-        this.spoilerText.focus();
144
+        this.spoilerText.input.focus();
140 145
       } else {
141 146
         this.autosuggestTextarea.textarea.focus();
142 147
       }
@@ -179,10 +184,21 @@ class ComposeForm extends ImmutablePureComponent {
179 184
         <ReplyIndicatorContainer />
180 185
 
181 186
         <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
182
-          <label>
183
-            <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
184
-            <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} />
185
-          </label>
187
+          <AutosuggestInput
188
+            placeholder={intl.formatMessage(messages.spoiler_placeholder)}
189
+            value={this.props.spoilerText}
190
+            onChange={this.handleChangeSpoilerText}
191
+            onKeyDown={this.handleKeyDown}
192
+            disabled={!this.props.spoiler}
193
+            ref={this.setSpoilerText}
194
+            suggestions={this.props.suggestions}
195
+            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
196
+            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
197
+            onSuggestionSelected={this.onSpoilerSuggestionSelected}
198
+            searchTokens={[':']}
199
+            id='cw-spoiler-input'
200
+            className='spoiler-input__input'
201
+          />
186 202
         </div>
187 203
 
188 204
         <div className='compose-form__autosuggest-wrapper'>

+ 30
- 4
app/javascript/mastodon/features/compose/components/poll_form.js View File

@@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
5 5
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
6 6
 import IconButton from 'mastodon/components/icon_button';
7 7
 import Icon from 'mastodon/components/icon';
8
+import AutosuggestInput from 'mastodon/components/autosuggest_input';
8 9
 import classNames from 'classnames';
9 10
 
10 11
 const messages = defineMessages({
@@ -27,6 +28,10 @@ class Option extends React.PureComponent {
27 28
     onChange: PropTypes.func.isRequired,
28 29
     onRemove: PropTypes.func.isRequired,
29 30
     onToggleMultiple: PropTypes.func.isRequired,
31
+    suggestions: ImmutablePropTypes.list,
32
+    onClearSuggestions: PropTypes.func.isRequired,
33
+    onFetchSuggestions: PropTypes.func.isRequired,
34
+    onSuggestionSelected: PropTypes.func.isRequired,
30 35
     intl: PropTypes.object.isRequired,
31 36
   };
32 37
 
@@ -38,12 +43,25 @@ class Option extends React.PureComponent {
38 43
     this.props.onRemove(this.props.index);
39 44
   };
40 45
 
46
+
41 47
   handleToggleMultiple = e => {
42 48
     this.props.onToggleMultiple();
43 49
     e.preventDefault();
44 50
     e.stopPropagation();
45 51
   };
46 52
 
53
+  onSuggestionsClearRequested = () => {
54
+    this.props.onClearSuggestions();
55
+  }
56
+
57
+  onSuggestionsFetchRequested = (token) => {
58
+    this.props.onFetchSuggestions(token);
59
+  }
60
+
61
+  onSuggestionSelected = (tokenStart, token, value) => {
62
+    this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
63
+  }
64
+
47 65
   render () {
48 66
     const { isPollMultiple, title, index, intl } = this.props;
49 67
 
@@ -57,12 +75,16 @@ class Option extends React.PureComponent {
57 75
             tabIndex='0'
58 76
           />
59 77
 
60
-          <input
61
-            type='text'
78
+          <AutosuggestInput
62 79
             placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
63 80
             maxLength={25}
64 81
             value={title}
65 82
             onChange={this.handleOptionTitleChange}
83
+            suggestions={this.props.suggestions}
84
+            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
85
+            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
86
+            onSuggestionSelected={this.onSuggestionSelected}
87
+            searchTokens={[':']}
66 88
           />
67 89
         </label>
68 90
 
@@ -87,6 +109,10 @@ class PollForm extends ImmutablePureComponent {
87 109
     onAddOption: PropTypes.func.isRequired,
88 110
     onRemoveOption: PropTypes.func.isRequired,
89 111
     onChangeSettings: PropTypes.func.isRequired,
112
+    suggestions: ImmutablePropTypes.list,
113
+    onClearSuggestions: PropTypes.func.isRequired,
114
+    onFetchSuggestions: PropTypes.func.isRequired,
115
+    onSuggestionSelected: PropTypes.func.isRequired,
90 116
     intl: PropTypes.object.isRequired,
91 117
   };
92 118
 
@@ -103,7 +129,7 @@ class PollForm extends ImmutablePureComponent {
103 129
   };
104 130
 
105 131
   render () {
106
-    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
132
+    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
107 133
 
108 134
     if (!options) {
109 135
       return null;
@@ -112,7 +138,7 @@ class PollForm extends ImmutablePureComponent {
112 138
     return (
113 139
       <div className='compose-form__poll-wrapper'>
114 140
         <ul>
115
-          {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)}
141
+          {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)}
116 142
         </ul>
117 143
 
118 144
         <div className='poll__footer'>

+ 2
- 2
app/javascript/mastodon/features/compose/containers/compose_form_container.js View File

@@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
45 45
     dispatch(fetchComposeSuggestions(token));
46 46
   },
47 47
 
48
-  onSuggestionSelected (position, token, suggestion) {
49
-    dispatch(selectComposeSuggestion(position, token, suggestion));
48
+  onSuggestionSelected (position, token, suggestion, path) {
49
+    dispatch(selectComposeSuggestion(position, token, suggestion, path));
50 50
   },
51 51
 
52 52
   onChangeSpoilerText (checked) {

+ 19
- 0
app/javascript/mastodon/features/compose/containers/poll_form_container.js View File

@@ -1,8 +1,14 @@
1 1
 import { connect } from 'react-redux';
2 2
 import PollForm from '../components/poll_form';
3 3
 import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
4
+import {
5
+  clearComposeSuggestions,
6
+  fetchComposeSuggestions,
7
+  selectComposeSuggestion,
8
+} from '../../../actions/compose';
4 9
 
5 10
 const mapStateToProps = state => ({
11
+  suggestions: state.getIn(['compose', 'suggestions']),
6 12
   options: state.getIn(['compose', 'poll', 'options']),
7 13
   expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
8 14
   isMultiple: state.getIn(['compose', 'poll', 'multiple']),
@@ -24,6 +30,19 @@ const mapDispatchToProps = dispatch => ({
24 30
   onChangeSettings(expiresIn, isMultiple) {
25 31
     dispatch(changePollSettings(expiresIn, isMultiple));
26 32
   },
33
+
34
+  onClearSuggestions () {
35
+    dispatch(clearComposeSuggestions());
36
+  },
37
+
38
+  onFetchSuggestions (token) {
39
+    dispatch(fetchComposeSuggestions(token));
40
+  },
41
+
42
+  onSuggestionSelected (position, token, accountId, path) {
43
+    dispatch(selectComposeSuggestion(position, token, accountId, path));
44
+  },
45
+
27 46
 });
28 47
 
29 48
 export default connect(mapStateToProps, mapDispatchToProps)(PollForm);

+ 3
- 1
app/javascript/mastodon/features/ui/components/boost_modal.js View File

@@ -11,6 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
11 11
 import Icon from 'mastodon/components/icon';
12 12
 
13 13
 const messages = defineMessages({
14
+  cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
14 15
   reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
15 16
 });
16 17
 
@@ -51,6 +52,7 @@ class BoostModal extends ImmutablePureComponent {
51 52
 
52 53
   render () {
53 54
     const { status, intl } = this.props;
55
+    const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
54 56
 
55 57
     return (
56 58
       <div className='modal-root__modal boost-modal'>
@@ -76,7 +78,7 @@ class BoostModal extends ImmutablePureComponent {
76 78
 
77 79
         <div className='boost-modal__action-bar'>
78 80
           <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>
79
-          <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} />
81
+          <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
80 82
         </div>
81 83
       </div>
82 84
     );

+ 5
- 4
app/javascript/mastodon/locales/ca.json View File

@@ -17,7 +17,7 @@
17 17
   "account.hide_reblogs": "Amaga els impulsos de @{name}",
18 18
   "account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}",
19 19
   "account.locked_info": "Aquest estat de privadesa del compte està definit com a bloquejat. El propietari revisa manualment qui pot seguir-lo.",
20
-  "account.media": "Media",
20
+  "account.media": "Mèdia",
21 21
   "account.mention": "Esmentar @{name}",
22 22
   "account.moved_to": "{name} s'ha mogut a:",
23 23
   "account.mute": "Silencia @{name}",
@@ -77,6 +77,7 @@
77 77
   "compose_form.poll.remove_option": "Elimina aquesta opció",
78 78
   "compose_form.publish": "Toot",
79 79
   "compose_form.publish_loud": "{publish}!",
80
+  "compose_form.sensitive.hide": "Marcar mèdia com a sensible",
80 81
   "compose_form.sensitive.marked": "Mèdia marcat com a sensible",
81 82
   "compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible",
82 83
   "compose_form.spoiler.marked": "Text es ocult sota l'avís",
@@ -85,7 +86,7 @@
85 86
   "confirmation_modal.cancel": "Cancel·la",
86 87
   "confirmations.block.block_and_report": "Block & Report",
87 88
   "confirmations.block.confirm": "Bloca",
88
-  "confirmations.block.message": "Estàs segur que vols blocar {name}?",
89
+  "confirmations.block.message": "Estàs segur que vols bloquejar a {name}?",
89 90
   "confirmations.delete.confirm": "Suprimeix",
90 91
   "confirmations.delete.message": "Estàs segur que vols suprimir aquest estat?",
91 92
   "confirmations.delete_list.confirm": "Suprimeix",
@@ -125,7 +126,7 @@
125 126
   "empty_column.favourited_statuses": "Encara no tens cap toot favorit. Quan en tinguis, apareixerà aquí.",
126 127
   "empty_column.favourites": "Encara ningú ha marcat aquest toot com a favorit. Quan algú ho faci, apareixera aquí.",
127 128
   "empty_column.follow_requests": "Encara no teniu cap petició de seguiment. Quan rebeu una, apareixerà aquí.",
128
-  "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
129
+  "empty_column.hashtag": "Encara no hi ha res en aquesta etiqueta.",
129 130
   "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
130 131
   "empty_column.home.public_timeline": "la línia de temps pública",
131 132
   "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 @@
209 210
   "lightbox.close": "Tancar",
210 211
   "lightbox.next": "Següent",
211 212
   "lightbox.previous": "Anterior",
213
+  "lightbox.view_context": "Veure el context",
212 214
   "lists.account.add": "Afegir a la llista",
213 215
   "lists.account.remove": "Treure de la llista",
214 216
   "lists.delete": "Delete list",
@@ -340,7 +342,6 @@
340 342
   "status.reply": "Respondre",
341 343
   "status.replyAll": "Respondre al tema",
342 344
   "status.report": "Informar sobre @{name}",
343
-  "status.sensitive_toggle": "Clic per veure",
344 345
   "status.sensitive_warning": "Contingut sensible",
345 346
   "status.share": "Compartir",
346 347
   "status.show_less": "Mostra menys",

+ 17
- 8
app/javascript/mastodon/locales/defaultMessages.json View File

@@ -180,10 +180,6 @@
180 180
       {
181 181
         "defaultMessage": "Media hidden",
182 182
         "id": "status.media_hidden"
183
-      },
184
-      {
185
-        "defaultMessage": "Click to view",
186
-        "id": "status.sensitive_toggle"
187 183
       }
188 184
     ],
189 185
     "path": "app/javascript/mastodon/components/media_gallery.json"
@@ -1096,6 +1092,10 @@
1096 1092
       {
1097 1093
         "defaultMessage": "Media is not marked as sensitive",
1098 1094
         "id": "compose_form.sensitive.unmarked"
1095
+      },
1096
+      {
1097
+        "defaultMessage": "Mark media as sensitive",
1098
+        "id": "compose_form.sensitive.hide"
1099 1099
       }
1100 1100
     ],
1101 1101
     "path": "app/javascript/mastodon/features/compose/containers/sensitive_button_container.json"
@@ -2262,6 +2262,10 @@
2262 2262
       {
2263 2263
         "defaultMessage": "Next",
2264 2264
         "id": "lightbox.next"
2265
+      },
2266
+      {
2267
+        "defaultMessage": "View context",
2268
+        "id": "lightbox.view_context"
2265 2269
       }
2266 2270
     ],
2267 2271
     "path": "app/javascript/mastodon/features/ui/components/media_modal.json"
@@ -2357,6 +2361,15 @@
2357 2361
   {
2358 2362
     "descriptors": [
2359 2363
       {
2364
+        "defaultMessage": "View context",
2365
+        "id": "lightbox.view_context"
2366
+      }
2367
+    ],
2368
+    "path": "app/javascript/mastodon/features/ui/components/video_modal.json"
2369
+  },
2370
+  {
2371
+    "descriptors": [
2372
+      {
2360 2373
         "defaultMessage": "Your draft will be lost if you leave Mastodon.",
2361 2374
         "id": "ui.beforeunload"
2362 2375
       }
@@ -2408,10 +2421,6 @@
2408 2421
       {
2409 2422
         "defaultMessage": "Media hidden",
2410 2423
         "id": "status.media_hidden"
2411
-      },
2412
-      {
2413
-        "defaultMessage": "Click to view",
2414
-        "id": "status.sensitive_toggle"
2415 2424
       }
2416 2425
     ],
2417 2426
     "path": "app/javascript/mastodon/features/video/index.json"

+ 2
- 1
app/javascript/mastodon/locales/en.json View File

@@ -81,6 +81,7 @@
81 81
   "compose_form.poll.remove_option": "Remove this choice",
82 82
   "compose_form.publish": "Toot",
83 83
   "compose_form.publish_loud": "{publish}!",
84
+  "compose_form.sensitive.hide": "Mark media as sensitive",
84 85
   "compose_form.sensitive.marked": "Media is marked as sensitive",
85 86
   "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
86 87
   "compose_form.spoiler.marked": "Text is hidden behind warning",
@@ -213,6 +214,7 @@
213 214
   "lightbox.close": "Close",
214 215
   "lightbox.next": "Next",
215 216
   "lightbox.previous": "Previous",
217
+  "lightbox.view_context": "View context",
216 218
   "lists.account.add": "Add to list",
217 219
   "lists.account.remove": "Remove from list",
218 220
   "lists.delete": "Delete list",
@@ -345,7 +347,6 @@
345 347
   "status.reply": "Reply",
346 348
   "status.replyAll": "Reply to thread",
347 349
   "status.report": "Report @{name}",
348
-  "status.sensitive_toggle": "Click to view",
349 350
   "status.sensitive_warning": "Sensitive content",
350 351
   "status.share": "Share",
351 352
   "status.show_less": "Show less",

+ 2
- 1
app/javascript/mastodon/locales/ja.json View File

@@ -81,6 +81,7 @@
81 81
   "compose_form.poll.remove_option": "この項目を削除",
82 82
   "compose_form.publish": "トゥート",
83 83
   "compose_form.publish_loud": "{publish}!",
84
+  "compose_form.sensitive.hide": "メディアを閲覧注意にする",
84 85
   "compose_form.sensitive.marked": "メディアに閲覧注意が設定されています",
85 86
   "compose_form.sensitive.unmarked": "メディアに閲覧注意が設定されていません",
86 87
   "compose_form.spoiler.marked": "閲覧注意が設定されています",
@@ -213,6 +214,7 @@
213 214
   "lightbox.close": "閉じる",
214 215
   "lightbox.next": "次",
215 216
   "lightbox.previous": "前",
217
+  "lightbox.view_context": "トゥートを表示",
216 218
   "lists.account.add": "リストに追加",
217 219
   "lists.account.remove": "リストから外す",
218 220
   "lists.delete": "リストを削除",
@@ -345,7 +347,6 @@
345 347
   "status.reply": "返信",
346 348
   "status.replyAll": "全員に返信",
347 349
   "status.report": "@{name}さんを通報",
348
-  "status.sensitive_toggle": "クリックして表示",
349 350
   "status.sensitive_warning": "閲覧注意",
350 351
   "status.share": "共有",
351 352
   "status.show_less": "隠す",

+ 1
- 0
app/javascript/mastodon/locales/nl.json View File

@@ -77,6 +77,7 @@
77 77
   "compose_form.poll.remove_option": "Deze keuze verwijderen",
78 78
   "compose_form.publish": "Toot",
79 79
   "compose_form.publish_loud": "{publish}!",
80
+  "compose_form.sensitive.hide": "Media als gevoelig markeren",
80 81
   "compose_form.sensitive.marked": "Media is als gevoelig gemarkeerd",
81 82
   "compose_form.sensitive.unmarked": "Media is niet als gevoelig gemarkeerd",
82 83
   "compose_form.spoiler.marked": "Tekst is achter een waarschuwing verborgen",

+ 8
- 6
app/javascript/mastodon/reducers/compose.js View File

@@ -131,13 +131,15 @@ function removeMedia(state, mediaId) {
131 131
   });
132 132
 };
133 133
 
134
-const insertSuggestion = (state, position, token, completion) => {
134
+const insertSuggestion = (state, position, token, completion, path) => {
135 135
   return state.withMutations(map => {
136
-    map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
136
+    map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
137 137
     map.set('suggestion_token', null);
138
-    map.update('suggestions', ImmutableList(), list => list.clear());
139
-    map.set('focusDate', new Date());
140
-    map.set('caretPosition', position + completion.length + 1);
138
+    map.set('suggestions', ImmutableList());
139
+    if (path.length === 1 && path[0] === 'text') {
140
+      map.set('focusDate', new Date());
141
+      map.set('caretPosition', position + completion.length + 1);
142
+    }
141 143
     map.set('idempotencyKey', uuid());
142 144
   });
143 145
 };
@@ -304,7 +306,7 @@ export default function compose(state = initialState, action) {
304 306
   case COMPOSE_SUGGESTIONS_READY:
305 307
     return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
306 308
   case COMPOSE_SUGGESTION_SELECT:
307
-    return insertSuggestion(state, action.position, action.token, action.completion);
309
+    return insertSuggestion(state, action.position, action.token, action.completion, action.path);
308 310
   case COMPOSE_SUGGESTION_TAGS_UPDATE:
309 311
     return updateSuggestionTags(state, action.token);
310 312
   case COMPOSE_TAG_HISTORY_UPDATE:

+ 8
- 0
app/javascript/styles/contrast/diff.scss View File

@@ -67,3 +67,11 @@
67 67
     text-decoration: none;
68 68
   }
69 69
 }
70
+
71
+.nothing-here {
72
+  color: $darker-text-color;
73
+}
74
+
75
+.public-layout .public-account-header__tabs__tabs .counter.active::after {
76
+  border-bottom: 4px solid $ui-highlight-color;
77
+}

+ 9
- 0
app/javascript/styles/mastodon/components.scss View File

@@ -319,6 +319,7 @@
319 319
   }
320 320
 
321 321
   .autosuggest-textarea,
322
+  .autosuggest-input,
322 323
   .spoiler-input {
323 324
     position: relative;
324 325
   }
@@ -4829,6 +4830,14 @@ a.status-card.compact:hover {
4829 4830
   border-radius: 4px;
4830 4831
   overflow: hidden;
4831 4832
   margin: 2px;
4833
+
4834
+  &__icons {
4835
+    position: absolute;
4836
+    top: 50%;
4837
+    left: 50%;
4838
+    transform: translate(-50%, -50%);
4839
+    font-size: 24px;
4840
+  }
4832 4841
 }
4833 4842
 
4834 4843
 .notification__filter-bar,

+ 12
- 5
app/javascript/styles/mastodon/polls.scss View File

@@ -37,11 +37,14 @@
37 37
       display: none;
38 38
     }
39 39
 
40
+    .autossugest-input {
41
+      flex: 1 1 auto;
42
+    }
43
+
40 44
     input[type=text] {
41 45
       display: block;
42 46
       box-sizing: border-box;
43
-      flex: 1 1 auto;
44
-      width: 20px;
47
+      width: 100%;
45 48
       font-size: 14px;
46 49
       color: $inverted-text-color;
47 50
       display: block;
@@ -64,6 +67,7 @@
64 67
     &.editable {
65 68
       display: flex;
66 69
       align-items: center;
70
+      overflow: visible;
67 71
     }
68 72
   }
69 73
 
@@ -114,11 +118,14 @@
114 118
     text-decoration: underline;
115 119
     font-size: inherit;
116 120
 
117
-    &:hover,
118
-    &:focus,
119
-    &:active {
121
+    &:hover {
120 122
       text-decoration: none;
121 123
     }
124
+
125
+    &:active,
126
+    &:focus {
127
+      background-color: rgba($dark-text-color, .1);
128
+    }
122 129
   }
123 130
 
124 131
   .button {

+ 18
- 2
app/lib/activitypub/tag_manager.rb View File

@@ -65,7 +65,14 @@ class ActivityPub::TagManager
65 65
     when 'unlisted', 'private'
66 66
       [account_followers_url(status.account)]
67 67
     when 'direct', 'limited'
68
-      status.active_mentions.map { |mention| uri_for(mention.account) }
68
+      if status.account.silenced?
69
+        # Only notify followers if the account is locally silenced
70
+        account_ids = status.active_mentions.pluck(:account_id)
71
+        to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) }
72
+        to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
73
+      else
74
+        status.active_mentions.map { |mention| uri_for(mention.account) }
75
+      end
69 76
     end
70 77
   end
71 78
 
@@ -86,7 +93,16 @@ class ActivityPub::TagManager
86 93
       cc << COLLECTIONS[:public]
87 94
     end
88 95
 
89
-    cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) unless status.direct_visibility? || status.limited_visibility?
96
+    unless status.direct_visibility? || status.limited_visibility?
97
+      if status.account.silenced?
98
+        # Only notify followers if the account is locally silenced
99
+        account_ids = status.active_mentions.pluck(:account_id)
100
+        cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) })
101
+        cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
102
+      else
103
+        cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) })
104
+      end
105
+    end
90 106
 
91 107
     cc
92 108
   end

+ 1
- 0
app/models/form/status_batch.rb View File

@@ -35,6 +35,7 @@ class Form::StatusBatch
35 35
   def delete_statuses
36 36
     Status.where(id: status_ids).reorder(nil).find_each do |status|
37 37
       RemovalWorker.perform_async(status.id)
38
+      Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true)
38 39
       log_action :destroy, status
39 40
     end
40 41
 

+ 6
- 5
app/models/tombstone.rb View File

@@ -4,11 +4,12 @@
4 4
 #
5 5
 # Table name: tombstones
6 6
 #
7
-#  id         :bigint(8)        not null, primary key
8
-#  account_id :bigint(8)
9
-#  uri        :string           not null
10
-#  created_at :datetime         not null
11
-#  updated_at :datetime         not null
7
+#  id           :bigint(8)        not null, primary key
8
+#  account_id   :bigint(8)
9
+#  uri          :string           not null
10
+#  created_at   :datetime         not null
11
+#  updated_at   :datetime         not null
12
+#  by_moderator :boolean
12 13
 #
13 14
 
14 15
 class Tombstone < ApplicationRecord

+ 5
- 4
config/locales/ca.yml View File

@@ -269,6 +269,7 @@ ca:
269 269
       created_msg: El bloqueig de domini ara s'està processant
270 270
       destroyed_msg: El bloqueig de domini s'ha desfet
271 271
       domain: Domini
272
+      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.
272 273
       new:
273 274
         create: Crea un bloqueig
274 275
         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:
655 656
         invalid_token: Els tokens de Keybase són hashs de signatures i han de tenir 66 caràcters hexadecimals
656 657
         verification_failed: Keybase no reconeix aquest token com a signatura del usuari de Keybase %{kb_username}. Si us plau prova des de Keybase.
657 658
       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.
658
-    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.
659
+    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.
659 660
     i_am_html: Sóc %{username} a %{service}.
660 661
     identity: Identitat
661 662
     inactive: Inactiu
@@ -675,7 +676,7 @@ ca:
675 676
       blocking: Llista de blocats
676 677
       domain_blocking: Llistat de dominis bloquejats
677 678
       following: Llista de seguits
678
-      muting: Llista d'apagats
679
+      muting: Llista de silenciats
679 680
     upload: Carregar
680 681
   in_memoriam_html: En Memòria.
681 682
   invites:
@@ -778,7 +779,7 @@ ca:
778 779
   preferences:
779 780
     languages: Llengues
780 781
     other: Altre
781
-    publishing: Publicació
782
+    publishing: Publicant
782 783
     web: Web
783 784
   relationships:
784 785
     activity: Activitat del compte
@@ -922,7 +923,7 @@ ca:
922 923
     sensitive_content: Contingut sensible
923 924
   terms:
924 925
     body_html: |
925
-      <h2>Privacy Policy</h2>
926
+      <h2>Política de Privacitat</h2>
926 927
       <h3 id="collect">Quina informació recollim?</h3>
927 928
 
928 929
       <ul>

+ 1
- 1
config/locales/devise.ja.yml View File

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

+ 99
- 24
config/locales/fr.yml View File

@@ -4,17 +4,25 @@ fr:
4 4
     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.
5 5
     about_mastodon_html: Mastodon est un réseau social utilisant des formats ouverts et des logiciels libres. Comme le courriel, il est décentralisé.
6 6
     about_this: À propos
7
+    active_count_after: actif·ve·s
8
+    active_footnote: Utilisateur·rice·s actif·ve·s mensuels (MAU)
7 9
     administered_by: 'Administrée par :'
8 10
     api: API
9 11
     apps: Applications mobiles
12
+    apps_platforms: Utilisez Mastodon depuis iOS, Android et d’autres plates-formes
13
+    browse_directory: Parcourir l’annuaire des profils et filtrer par centres d’intérêt
14
+    browse_public_posts: Parcourir un flux en direct de messages publics sur Mastodon
10 15
     contact: Contact
11 16
     contact_missing: Manquant
12 17
     contact_unavailable: Non disponible
18
+    discover_users: Découvrez des utilisateur·rice·s
13 19
     documentation: Documentation
14 20
     extended_description_html: |
15 21
       <h3>Un bon endroit pour les règles</h3>
16 22
       <p>La description étendue n’a pas été remplie.</p>
23
+    federation_hint_html: Avec un compte sur %{instance}, vous pourrez suivre les gens sur n’importe quel serveur Mastodon et au-delà.
17 24
     generic_description: "%{domain} est seulement un serveur du réseau"
25
+    get_apps: Essayez une application mobile
18 26
     hosted_on: Serveur Mastodon hébergée par %{domain}
19 27
     learn_more: En savoir plus
20 28
     privacy_policy: Politique de vie privée
@@ -23,7 +31,8 @@ fr:
23 31
       one: Statut
24 32
       other: Statuts
25 33
     status_count_before: Ayant publié
26
-    terms: Conditions d'utilisation
34
+    tagline: Suivez vos ami·e·s et découvrez en de nouveaux·elles
35
+    terms: Conditions d’utilisation
27 36
     user_count_after:
28 37
       one: utilisateur
29 38
       other: utilisateurs
@@ -113,15 +122,18 @@ fr:
113 122
       moderation:
114 123
         active: Actif
115 124
         all: Tous
125
+        pending: En cours de traitement
116 126
         silenced: Masqués
117 127
         suspended: Suspendus
118 128
         title: Modération
119 129
       moderation_notes: Notes de modération
120 130
       most_recent_activity: Dernière activité
121 131
       most_recent_ip: Adresse IP la plus récente
132
+      no_account_selected: Aucun compte n’a été modifié, car aucun n’a été sélectionné
122 133
       no_limits_imposed: Aucune limite imposée
123 134
       not_subscribed: Non abonné
124 135
       outbox_url: URL de sortie
136
+      pending: En attente d’approbation
125 137
       perform_full_suspension: Suspendre
126 138
       profile_url: URL du profil
127 139
       promote: Promouvoir
@@ -129,8 +141,10 @@ fr:
129 141
       public: Publique
130 142
       push_subscription_expires: Expiration de l’abonnement PuSH
131 143
       redownload: Rafraîchir le profil
144
+      reject: Rejeter
145
+      reject_all: Tout rejeter
132 146
       remove_avatar: Supprimer l’avatar
133
-      remove_header: Supprimer l'entête
147
+      remove_header: Supprimer lentête
134 148
       resend_confirmation:
135 149
         already_confirmed: Cet·te utilisateur·ice est déjà confirmé·e
136 150
         send: Renvoyer un courriel de confirmation
@@ -149,7 +163,7 @@ fr:
149 163
       shared_inbox_url: URL de la boite de réception partagée
150 164
       show:
151 165
         created_reports: Signalements faits
152
-        targeted_reports: Signalés par d'autres
166
+        targeted_reports: Signalés par dautres
153 167
       silence: Masquer
154 168
       silenced: Silencié
155 169
       statuses: Statuts
@@ -173,7 +187,7 @@ fr:
173 187
         create_domain_block: "%{name} a bloqué le domaine %{target}"
174 188
         create_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste noire"
175 189
         demote_user: "%{name} a rétrogradé l’utilisateur·ice %{target}"
176
-        destroy_custom_emoji: "%{name} a détruit l'émoticône %{target}"
190
+        destroy_custom_emoji: "%{name} a détruit lémoticône %{target}"
177 191
         destroy_domain_block: "%{name} a débloqué le domaine %{target}"
178 192
         destroy_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste blanche"
179 193
         destroy_status: "%{name} a enlevé le statut de %{target}"
@@ -230,6 +244,7 @@ fr:
230 244
       feature_profile_directory: Annuaire des profils
231 245
       feature_registrations: Inscriptions
232 246
       feature_relay: Relais de fédération
247
+      feature_timeline_preview: Aperçu du fil public
233 248
       features: Fonctionnalités
234 249
       hidden_service: Fédération avec des services cachés
235 250
       open_reports: signalements non résolus
@@ -249,6 +264,7 @@ fr:
249 264
       created_msg: Le blocage de domaine est désormais activé
250 265
       destroyed_msg: Le blocage de domaine a été désactivé
251 266
       domain: Domaine
267
+      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>.
252 268
       new:
253 269
         create: Créer le blocage
254 270
         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:
314 330
         expired: Expiré
315 331
         title: Filtre
316 332
       title: Invitations
333
+    pending_accounts:
334
+      title: Comptes en attente (%{count})
317 335
     relays:
318 336
       add_new: Ajouter un nouveau relais
319 337
       delete: Effacer
@@ -324,7 +342,7 @@ fr:
324 342
       enable_hint: Une fois activé, votre serveur souscrira à tous les pouets publics présents sur ce relais et y enverra ses propres pouets publics.
325 343
       enabled: Activé
326 344
       inbox_url: URL de relais
327
-      pending: En attente de l'approbation du relai
345
+      pending: En attente de lapprobation du relai
328 346
       save_and_enable: Sauvegarder et activer
329 347
       setup: Paramétrer une connexion de relais
330 348
       status: Statut
@@ -373,13 +391,13 @@ fr:
373 391
         email: Entrez une adresse courriel publique
374 392
         username: Entrez un nom d’utilisateur⋅ice
375 393
       custom_css:
376
-        desc_html: Modifier l'apparence avec une CSS chargée sur chaque page
394
+        desc_html: Modifier lapparence avec une CSS chargée sur chaque page
377 395
         title: CSS personnalisé
378 396
       hero:
379 397
         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
380 398
         title: Image d’en-tête
381 399
       mascot:
382
-        desc_html: Affiché sur plusieurs pages. Au moins 293×205px recommandé. Lorsqu'il n'est pas défini, retombe à la mascotte par défaut
400
+        desc_html: Affiché sur plusieurs pages. Au moins 293×205px recommandé. Lorsqu’il n’est pas défini, retombe à la mascotte par défaut
383 401
         title: Image de la mascotte
384 402
       peers_api_enabled:
385 403
         desc_html: Noms des domaines que ce serveur a découvert dans le fediverse
@@ -388,8 +406,8 @@ fr:
388 406
         desc_html: Les liens de prévisualisation sur les autres sites web afficheront une vignette même si le média est sensible
389 407
         title: Afficher les médias sensibles dans les prévisualisations OpenGraph
390 408
       profile_directory:
391
-        desc_html: Permettre aux utilisateurs d'être découverts
392
-        title: Activer l'annuaire des profils
409
+        desc_html: Permettre aux utilisateurs dêtre découverts
410
+        title: Activer lannuaire des profils
393 411
       registrations:
394 412
         closed_message:
395 413
           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:
400 418
         min_invite_role:
401 419
           disabled: Personne
402 420
           title: Autoriser les invitations par
421
+      registrations_mode:
422
+        modes:
423
+          approved: Approbation requise pour s’inscrire
424
+          none: Personne ne peut s’inscrire
425
+          open: N’importe qui peut s’inscrire
426
+        title: Mode d’enregistrement
403 427
       show_known_fediverse_at_about_page:
404 428
         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.
405 429
         title: Afficher le fediverse connu dans la prévisualisation du fil
@@ -410,7 +434,7 @@ fr:
410 434
         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>.
411 435
         title: Description du serveur
412 436
       site_description_extended:
413
-        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
437
+        desc_html: Lendroit 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
414 438
         title: Description étendue du serveur
415 439
       site_short_description:
416 440
         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:
449 473
     tags:
450 474
       accounts: Comptes
451 475
       hidden: Masqué
452
-      hide: Masquer dans l'annuaire
476
+      hide: Masquer dans lannuaire
453 477
       name: Hashtag
454 478
       title: Hashtags
455
-      unhide: Afficher dans l'annuaire
479
+      unhide: Afficher dans lannuaire
456 480
       visible: Visible
457 481
     title: Administration
458 482
     warning_presets:
459 483
       add_new: Ajouter un nouveau
460 484
       delete: Effacer
461 485
       edit: Éditer
462
-      edit_preset: Éditer la présélection d'attention
463
-      title: Gérer les présélections d'attention
486
+      edit_preset: Éditer la présélection d’avertissement
487
+      title: Gérer les présélections d’avertissement
464 488
   admin_mailer:
489
+    new_pending_account:
490
+      body: Les détails du nouveau compte se trouvent ci-dessous. Vous pouvez approuver ou rejeter cette demande.
491
+      subject: Nouveau compte à examiner sur %{instance} (%{username})
465 492
     new_report:
466 493
       body: "%{reporter} a signalé %{target}"
467 494
       body_remote: Quelqu’un de %{domain} a signalé %{target}
@@ -482,7 +509,9 @@ fr:
482 509
     warning: Soyez prudent⋅e avec ces données. Ne les partagez pas !
483 510
     your_token: Votre jeton d’accès
484 511
   auth:
512
+    apply_for_account: Demander une invitation
485 513
     change_password: Mot de passe
514
+    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>
486 515
     confirm_email: Confirmer mon adresse mail
487 516
     delete_account: Supprimer le compte
488 517
     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:
498 527
       cas: CAS
499 528
       saml: SAML
500 529
     register: S’inscrire
530
+    registration_closed: "%{instance} n’accepte pas de nouveaux membres"
501 531
     resend_confirmation: Envoyer à nouveau les consignes de confirmation
502 532
     reset_password: Réinitialiser le mot de passe
503 533
     security: Sécurité
504 534
     set_new_password: Définir le nouveau mot de passe
535
+    trouble_logging_in: Vous avez un problème pour vous connecter ?
505 536
   authorize_follow:
506 537
     already_following: Vous suivez déjà ce compte
507 538
     error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant
@@ -537,11 +568,11 @@ fr:
537 568
     warning_title: Disponibilité du contenu disséminé
538 569
   directories:
539 570
     directory: Annuaire des profils
540
-    enabled: Vous êtes actuellement listé dans l'annuaire.
541
-    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.
542
-    explanation: Découvrir des utilisateurs en se basant sur leurs centres d'intérêt
571
+    enabled: Vous êtes actuellement listé dans lannuaire.
572
+    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.
573
+    explanation: Découvrir des utilisateurs en se basant sur leurs centres dintérêt
543 574
     explore_mastodon: Explorer %{title}
544
-    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 !
575
+    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 !
545 576
     people:
546 577
       one: "%{count} personne"
547 578
       other: "%{count} personne"
@@ -557,6 +588,9 @@ fr:
557 588
       content: Nous sommes désolé·e·s, mais quelque chose s’est mal passé de notre côté.
558 589
       title: Cette page n’est pas correcte
559 590
     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.
591
+  existing_username_validator:
592
+    not_found: n’a pas trouvé d’utilisateur·rice local·e avec ce nom
593
+    not_found_multiple: n’a pas trouvé %{usernames}
560 594
   exports:
561 595
     archive_takeout:
562 596
       date: Date
@@ -597,19 +631,41 @@ fr:
597 631
     more: Davantage…
598 632
     resources: Ressources
599 633
   generic:
634
+    all: Tous
600 635
     changes_saved_msg: Les modifications ont été enregistrées avec succès !
601 636
     copy: Copier
637
+    order_by: Classer par
602 638
     save_changes: Enregistrer les modifications
603 639
     validation_errors:
604 640
       one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous
605 641
       other: Certaines choses ne vont pas ! Vérifiez les %{count} erreurs ci-dessous
642
+  html_validator:
643
+    invalid_markup: 'contient un balisage HTML invalide: %{error}'
644
+  identity_proofs:
645
+    active: Actif
646
+    authorize: Oui, autoriser
647
+    authorize_connection_prompt: Autoriser cette connexion chiffrée ?
648
+    errors:
649
+      failed: La connexion chiffrée a échoué. Veuillez réessayer à partir de %{provider}.
650
+      keybase:
651
+        invalid_token: Les jetons Keybase sont des hachages de signatures et doivent comporter 66 caractères hexadécimaux
652
+        verification_failed: Keybase ne reconnaît pas ce jeton comme une signature de l’utilisateur Keybase %{kb_username}. Veuillez réessayer à partir de Keybase.
653
+      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.
654
+    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.
655
+    i_am_html: Je suis %{username} sur %{service}.
656
+    identity: Identité
657
+    inactive: Inactif
658
+    publicize_checkbox: 'Et le poueter:'
659
+    publicize_toot: 'C’est prouvé ! Je suis %{username} sur %{service}: %{url}'
660
+    status: Statut de vérification
661
+    view_proof: Voir la preuve
606 662
   imports:
607 663
     modes:
608 664
       merge: Fusionner
609 665
       merge_long: Garder les enregistrements existants et ajouter les nouveaux
610 666
       overwrite: Réécrire
611 667
       overwrite_long: Remplacer les enregistrements actuels par les nouveaux
612
-    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.
668
+    preface: Vous pouvez importer certaines données que vous avez exporté dun autre serveur, comme une liste des personnes que vous suivez ou bloquez sur votre compte.
613 669
     success: Vos données ont été importées avec succès et seront traitées en temps et en heure
614 670
     types:
615 671
       blocking: Liste d’utilisateur⋅ice⋅s bloqué⋅e⋅s
@@ -713,13 +769,26 @@ fr:
713 769
       duration_too_short: est trop tôt
714 770
       expired: Ce sondage est déjà terminé
715 771
       over_character_limit: ne peuvent être plus long que %{max} caractères chacun
716
-      too_few_options: doit avoir plus qu'une proposition
772
+      too_few_options: doit avoir plus quune proposition
717 773
       too_many_options: ne peut contenir plus que %{max} propositions
718 774
   preferences:
719 775
     languages: Langues
720 776
     other: Autre
721 777
     publishing: Publication
722 778
     web: Web
779
+  relationships:
780
+    activity: Activité du compte
781
+    dormant: Dormant
782
+    last_active: Dernière activité
783
+    most_recent: Plus récent
784
+    moved: Déménagé
785
+    mutual: Mutuel
786
+    primary: Primaire
787
+    relationship: Relation
788
+    remove_selected_domains: Supprimer tous les abonné·e·s des domaines sélectionnés
789
+    remove_selected_followers: Supprimer les abonné·e·s sélectionnés
790
+    remove_selected_follows: Cesser de suivre les utilisateur·rice·s sélectionné·e·s
791
+    status: Statut du compte
723 792
   remote_follow:
724 793
     acct: Entrez l’adresse profil@serveur depuis laquelle vous voulez vous abonner
725 794
     missing_resource: L’URL de redirection n’a pas pu être trouvée
@@ -729,7 +798,7 @@ fr:
729 798
     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."
730 799
   remote_interaction:
731 800
     favourite:
732
-      proceed: Confirmer l'ajout aux favoris
801
+      proceed: Confirmer lajout aux favoris
733 802
       prompt: 'Vous souhaitez mettre ce pouet en favori :'
734 803
     reblog:
735 804
       proceed: Confirmer le repartage
@@ -787,6 +856,9 @@ fr:
787 856
     revoke_success: Session révoquée avec succès
788 857
     title: Sessions
789 858
   settings:
859
+    account: Compte
860
+    account_settings: Paramètres du compte
861
+    appearance: Apparence
790 862
     authorized_apps: Applications autorisées
791 863
     back: Retour vers Mastodon
792 864
     delete: Suppression de compte
@@ -794,10 +866,13 @@ fr:
794 866
     edit_profile: Modifier le profil
795 867
     export: Export de données
796 868
     featured_tags: Hashtags mis en avant
869
+    identity_proofs: Preuves d’identité
797 870
     import: Import de données
798 871
     migrate: Migration de compte
799 872
     notifications: Notifications
800 873
     preferences: Préférences
874
+    profile: Profil
875
+    relationships: Abonnements et abonné·e·s
801 876
     two_factor_authentication: Identification à deux facteurs
802 877
   statuses:
803 878
     attached:
@@ -954,8 +1029,8 @@ fr:
954 1029
       title: Récupération de l’archive
955 1030
     warning:
956 1031
       explanation:
957
-        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é.
958
-        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.
1032
+        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é.
1033
+        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, dautres utilisateurs peuvent vous suivre manuellement.
959 1034
         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.
960 1035
       review_server_policies: Passer en revue les politiques du serveur
961 1036
       subject:
@@ -993,5 +1068,5 @@ fr:
993 1068
     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.
994 1069
     signed_in_as: 'Connecté·e en tant que :'
995 1070
   verification:
996
-    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 :'
1071
+    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 :'
997 1072
     verification: Vérification

+ 13
- 12
config/locales/ja.yml View File

@@ -20,7 +20,7 @@ ja:
20 20
     extended_description_html: |
21 21
       <h3>ルールを書くのに適した場所</h3>
22 22
       <p>詳細説明が設定されていません。</p>
23
-    federation_hint_html: "%{instance} にアカウントがあればどの互換性のあるサーバーのユーザーでもフォローできるでしょう。"
23
+    federation_hint_html: "%{instance} のアカウントひとつでどんなMastodon互換サーバーのユーザーでもフォローできるでしょう。"
24 24
     generic_description: "%{domain} は、Mastodon サーバーの一つです"
25 25
     get_apps: モバイルアプリを試す
26 26
     hosted_on: Mastodon hosted on %{domain}
@@ -269,6 +269,7 @@ ja:
269 269
       created_msg: ドメインブロック処理を完了しました
270 270
       destroyed_msg: ドメインブロックを外しました
271 271
       domain: ドメイン
272
+      existing_domain_block_html: 既に%{name}に対しより厳しい制限を課しています 。まずは<a href="%{unblock_url}">それを解除する</a>必要があります。
272 273
       new:
273 274
         create: ブロックを作成
274 275
         hint: ドメインブロックはデータベース中のアカウント項目の作成を妨げませんが、遡って自動的に指定されたモデレーションをそれらのアカウントに適用します。
@@ -660,7 +661,7 @@ ja:
660 661
     i_am_html: I am %{username} on %{service}.
661 662
     identity: Identity
662 663
     inactive: 非アクティブ
663
-    publicize_checkbox: 'そしてこれをトゥートしてください:'
664
+    publicize_checkbox: 'そしてこれをトゥートします:'
664 665
     publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}'
665 666
     status: 認証状態
666 667
     view_proof: 証明を表示
@@ -1051,21 +1052,21 @@ ja:
1051 1052
         suspend: アカウントが停止されました
1052 1053
     welcome:
1053 1054
       edit_profile_action: プロフィールを設定
1054
-      edit_profile_step: アバター画像やヘッダー画像をアップロードしたり、表示名やその他プロフィールを変更しカスタマイズすることができます。新しいフォロワーからのフォローを許可する前に検討したい場合、アカウントを承認制にすることができます。
1055
+      edit_profile_step: アイコンやヘッダーの画像をアップロードしたり、表示名を変更したりして、自分のプロフィールをカスタマイズすることができます。また、誰かからの新規フォローを許可する前にその人の様子を見ておきたい場合、アカウントを承認制にすることもできます。
1055 1056
       explanation: 始めるにあたってのアドバイスです
1056 1057
       final_action: 始めましょう
1057
-      final_step: 'さあ始めましょう! たとえフォロワーがいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどで誰かの目に止まるかもしれません。自己紹介をしたい時は #introductions ハッシュタグを使うといいかもしれません。'
1058
-      full_handle: あなたの正式なユーザー
1059
-      full_handle_hint: これは別のサーバーからフォローしてもらったりメッセージのやり取りをする際に、友達に伝えるといいでしょう
1058
+      final_step: 'さあ始めましょう! たとえフォロワーがまだいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #introductions ハッシュタグが便利かもしれません。'
1059
+      full_handle: あなたの正式なユーザーID
1060
+      full_handle_hint: 別のサーバーの友達とフォローやメッセージをやり取りする際には、これを伝えることになります
1060 1061
       review_preferences_action: 設定の変更
1061
-      review_preferences_step: 受け取りたいメールや投稿の公開範囲などの設定を必ず行ってください。不快でないならアニメーション GIF の自動再生を有効にすることもできます
1062
+      review_preferences_step: 受け取りたいメールの種類や投稿のデフォルト公開範囲など、ユーザー設定を必ず済ませておきましょう。目が回らない自信があるなら、アニメーション GIF を自動再生する設定もご検討ください
1062 1063
       subject: Mastodon へようこそ
1063
-      tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じサーバーの人がフォローしている人だけが含まれるので、それが全てではありません。
1064
-      tip_following: 標準では自動でサーバーの管理者をフォローしています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してください
1065
-      tip_local_timeline: ローカルタイムラインは %{instance} にいる人々の流れを見られるものです。彼らはあなたと同じサーバーにいる隣人のようなものです!
1066
-      tip_mobile_webapp: もしモバイル端末のブラウザで Mastodon をホーム画面に追加できる場合、プッシュ通知を受け取ることができます。それはまるでネイティブアプリのように動作します!
1064
+      tip_federated_timeline: 連合タイムラインは、Mastodon ネットワークによる巨大流しそうめんです。ただし、あなたの「隣人」達がフォローしている人々だけが流れてくる場所なので、決してそこに全てがあるわけではありません。
1065
+      tip_following: 最初は、サーバーの管理者をフォローした状態になっています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してみましょう
1066
+      tip_local_timeline: ローカルタイムラインには、%{instance} にいる人々が流しそうめんのごとく流れてきます。彼らはあなたと同じサーバーに暮らす、愛すべき隣人です!
1067
+      tip_mobile_webapp: お使いのモバイル端末で、ブラウザから Mastodon をホーム画面に追加できますか? もし追加できる場合、プッシュ通知の受け取りなど、まるで「普通の」アプリのような機能が楽しめます!
1067 1068
       tips: 豆知識
1068
-      title: ようこそ、%{name} 
1069
+      title: ようこそ、%{name}!
1069 1070
   users:
1070 1071
     follow_limit_reached: あなたは現在 %{limit} 人以上フォローできません
1071 1072
     invalid_email: メールアドレスが無効です

+ 14
- 10
config/locales/simple_form.fr.yml View File

@@ -5,8 +5,8 @@ fr:
5 5
       account_warning_preset:
6 6
         text: Vous pouvez utiliser la syntaxe des pouets, comme les URLs, les hashtags et les mentions
7 7
       admin_account_action:
8
-        send_email_notification: L'utilisateur recevra une explication de ce qu'il s'est passé avec son compte
9
-        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
8
+        send_email_notification: L’utilisateur recevra une explication de ce qu’il s’est passé avec son compte
9
+        text_html: Optionnel. Vous pouvez utilisez la syntaxe des pouets. Vous pouvez <a href="%{path}">ajouter des présélections dattention</a> pour économiser du temps
10 10
         type_html: Choisir que faire avec <strong>%{acct}</strong>
11 11
         warning_preset_id: Optionnel. Vous pouvez toujours ajouter un texte personnalisé à la fin de la présélection
12 12
       defaults:
@@ -15,7 +15,7 @@ fr:
15 15
         bot: Ce compte exécute principalement des actions automatisées et pourrait ne pas être surveillé
16 16
         context: Un ou plusieurs contextes où le filtre devrait s’appliquer
17 17
         digest: Uniquement envoyé après une longue période d’inactivité et uniquement si vous avez reçu des messages personnels pendant votre absence
18
-        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
18
+        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
19 19
         email: Vous recevrez un courriel de confirmation
20 20
         fields: Vous pouvez avoir jusqu’à 4 éléments affichés en tant que tableau sur votre profil
21 21
         header: Au format PNG, GIF ou JPG. %{size} maximum. Sera réduit à %{dimensions}px
@@ -26,21 +26,23 @@ fr:
26 26
         password: Utilisez au moins 8 caractères
27 27
         phrase: Sera trouvé sans que la case ou l’avertissement de contenu du pouet soit pris en compte
28 28
         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.
29
-        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)
29
+        setting_aggregate_reblogs: Ne pas afficher de nouveaux repartagés pour les pouets qui ont été récemment repartagés (naffecte que les repartagés nouvellement reçus)
30 30
         setting_default_language: La langue de vos pouets peut être détectée automatiquement, mais ça n’est pas toujours pertinent
31 31
         setting_display_media_default: Masquer les supports marqués comme sensibles
32 32
         setting_display_media_hide_all: Toujours masquer tous les médias
33 33
         setting_display_media_show_all: Toujours afficher les médias marqués comme sensibles
34 34
         setting_hide_network: Ceux que vous suivez et ceux qui vous suivent ne seront pas affichés sur votre profil
35 35
         setting_noindex: Affecte votre profil public ainsi que vos statuts
36
-        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
36
+        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
37 37
         setting_theme: Affecte l’apparence de Mastodon quand vous êtes connecté·e depuis n’importe quel appareil.
38 38
         username: Votre nom d’utilisateur sera unique sur %{domain}
39 39
         whole_word: Lorsque le mot-clef ou la phrase-clef est uniquement alphanumérique, ça sera uniquement appliqué s’il correspond au mot entier
40 40
       featured_tag:
41
-        name: 'Vous pourriez utiliser l''un d''entre eux :'
41
+        name: 'Vous pourriez vouloir utiliser l’un d’entre eux :'
42 42
       imports:
43 43
         data: Un fichier CSV généré par un autre serveur de Mastodon
44
+      invite_request:
45
+        text: Cela nous aidera à considérer votre demande
44 46
       sessions:
45 47
         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 :'
46 48
       user:
@@ -53,7 +55,7 @@ fr:
53 55
       account_warning_preset:
54 56
         text: Texte de présélection
55 57
       admin_account_action:
56
-        send_email_notification: Notifier l'utilisateur par courriel
58
+        send_email_notification: Notifier lutilisateur par courriel
57 59
         text: Attention personnalisée
58 60
         type: Action
59 61
         types:
@@ -61,7 +63,7 @@ fr:
61 63
           none: Ne rien faire
62 64
           silence: Silence
63 65
           suspend: Suspendre et effacer les données du compte de manière irréversible
64
-        warning_preset_id: Utiliser un modèle d'avertissement
66
+        warning_preset_id: Utiliser un modèle davertissement
65 67
       defaults:
66 68
         autofollow: Invitation à suivre votre compte
67 69
         avatar: Image de profil
@@ -72,7 +74,7 @@ fr:
72 74
         context: Contextes du filtre
73 75
         current_password: Mot de passe actuel
74 76
         data: Données
75
-        discoverable: Inscrire ce compte dans l'annuaire
77
+        discoverable: Inscrire ce compte dans lannuaire
76 78
         display_name: Nom public
77 79
         email: Adresse courriel
78 80
         expires_in: Expire après
@@ -103,7 +105,7 @@ fr:
103 105
         setting_hide_network: Cacher votre réseau
104 106
         setting_noindex: Demander aux moteurs de recherche de ne pas indexer vos informations personnelles
105 107
         setting_reduce_motion: Réduire la vitesse des animations
106
-        setting_show_application: Dévoiler le nom de l'application utilisée pour envoyer des pouets
108
+        setting_show_application: Dévoiler le nom de lapplication utilisée pour envoyer des pouets
107 109
         setting_system_font_ui: Utiliser la police par défaut du système
108 110
         setting_theme: Thème du site
109 111
         setting_unfollow_modal: Afficher une fenêtre de confirmation avant de vous désabonner d’un compte
@@ -118,6 +120,8 @@ fr:
118 120
         must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas
119 121
         must_be_following: Masquer les notifications des personnes que vous ne suivez pas
120 122
         must_be_following_dm: Bloquer les messages directs des personnes que vous ne suivez pas
123
+      invite_request:
124
+        text: Pourquoi voulez-vous vous inscrire ?
121 125
       notification_emails:
122 126
         digest: Envoyer des courriels récapitulatifs
123 127
         favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statuts à ses favoris

+ 12
- 9
config/locales/sk.yml View File

@@ -115,7 +115,7 @@ sk:
115 115
       followers: Sledujúci
116 116
       followers_url: URL adresa sledujúcich
117 117
       follows: Sledovania
118
-      header: Hlavička
118
+      header: Záhlavie
119 119
       inbox_url: URL adresa prijatých správ
120 120
       invited_by: Pozvaný/á užívateľom
121 121
       ip: IP adresa
@@ -138,6 +138,7 @@ sk:
138 138
       moderation_notes: Moderátorské poznámky
139 139
       most_recent_activity: Posledná aktivita
140 140
       most_recent_ip: Posledná IP adresa
141
+      no_account_selected: Nedošlo k žiadnému pozmeneniu účtov, keďže žiadne neboli vybrané
141 142
       no_limits_imposed: Nie sú stanovené žiadné obmedzenia
142 143
       not_subscribed: Neodoberá
143 144
       outbox_url: URL poslaných
@@ -152,7 +153,7 @@ sk:
152 153
       reject: Zamietni
153 154
       reject_all: Zamietni všetky
154 155
       remove_avatar: Vymaž avatar
155
-      remove_header: Vymaž hlavičku
156
+      remove_header: Vymaž záhlavie
156 157
       resend_confirmation:
157 158
         already_confirmed: Tento užívateľ je už potvrdený
158 159
         send: Odošli potvrdzovací email znovu
@@ -319,7 +320,7 @@ sk:
319 320
       by_domain: Doména
320 321
       delivery_available: Je v dosahu doručovania
321 322
       known_accounts:
322
-        few: "%{count} známe účty"
323
+        few: "%{count} známych účtov"
323 324
         one: "%{count} známy účet"
324 325
         other: "%{count} známe účty"
325 326
       moderation:
@@ -340,18 +341,20 @@ sk:
340 341
         expired: Vypršalo
341 342
         title: Filtruj
342 343
       title: Pozvánky
344
+    pending_accounts:
345
+      title: Čakajúcich účtov (%{count})
343 346
     relays:
344 347
       add_new: Pridaj nový federovací mostík
345 348
       delete: Vymaž
346
-      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í."
347
-      disable: Pozastav
348
-      disabled: Zastavené
349
+      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í."
350
+      disable: Vypni
351
+      disabled: Vypnutý
349 352
       enable: Povoľ
350 353
       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.
351 354
       enabled: Povolené
352 355
       inbox_url: URL adresa mostu
353
-      pending: Čakám na povolenie od prechodného mostu
354
-      save_and_enable: Uložiť a povoliť
356
+      pending: Čaká sa na povolenie od prechodného mostu
357
+      save_and_enable: Ulož a povoľ
355 358
       setup: Nastav prepojenie s mostom
356 359
       status: Stav
357 360
       title: Mosty
@@ -390,7 +393,7 @@ sk:
390 393
       updated_at: Aktualizované
391 394
     settings:
392 395
       activity_api_enabled:
393
-        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
396
+        desc_html: Sčítanie miestne uverejnených príspevkov, aktívnych užívateľov, a nových registrácii, v týždenných intervaloch
394 397
         title: Vydať hromadné štatistiky o užívateľskej aktivite
395 398
       bootstrap_timeline_accounts:
396 399
         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.

+ 5
- 0
db/migrate/20190509164208_add_by_moderator_to_tombstone.rb View File

@@ -0,0 +1,5 @@
1
+class AddByModeratorToTombstone < ActiveRecord::Migration[5.2]
2
+  def change
3
+    add_column :tombstones, :by_moderator, :boolean
4
+  end
5
+end

+ 2
- 1
db/schema.rb View File

@@ -10,7 +10,7 @@
10 10
 #
11 11
 # It's strongly recommended that you check this file into your version control system.
12 12
 
13
-ActiveRecord::Schema.define(version: 2019_04_20_025523) do
13
+ActiveRecord::Schema.define(version: 2019_05_09_164208) do
14 14
 
15 15
   # These are extensions that must be enabled in order to support this database
16 16
   enable_extension "plpgsql"
@@ -688,6 +688,7 @@ ActiveRecord::Schema.define(version: 2019_04_20_025523) do
688 688
     t.string "uri", null: false
689 689
     t.datetime "created_at", null: false
690 690
     t.datetime "updated_at", null: false
691
+    t.boolean "by_moderator"
691 692
     t.index ["account_id"], name: "index_tombstones_on_account_id"
692 693
     t.index ["uri"], name: "index_tombstones_on_uri"
693 694
   end

+ 6
- 1
lib/mastodon/domains_cli.rb View File

@@ -28,10 +28,15 @@ module Mastodon
28 28
         say('.', :green, false)
29 29
       end
30 30
 
31
-      DomainBlock.where(domain: domain).destroy_all
31
+      DomainBlock.where(domain: domain).destroy_all unless options[:dry_run]
32 32
 
33 33
       say
34 34
       say("Removed #{removed} accounts#{dry_run}", :green)
35
+
36
+      custom_emojis = CustomEmoji.where(domain: domain)
37
+      custom_emojis_count = custom_emojis.count
38
+      custom_emojis.destroy_all unless options[:dry_run]
39
+      say("Removed #{custom_emojis_count} custom emojis", :green)
35 40
     end
36 41
 
37 42
     option :concurrency, type: :numeric, default: 50, aliases: [:c]

+ 1
- 1
lib/mastodon/version.rb View File

@@ -13,7 +13,7 @@ module Mastodon
13 13
     end
14 14
 
15 15
     def patch
16
-      1
16
+      2
17 17
     end
18 18
 
19 19
     def pre

+ 32
- 0
spec/lib/activitypub/tag_manager_spec.rb View File

@@ -41,6 +41,22 @@ RSpec.describe ActivityPub::TagManager do
41 41
       status.mentions.create(account: mentioned)
42 42
       expect(subject.to(status)).to eq [subject.uri_for(mentioned)]
43 43
     end
44
+
45
+    it "returns URIs of mentions for direct silenced author's status only if they are followers or requesting to be" do
46
+      bob    = Fabricate(:account, username: 'bob')
47
+      alice  = Fabricate(:account, username: 'alice')
48
+      foo    = Fabricate(:account)
49
+      author = Fabricate(:account, username: 'author', silenced: true)
50
+      status = Fabricate(:status, visibility: :direct, account: author)
51
+      bob.follow!(author)
52
+      FollowRequest.create!(account: foo, target_account: author)
53
+      status.mentions.create(account: alice)
54
+      status.mentions.create(account: bob)
55
+      status.mentions.create(account: foo)
56
+      expect(subject.to(status)).to include(subject.uri_for(bob))
57
+      expect(subject.to(status)).to include(subject.uri_for(foo))
58
+      expect(subject.to(status)).to_not include(subject.uri_for(alice))
59
+    end
44 60
   end
45 61
 
46 62
   describe '#cc' do
@@ -70,6 +86,22 @@ RSpec.describe ActivityPub::TagManager do
70 86
       status.mentions.create(account: mentioned)
71 87
       expect(subject.cc(status)).to include(subject.uri_for(mentioned))
72 88
     end
89
+
90
+    it "returns URIs of mentions for silenced author's non-direct status only if they are followers or requesting to be" do
91
+      bob    = Fabricate(:account, username: 'bob')
92
+      alice  = Fabricate(:account, username: 'alice')
93
+      foo    = Fabricate(:account)
94
+      author = Fabricate(:account, username: 'author', silenced: true)
95
+      status = Fabricate(:status, visibility: :public, account: author)
96
+      bob.follow!(author)
97
+      FollowRequest.create!(account: foo, target_account: author)
98
+      status.mentions.create(account: alice)
99
+      status.mentions.create(account: bob)
100
+      status.mentions.create(account: foo)
101
+      expect(subject.cc(status)).to include(subject.uri_for(bob))
102
+      expect(subject.cc(status)).to include(subject.uri_for(foo))
103
+      expect(subject.cc(status)).to_not include(subject.uri_for(alice))
104
+    end
73 105
   end
74 106
 
75 107
   describe '#local_uri?' do

+ 24
- 3
spec/services/process_mentions_service_spec.rb View File

@@ -1,10 +1,11 @@
1 1
 require 'rails_helper'
2 2
 
3 3
 RSpec.describe ProcessMentionsService, type: :service do
4
-  let(:account) { Fabricate(:account, username: 'alice') }
5
-  let(:status)  { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") }
4
+  let(:account)    { Fabricate(:account, username: 'alice') }
5
+  let(:visibility) { :public }
6
+  let(:status)     { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: visibility) }
6 7
 
7
-  context 'OStatus' do
8
+  context 'OStatus with public toot' do
8 9
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
9 10
 
10 11
     subject { ProcessMentionsService.new }
@@ -23,6 +24,26 @@ RSpec.describe ProcessMentionsService, type: :service do
23 24
     end
24 25
   end
25 26
 
27
+  context 'OStatus with private toot' do
28
+    let(:visibility)  { :private }
29
+    let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
30
+
31
+    subject { ProcessMentionsService.new }
32
+
33
+    before do
34
+      stub_request(:post, remote_user.salmon_url)
35
+      subject.call(status)
36
+    end
37
+
38
+    it 'does not create a mention' do
39
+      expect(remote_user.mentions.where(status: status).count).to eq 0
40
+    end
41
+
42
+    it 'does not post to remote user\'s Salmon end point' do
43
+      expect(a_request(:post, remote_user.salmon_url)).to_not have_been_made
44
+    end
45
+  end
46
+
26 47
   context 'ActivityPub' do
27 48
     let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
28 49
 

Loading…
Cancel
Save