Browse Source

Merge remote-tracking branch 'upstream/master' into cofe

cofe
Lukas Breuer 6 months ago
parent
commit
3e4bb2e9ad
49 changed files with 444 additions and 104 deletions
  1. 1
    1
      Dockerfile
  2. 1
    1
      Gemfile
  3. 4
    4
      Gemfile.lock
  4. 1
    11
      app/controllers/directories_controller.rb
  5. 1
    1
      app/helpers/home_helper.rb
  6. 5
    1
      app/javascript/mastodon/actions/compose.js
  7. 23
    1
      app/javascript/mastodon/actions/notifications.js
  8. 1
    1
      app/javascript/mastodon/features/account/components/header.js
  9. 4
    3
      app/javascript/mastodon/features/compose/components/compose_form.js
  10. 1
    0
      app/javascript/mastodon/features/compose/containers/compose_form_container.js
  11. 15
    3
      app/javascript/mastodon/features/notifications/components/column_settings.js
  12. 93
    0
      app/javascript/mastodon/features/notifications/components/filter_bar.js
  13. 4
    0
      app/javascript/mastodon/features/notifications/containers/column_settings_container.js
  14. 16
    0
      app/javascript/mastodon/features/notifications/containers/filter_bar_container.js
  15. 20
    3
      app/javascript/mastodon/features/notifications/index.js
  16. 8
    0
      app/javascript/mastodon/locales/en.json
  17. 8
    0
      app/javascript/mastodon/locales/pl.json
  18. 7
    3
      app/javascript/mastodon/reducers/compose.js
  19. 3
    0
      app/javascript/mastodon/reducers/notifications.js
  20. 8
    0
      app/javascript/mastodon/reducers/settings.js
  21. 46
    0
      app/javascript/styles/mastodon/components.scss
  22. 6
    0
      app/javascript/styles/mastodon/containers.scss
  23. 30
    13
      app/javascript/styles/mastodon/widgets.scss
  24. 2
    2
      app/models/account.rb
  25. 3
    0
      app/models/web/push_subscription.rb
  26. 2
    5
      app/views/accounts/_header.html.haml
  27. 2
    6
      app/views/directories/index.html.haml
  28. 3
    3
      app/views/layouts/public.html.haml
  29. 2
    2
      app/workers/web/push_notification_worker.rb
  30. 0
    2
      config/database.yml
  31. 0
    1
      config/locales/ar.yml
  32. 0
    2
      config/locales/co.yml
  33. 10
    12
      config/locales/cs.yml
  34. 1
    1
      config/locales/devise.cs.yml
  35. 0
    2
      config/locales/el.yml
  36. 0
    2
      config/locales/en.yml
  37. 0
    2
      config/locales/eu.yml
  38. 0
    2
      config/locales/fr.yml
  39. 0
    2
      config/locales/gl.yml
  40. 0
    2
      config/locales/ja.yml
  41. 0
    2
      config/locales/nl.yml
  42. 0
    2
      config/locales/oc.yml
  43. 0
    2
      config/locales/pl.yml
  44. 0
    2
      config/locales/sk.yml
  45. 0
    2
      config/routes.rb
  46. 14
    0
      spec/controllers/admin/action_logs_controller_spec.rb
  47. 14
    0
      spec/controllers/admin/dashboard_controller_spec.rb
  48. 46
    0
      spec/controllers/api/v1/accounts/pins_controller_spec.rb
  49. 39
    0
      spec/controllers/remote_interaction_controller_spec.rb

+ 1
- 1
Dockerfile View File

@@ -1,4 +1,4 @@
1
-FROM node:8.12.0-alpine as node
1
+FROM node:8.14.0-alpine as node
2 2
 FROM ruby:2.4.5-alpine3.8
3 3
 
4 4
 LABEL maintainer="https://github.com/tootsuite/mastodon" \

+ 1
- 1
Gemfile View File

@@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
15 15
 gem 'pghero', '~> 2.2'
16 16
 gem 'dotenv-rails', '~> 2.5'
17 17
 
18
-gem 'aws-sdk-s3', '~> 1.27', require: false
18
+gem 'aws-sdk-s3', '~> 1.30', require: false
19 19
 gem 'fog-core', '<= 2.1.0'
20 20
 gem 'fog-openstack', '~> 0.3', require: false
21 21
 gem 'paperclip', '~> 6.0'

+ 4
- 4
Gemfile.lock View File

@@ -76,8 +76,8 @@ GEM
76 76
     av (0.9.0)
77 77
       cocaine (~> 0.5.3)
78 78
     aws-eventstream (1.0.1)
79
-    aws-partitions (1.118.0)
80
-    aws-sdk-core (3.41.0)
79
+    aws-partitions (1.122.0)
80
+    aws-sdk-core (3.43.0)
81 81
       aws-eventstream (~> 1.0)
82 82
       aws-partitions (~> 1.0)
83 83
       aws-sigv4 (~> 1.0)
@@ -85,7 +85,7 @@ GEM
85 85
     aws-sdk-kms (1.13.0)
86 86
       aws-sdk-core (~> 3, >= 3.39.0)
87 87
       aws-sigv4 (~> 1.0)
88
-    aws-sdk-s3 (1.27.0)
88
+    aws-sdk-s3 (1.30.0)
89 89
       aws-sdk-core (~> 3, >= 3.39.0)
90 90
       aws-sdk-kms (~> 1)
91 91
       aws-sigv4 (~> 1.0)
@@ -655,7 +655,7 @@ DEPENDENCIES
655 655
   active_record_query_trace (~> 1.5)
656 656
   addressable (~> 2.5)
657 657
   annotate (~> 2.7)
658
-  aws-sdk-s3 (~> 1.27)
658
+  aws-sdk-s3 (~> 1.30)
659 659
   better_errors (~> 2.5)
660 660
   binding_of_caller (~> 0.7)
661 661
   bootsnap (~> 1.3)

+ 1
- 11
app/controllers/directories_controller.rb View File

@@ -32,22 +32,12 @@ class DirectoriesController < ApplicationController
32 32
   end
33 33
 
34 34
   def set_accounts
35
-    @accounts = Account.searchable.discoverable.page(params[:page]).per(50).tap do |query|
35
+    @accounts = Account.discoverable.page(params[:page]).per(30).tap do |query|
36 36
       query.merge!(Account.tagged_with(@tag.id)) if @tag
37
-
38
-      if popular_requested?
39
-        query.merge!(Account.popular)
40
-      else
41
-        query.merge!(Account.by_recent_status)
42
-      end
43 37
     end
44 38
   end
45 39
 
46 40
   def set_instance_presenter
47 41
     @instance_presenter = InstancePresenter.new
48 42
   end
49
-
50
-  def popular_requested?
51
-    request.path.ends_with?('/popular')
52
-  end
53 43
 end

+ 1
- 1
app/helpers/home_helper.rb View File

@@ -23,7 +23,7 @@ module HomeHelper
23 23
                   else
24 24
                     link_to(path || TagManager.instance.url_for(account), class: 'account__display-name') do
25 25
                       content_tag(:div, class: 'account__avatar-wrapper') do
26
-                        content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{account.avatar.url})")
26
+                        content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})")
27 27
                       end +
28 28
                         content_tag(:span, class: 'display-name') do
29 29
                           content_tag(:bdi) do

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

@@ -144,7 +144,11 @@ export function submitCompose(routerHistory) {
144 144
 
145 145
       if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
146 146
         routerHistory.push('/timelines/direct');
147
-      } else if (response.data.visibility !== 'direct') {
147
+      } else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
148
+        routerHistory.goBack();
149
+      }
150
+
151
+      if (response.data.visibility !== 'direct') {
148 152
         insertIfOnline('home');
149 153
       }
150 154
 

+ 23
- 1
app/javascript/mastodon/actions/notifications.js View File

@@ -8,6 +8,7 @@ import {
8 8
   importFetchedStatuses,
9 9
 } from './importer';
10 10
 import { defineMessages } from 'react-intl';
11
+import { List as ImmutableList } from 'immutable';
11 12
 import { unescapeHTML } from '../utils/html';
12 13
 import { getFilters, regexFromFilters } from '../selectors';
13 14
 
@@ -18,6 +19,8 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
18 19
 export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
19 20
 export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';
20 21
 
22
+export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
23
+
21 24
 export const NOTIFICATIONS_CLEAR      = 'NOTIFICATIONS_CLEAR';
22 25
 export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
23 26
 
@@ -88,10 +91,16 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
88 91
 
89 92
 const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
90 93
 
94
+const excludeTypesFromFilter = filter => {
95
+  const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']);
96
+  return allTypes.filterNot(item => item === filter).toJS();
97
+};
98
+
91 99
 const noOp = () => {};
92 100
 
93 101
 export function expandNotifications({ maxId } = {}, done = noOp) {
94 102
   return (dispatch, getState) => {
103
+    const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
95 104
     const notifications = getState().get('notifications');
96 105
     const isLoadingMore = !!maxId;
97 106
 
@@ -102,7 +111,9 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
102 111
 
103 112
     const params = {
104 113
       max_id: maxId,
105
-      exclude_types: excludeTypesFromSettings(getState()),
114
+      exclude_types: activeFilter === 'all'
115
+        ? excludeTypesFromSettings(getState())
116
+        : excludeTypesFromFilter(activeFilter),
106 117
     };
107 118
 
108 119
     if (!maxId && notifications.get('items').size > 0) {
@@ -167,3 +178,14 @@ export function scrollTopNotifications(top) {
167 178
     top,
168 179
   };
169 180
 };
181
+
182
+export function setFilter (filterType) {
183
+  return dispatch => {
184
+    dispatch({
185
+      type: NOTIFICATIONS_FILTER_SET,
186
+      path: ['notifications', 'quickFilter', 'active'],
187
+      value: filterType,
188
+    });
189
+    dispatch(expandNotifications());
190
+  };
191
+};

+ 1
- 1
app/javascript/mastodon/features/account/components/header.js View File

@@ -158,7 +158,7 @@ class Header extends ImmutablePureComponent {
158 158
     const badge           = account.get('bot') ? (<div className='roles'><div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div></div>) : null;
159 159
 
160 160
     return (
161
-      <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}>
161
+      <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${autoPlayGif ? account.get('header') : account.get('header_static')})` }}>
162 162
         <div>
163 163
           <Avatar account={account} />
164 164
 

+ 4
- 3
app/javascript/mastodon/features/compose/components/compose_form.js View File

@@ -49,6 +49,7 @@ class ComposeForm extends ImmutablePureComponent {
49 49
     caretPosition: PropTypes.number,
50 50
     preselectDate: PropTypes.instanceOf(Date),
51 51
     is_submitting: PropTypes.bool,
52
+    is_changing_upload: PropTypes.bool,
52 53
     is_uploading: PropTypes.bool,
53 54
     onChange: PropTypes.func.isRequired,
54 55
     onSubmit: PropTypes.func.isRequired,
@@ -84,10 +85,10 @@ class ComposeForm extends ImmutablePureComponent {
84 85
     }
85 86
 
86 87
     // Submit disabled:
87
-    const { is_submitting, is_uploading, anyMedia } = this.props;
88
+    const { is_submitting, is_changing_upload, is_uploading, anyMedia } = this.props;
88 89
     const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join('');
89 90
 
90
-    if (is_submitting || is_uploading || length(fulltext) > maxChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
91
+    if (is_submitting || is_uploading || is_changing_upload || length(fulltext) > maxChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
91 92
       return;
92 93
     }
93 94
 
@@ -163,7 +164,7 @@ class ComposeForm extends ImmutablePureComponent {
163 164
     const { intl, onPaste, showSearch, anyMedia } = this.props;
164 165
     const disabled = this.props.is_submitting;
165 166
     const text     = [this.props.spoiler_text, countableText(this.props.text)].join('');
166
-    const disabledButton = disabled || this.props.is_uploading || length(text) > maxChars || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
167
+    const disabledButton = disabled || this.props.is_uploading || this.props.is_changing_upload || length(text) > maxChars || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
167 168
     let publishText = '';
168 169
 
169 170
     if (this.props.privacy === 'private' || this.props.privacy === 'direct') {

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

@@ -22,6 +22,7 @@ const mapStateToProps = state => ({
22 22
   caretPosition: state.getIn(['compose', 'caretPosition']),
23 23
   preselectDate: state.getIn(['compose', 'preselectDate']),
24 24
   is_submitting: state.getIn(['compose', 'is_submitting']),
25
+  is_changing_upload: state.getIn(['compose', 'is_changing_upload']),
25 26
   is_uploading: state.getIn(['compose', 'is_uploading']),
26 27
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
27 28
   anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,

+ 15
- 3
app/javascript/mastodon/features/notifications/components/column_settings.js View File

@@ -21,9 +21,11 @@ export default class ColumnSettings extends React.PureComponent {
21 21
   render () {
22 22
     const { settings, pushSettings, onChange, onClear } = this.props;
23 23
 
24
-    const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
25
-    const showStr  = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
26
-    const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
24
+    const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
25
+    const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
26
+    const alertStr  = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
27
+    const showStr   = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
28
+    const soundStr  = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
27 29
 
28 30
     const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
29 31
     const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
@@ -34,6 +36,16 @@ export default class ColumnSettings extends React.PureComponent {
34 36
           <ClearColumnButton onClick={onClear} />
35 37
         </div>
36 38
 
39
+        <div role='group' aria-labelledby='notifications-filter-bar'>
40
+          <span id='notifications-filter-bar' className='column-settings__section'>
41
+            <FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
42
+          </span>
43
+          <div className='column-settings__row'>
44
+            <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
45
+            <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
46
+          </div>
47
+        </div>
48
+
37 49
         <div role='group' aria-labelledby='notifications-follow'>
38 50
           <span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
39 51
 

+ 93
- 0
app/javascript/mastodon/features/notifications/components/filter_bar.js View File

@@ -0,0 +1,93 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
4
+
5
+const tooltips = defineMessages({
6
+  mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
7
+  favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
8
+  boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
9
+  follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
10
+});
11
+
12
+export default @injectIntl
13
+class FilterBar extends React.PureComponent {
14
+
15
+  static propTypes = {
16
+    selectFilter: PropTypes.func.isRequired,
17
+    selectedFilter: PropTypes.string.isRequired,
18
+    advancedMode: PropTypes.bool.isRequired,
19
+    intl: PropTypes.object.isRequired,
20
+  };
21
+
22
+  onClick (notificationType) {
23
+    return () => this.props.selectFilter(notificationType);
24
+  }
25
+
26
+  render () {
27
+    const { selectedFilter, advancedMode, intl } = this.props;
28
+    const renderedElement = !advancedMode ? (
29
+      <div className='notification__filter-bar'>
30
+        <button
31
+          className={selectedFilter === 'all' ? 'active' : ''}
32
+          onClick={this.onClick('all')}
33
+        >
34
+          <FormattedMessage
35
+            id='notifications.filter.all'
36
+            defaultMessage='All'
37
+          />
38
+        </button>
39
+        <button
40
+          className={selectedFilter === 'mention' ? 'active' : ''}
41
+          onClick={this.onClick('mention')}
42
+        >
43
+          <FormattedMessage
44
+            id='notifications.filter.mentions'
45
+            defaultMessage='Mentions'
46
+          />
47
+        </button>
48
+      </div>
49
+    ) : (
50
+      <div className='notification__filter-bar'>
51
+        <button
52
+          className={selectedFilter === 'all' ? 'active' : ''}
53
+          onClick={this.onClick('all')}
54
+        >
55
+          <FormattedMessage
56
+            id='notifications.filter.all'
57
+            defaultMessage='All'
58
+          />
59
+        </button>
60
+        <button
61
+          className={selectedFilter === 'mention' ? 'active' : ''}
62
+          onClick={this.onClick('mention')}
63
+          title={intl.formatMessage(tooltips.mentions)}
64
+        >
65
+          <i className='fa fa-fw fa-at' />
66
+        </button>
67
+        <button
68
+          className={selectedFilter === 'favourite' ? 'active' : ''}
69
+          onClick={this.onClick('favourite')}
70
+          title={intl.formatMessage(tooltips.favourites)}
71
+        >
72
+          <i className='fa fa-fw fa-star' />
73
+        </button>
74
+        <button
75
+          className={selectedFilter === 'reblog' ? 'active' : ''}
76
+          onClick={this.onClick('reblog')}
77
+          title={intl.formatMessage(tooltips.boosts)}
78
+        >
79
+          <i className='fa fa-fw fa-retweet' />
80
+        </button>
81
+        <button
82
+          className={selectedFilter === 'follow' ? 'active' : ''}
83
+          onClick={this.onClick('follow')}
84
+          title={intl.formatMessage(tooltips.follows)}
85
+        >
86
+          <i className='fa fa-fw fa-user-plus' />
87
+        </button>
88
+      </div>
89
+    );
90
+    return renderedElement;
91
+  }
92
+
93
+}

+ 4
- 0
app/javascript/mastodon/features/notifications/containers/column_settings_container.js View File

@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
2 2
 import { defineMessages, injectIntl } from 'react-intl';
3 3
 import ColumnSettings from '../components/column_settings';
4 4
 import { changeSetting } from '../../../actions/settings';
5
+import { setFilter } from '../../../actions/notifications';
5 6
 import { clearNotifications } from '../../../actions/notifications';
6 7
 import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
7 8
 import { openModal } from '../../../actions/modal';
@@ -21,6 +22,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
21 22
   onChange (path, checked) {
22 23
     if (path[0] === 'push') {
23 24
       dispatch(changePushNotifications(path.slice(1), checked));
25
+    } else if (path[0] === 'quickFilter') {
26
+      dispatch(changeSetting(['notifications', ...path], checked));
27
+      dispatch(setFilter('all'));
24 28
     } else {
25 29
       dispatch(changeSetting(['notifications', ...path], checked));
26 30
     }

+ 16
- 0
app/javascript/mastodon/features/notifications/containers/filter_bar_container.js View File

@@ -0,0 +1,16 @@
1
+import { connect } from 'react-redux';
2
+import FilterBar from '../components/filter_bar';
3
+import { setFilter } from '../../../actions/notifications';
4
+
5
+const makeMapStateToProps = state => ({
6
+  selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
7
+  advancedMode: state.getIn(['settings', 'notifications', 'quickFilter', 'advanced']),
8
+});
9
+
10
+const mapDispatchToProps = (dispatch) => ({
11
+  selectFilter (newActiveFilter) {
12
+    dispatch(setFilter(newActiveFilter));
13
+  },
14
+});
15
+
16
+export default connect(makeMapStateToProps, mapDispatchToProps)(FilterBar);

+ 20
- 3
app/javascript/mastodon/features/notifications/index.js View File

@@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
9 9
 import NotificationContainer from './containers/notification_container';
10 10
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
11 11
 import ColumnSettingsContainer from './containers/column_settings_container';
12
+import FilterBarContainer from './containers/filter_bar_container';
12 13
 import { createSelector } from 'reselect';
13 14
 import { List as ImmutableList } from 'immutable';
14 15
 import { debounce } from 'lodash';
@@ -20,11 +21,22 @@ const messages = defineMessages({
20 21
 });
21 22
 
22 23
 const getNotifications = createSelector([
24
+  state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
25
+  state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
23 26
   state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
24 27
   state => state.getIn(['notifications', 'items']),
25
-], (excludedTypes, notifications) => notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))));
28
+], (showFilterBar, allowedType, excludedTypes, notifications) => {
29
+  if (!showFilterBar || allowedType === 'all') {
30
+    // used if user changed the notification settings after loading the notifications from the server
31
+    // otherwise a list of notifications will come pre-filtered from the backend
32
+    // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
33
+    return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
34
+  }
35
+  return notifications.filter(item => item !== null && allowedType === item.get('type'));
36
+});
26 37
 
27 38
 const mapStateToProps = state => ({
39
+  showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
28 40
   notifications: getNotifications(state),
29 41
   isLoading: state.getIn(['notifications', 'isLoading'], true),
30 42
   isUnread: state.getIn(['notifications', 'unread']) > 0,
@@ -38,6 +50,7 @@ class Notifications extends React.PureComponent {
38 50
   static propTypes = {
39 51
     columnId: PropTypes.string,
40 52
     notifications: ImmutablePropTypes.list.isRequired,
53
+    showFilterBar: PropTypes.bool.isRequired,
41 54
     dispatch: PropTypes.func.isRequired,
42 55
     shouldUpdateScroll: PropTypes.func,
43 56
     intl: PropTypes.object.isRequired,
@@ -117,12 +130,16 @@ class Notifications extends React.PureComponent {
117 130
   }
118 131
 
119 132
   render () {
120
-    const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
133
+    const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props;
121 134
     const pinned = !!columnId;
122 135
     const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
123 136
 
124 137
     let scrollableContent = null;
125 138
 
139
+    const filterBarContainer = showFilterBar
140
+      ? (<FilterBarContainer />)
141
+      : null;
142
+
126 143
     if (isLoading && this.scrollableContent) {
127 144
       scrollableContent = this.scrollableContent;
128 145
     } else if (notifications.size > 0 || hasMore) {
@@ -179,7 +196,7 @@ class Notifications extends React.PureComponent {
179 196
         >
180 197
           <ColumnSettingsContainer />
181 198
         </ColumnHeader>
182
-
199
+        {filterBarContainer}
183 200
         {scrollContainer}
184 201
       </Column>
185 202
     );

+ 8
- 0
app/javascript/mastodon/locales/en.json View File

@@ -223,6 +223,14 @@
223 223
   "notification.reblog": "{name} boosted your status",
224 224
   "notifications.clear": "Clear notifications",
225 225
   "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
226
+  "notifications.filter.all": "All",
227
+  "notifications.filter.mentions": "Mentions",
228
+  "notifications.filter.favourites": "Favourites",
229
+  "notifications.filter.boosts": "Boosts",
230
+  "notifications.filter.follows": "Follows",
231
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
232
+  "notifications.column_settings.filter_bar.show": "Show",
233
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
226 234
   "notifications.column_settings.alert": "Desktop notifications",
227 235
   "notifications.column_settings.favourite": "Favourites:",
228 236
   "notifications.column_settings.follow": "New followers:",

+ 8
- 0
app/javascript/mastodon/locales/pl.json View File

@@ -223,6 +223,14 @@
223 223
   "notification.reblog": "{name} podbił(a) Twój wpis",
224 224
   "notifications.clear": "Wyczyść powiadomienia",
225 225
   "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
226
+  "notifications.filter.all": "Wszystkie",
227
+  "notifications.filter.mentions": "Wspomnienia",
228
+  "notifications.filter.favourites": "Ulubione",
229
+  "notifications.filter.boosts": "Podbicia",
230
+  "notifications.filter.follows": "Śledzenia",
231
+  "notifications.column_settings.filter_bar.category": "Szybkie filtrowanie",
232
+  "notifications.column_settings.filter_bar.show": "Pokaż",
233
+  "notifications.column_settings.filter_bar.advanced": "Wyświetl wszystkie kategorie",
226 234
   "notifications.column_settings.alert": "Powiadomienia na pulpicie",
227 235
   "notifications.column_settings.favourite": "Dodanie do ulubionych:",
228 236
   "notifications.column_settings.follow": "Nowi śledzący:",

+ 7
- 3
app/javascript/mastodon/reducers/compose.js View File

@@ -51,6 +51,7 @@ const initialState = ImmutableMap({
51 51
   in_reply_to: null,
52 52
   is_composing: false,
53 53
   is_submitting: false,
54
+  is_changing_upload: false,
54 55
   is_uploading: false,
55 56
   progress: 0,
56 57
   media_attachments: ImmutableList(),
@@ -79,6 +80,7 @@ function clearAll(state) {
79 80
     map.set('spoiler', false);
80 81
     map.set('spoiler_text', '');
81 82
     map.set('is_submitting', false);
83
+    map.set('is_changing_upload', false);
82 84
     map.set('in_reply_to', null);
83 85
     map.set('privacy', state.get('default_privacy'));
84 86
     map.set('sensitive', false);
@@ -248,13 +250,15 @@ export default function compose(state = initialState, action) {
248 250
       map.set('idempotencyKey', uuid());
249 251
     });
250 252
   case COMPOSE_SUBMIT_REQUEST:
251
-  case COMPOSE_UPLOAD_CHANGE_REQUEST:
252 253
     return state.set('is_submitting', true);
254
+  case COMPOSE_UPLOAD_CHANGE_REQUEST:
255
+    return state.set('is_changing_upload', true);
253 256
   case COMPOSE_SUBMIT_SUCCESS:
254 257
     return clearAll(state);
255 258
   case COMPOSE_SUBMIT_FAIL:
256
-  case COMPOSE_UPLOAD_CHANGE_FAIL:
257 259
     return state.set('is_submitting', false);
260
+  case COMPOSE_UPLOAD_CHANGE_FAIL:
261
+    return state.set('is_changing_upload', false);
258 262
   case COMPOSE_UPLOAD_REQUEST:
259 263
     return state.set('is_uploading', true);
260 264
   case COMPOSE_UPLOAD_SUCCESS:
@@ -300,7 +304,7 @@ export default function compose(state = initialState, action) {
300 304
     return insertEmoji(state, action.position, action.emoji, action.needsSpace);
301 305
   case COMPOSE_UPLOAD_CHANGE_SUCCESS:
302 306
     return state
303
-      .set('is_submitting', false)
307
+      .set('is_changing_upload', false)
304 308
       .update('media_attachments', list => list.map(item => {
305 309
         if (item.get('id') === action.media.id) {
306 310
           return fromJS(action.media);

+ 3
- 0
app/javascript/mastodon/reducers/notifications.js View File

@@ -3,6 +3,7 @@ import {
3 3
   NOTIFICATIONS_EXPAND_SUCCESS,
4 4
   NOTIFICATIONS_EXPAND_REQUEST,
5 5
   NOTIFICATIONS_EXPAND_FAIL,
6
+  NOTIFICATIONS_FILTER_SET,
6 7
   NOTIFICATIONS_CLEAR,
7 8
   NOTIFICATIONS_SCROLL_TOP,
8 9
 } from '../actions/notifications';
@@ -98,6 +99,8 @@ export default function notifications(state = initialState, action) {
98 99
     return state.set('isLoading', true);
99 100
   case NOTIFICATIONS_EXPAND_FAIL:
100 101
     return state.set('isLoading', false);
102
+  case NOTIFICATIONS_FILTER_SET:
103
+    return state.set('items', ImmutableList()).set('hasMore', true);
101 104
   case NOTIFICATIONS_SCROLL_TOP:
102 105
     return updateTop(state, action.top);
103 106
   case NOTIFICATIONS_UPDATE:

+ 8
- 0
app/javascript/mastodon/reducers/settings.js View File

@@ -1,4 +1,5 @@
1 1
 import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
2
+import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
2 3
 import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns';
3 4
 import { STORE_HYDRATE } from '../actions/store';
4 5
 import { EMOJI_USE } from '../actions/emojis';
@@ -32,6 +33,12 @@ const initialState = ImmutableMap({
32 33
       mention: true,
33 34
     }),
34 35
 
36
+    quickFilter: ImmutableMap({
37
+      active: 'all',
38
+      show: true,
39
+      advanced: false,
40
+    }),
41
+
35 42
     shows: ImmutableMap({
36 43
       follow: true,
37 44
       favourite: true,
@@ -112,6 +119,7 @@ export default function settings(state = initialState, action) {
112 119
   switch(action.type) {
113 120
   case STORE_HYDRATE:
114 121
     return hydrate(state, action.state.get('settings'));
122
+  case NOTIFICATIONS_FILTER_SET:
115 123
   case SETTING_CHANGE:
116 124
     return state
117 125
       .setIn(action.path, action.value)

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

@@ -1484,6 +1484,52 @@ a.account__display-name {
1484 1484
   }
1485 1485
 }
1486 1486
 
1487
+.notification__filter-bar {
1488
+  display: flex;
1489
+  flex-wrap: wrap;
1490
+  justify-content: space-between;
1491
+  background: $ui-base-color;
1492
+
1493
+  & > button {
1494
+    position: relative;
1495
+    flex-grow: 1;
1496
+    color: $primary-text-color;
1497
+    padding: 10px 5px 12px;
1498
+    text-decoration: none;
1499
+    font-weight: 400;
1500
+    font-size: 15px;
1501
+    line-height: 18px;
1502
+    background: darken($ui-base-color, 4%);
1503
+    border: 0;
1504
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
1505
+    cursor: default;
1506
+
1507
+    &.active {
1508
+      color: $secondary-text-color;
1509
+
1510
+      &::before,
1511
+      &::after {
1512
+        display: block;
1513
+        content: "";
1514
+        position: absolute;
1515
+        bottom: 0;
1516
+        left: 50%;
1517
+        width: 0;
1518
+        height: 0;
1519
+        transform: translateX(-50%);
1520
+        border-style: solid;
1521
+        border-width: 0 10px 10px;
1522
+        border-color: transparent transparent lighten($ui-base-color, 8%);
1523
+      }
1524
+
1525
+      &::after {
1526
+        bottom: -1px;
1527
+        border-color: transparent transparent $ui-base-color;
1528
+      }
1529
+    }
1530
+  }
1531
+}
1532
+
1487 1533
 .notification__message {
1488 1534
   margin: 0 10px 0 68px;
1489 1535
   padding: 8px 0 0;

+ 6
- 0
app/javascript/styles/mastodon/containers.scss View File

@@ -294,6 +294,12 @@
294 294
         text-decoration: underline;
295 295
         color: $primary-text-color;
296 296
       }
297
+
298
+      @media screen and (max-width: $no-gap-breakpoint) {
299
+        &.optional {
300
+          display: none;
301
+        }
302
+      }
297 303
     }
298 304
 
299 305
     .nav-button {

+ 30
- 13
app/javascript/styles/mastodon/widgets.scss View File

@@ -229,18 +229,6 @@
229 229
   margin-bottom: 10px;
230 230
 }
231 231
 
232
-.moved-account-widget,
233
-.memoriam-widget,
234
-.box-widget,
235
-.contact-widget,
236
-.landing-page__information.contact-widget {
237
-  @media screen and (max-width: $no-gap-breakpoint) {
238
-    margin-bottom: 0;
239
-    box-shadow: none;
240
-    border-radius: 0;
241
-  }
242
-}
243
-
244 232
 .page-header {
245 233
   background: lighten($ui-base-color, 8%);
246 234
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
@@ -261,11 +249,20 @@
261 249
     font-size: 15px;
262 250
     color: $darker-text-color;
263 251
   }
252
+
253
+  @media screen and (max-width: $no-gap-breakpoint) {
254
+    margin-top: 0;
255
+    background: lighten($ui-base-color, 4%);
256
+
257
+    h1 {
258
+      font-size: 24px;
259
+    }
260
+  }
264 261
 }
265 262
 
266 263
 .directory {
267 264
   background: $ui-base-color;
268
-  border-radius: 0 0 4px 4px;
265
+  border-radius: 4px;
269 266
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
270 267
 
271 268
   &__tag {
@@ -407,4 +404,24 @@
407 404
       font-size: 14px;
408 405
     }
409 406
   }
407
+
408
+  @media screen and (max-width: $no-gap-breakpoint) {
409
+    tbody td.optional {
410
+      display: none;
411
+    }
412
+  }
413
+}
414
+
415
+.moved-account-widget,
416
+.memoriam-widget,
417
+.box-widget,
418
+.contact-widget,
419
+.landing-page__information.contact-widget,
420
+.directory,
421
+.page-header {
422
+  @media screen and (max-width: $no-gap-breakpoint) {
423
+    margin-bottom: 0;
424
+    box-shadow: none;
425
+    border-radius: 0;
426
+  }
410 427
 }

+ 2
- 2
app/models/account.rb View File

@@ -91,10 +91,10 @@ class Account < ApplicationRecord
91 91
   scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
92 92
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
93 93
   scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) }
94
-  scope :discoverable, -> { where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) }
94
+  scope :discoverable, -> { searchable.where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status }
95 95
   scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
96
-  scope :popular, -> { order('account_stats.followers_count desc') }
97 96
   scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
97
+  scope :popular, -> { order('account_stats.followers_count desc') }
98 98
 
99 99
   delegate :email,
100 100
            :unconfirmed_email,

+ 3
- 0
app/models/web/push_subscription.rb View File

@@ -68,6 +68,9 @@ class Web::PushSubscription < ApplicationRecord
68 68
       p256dh: key_p256dh,
69 69
       auth: key_auth,
70 70
       ttl: ttl,
71
+      ssl_timeout: 10,
72
+      open_timeout: 10,
73
+      read_timeout: 10,
71 74
       vapid: {
72 75
         subject: "mailto:#{::Setting.site_contact_email}",
73 76
         private_key: Rails.configuration.x.vapid_private_key,

+ 2
- 5
app/views/accounts/_header.html.haml View File

@@ -1,12 +1,9 @@
1 1
 .public-account-header{:class => ("inactive" if account.moved?)}
2 2
   .public-account-header__image
3
-    = image_tag account.header.url, class: 'parallax'
3
+    = image_tag (current_account&.user&.setting_auto_play_gif ? account.header_original_url : account.header_static_url), class: 'parallax'
4 4
   .public-account-header__bar
5 5
     = link_to short_account_url(account), class: 'avatar' do
6
-      - if current_account&.user&.setting_auto_play_gif
7
-        = image_tag account.avatar_original_url
8
-      - else
9
-        = image_tag account.avatar_static_url
6
+      = image_tag (current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)
10 7
     .public-account-header__tabs
11 8
       .public-account-header__tabs__name
12 9
         %h1

+ 2
- 6
app/views/directories/index.html.haml View File

@@ -16,10 +16,6 @@
16 16
 
17 17
 .grid
18 18
   .column-0
19
-    .account__section-headline
20
-      = active_link_to t('directories.most_recently_active'), @tag ? explore_hashtag_path(@tag) : explore_path
21
-      = active_link_to t('directories.most_popular'), @tag ? explore_hashtag_popular_path(@tag) : explore_popular_path
22
-
23 19
     - if @accounts.empty?
24 20
       = nothing_here
25 21
     - else
@@ -29,10 +25,10 @@
29 25
             - @accounts.each do |account|
30 26
               %tr
31 27
                 %td= account_link_to account
32
-                %td.accounts-table__count
28
+                %td.accounts-table__count.optional
33 29
                   = number_to_human account.statuses_count, strip_insignificant_zeros: true
34 30
                   %small= t('accounts.posts', count: account.statuses_count).downcase
35
-                %td.accounts-table__count
31
+                %td.accounts-table__count.optional
36 32
                   = number_to_human account.followers_count, strip_insignificant_zeros: true
37 33
                   %small= t('accounts.followers', count: account.followers_count).downcase
38 34
                 %td.accounts-table__count

+ 3
- 3
app/views/layouts/public.html.haml View File

@@ -10,9 +10,9 @@
10 10
             = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
11 11
 
12 12
           - if Setting.profile_directory
13
-            = link_to t('directories.directory'), explore_path, class: 'nav-link'
14
-          = link_to t('about.about_this'), about_more_path, class: 'nav-link'
15
-          = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link'
13
+            = link_to t('directories.directory'), explore_path, class: 'nav-link optional'
14
+          = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
15
+          = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
16 16
         .nav-center
17 17
         .nav-right
18 18
           - if user_signed_in?

+ 2
- 2
app/workers/web/push_notification_worker.rb View File

@@ -10,8 +10,8 @@ class Web::PushNotificationWorker
10 10
     notification = Notification.find(notification_id)
11 11
 
12 12
     subscription.push(notification) unless notification.activity.nil?
13
-  rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
14
-    subscription.destroy!
13
+  rescue Webpush::ResponseError => e
14
+    subscription.destroy! if (400..499).cover?(e.response.code.to_i)
15 15
   rescue ActiveRecord::RecordNotFound
16 16
     true
17 17
   end

+ 0
- 2
config/database.yml View File

@@ -3,8 +3,6 @@ default: &default
3 3
   pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %>
4 4
   timeout: 5000
5 5
   encoding: unicode
6
-  variables:
7
-    statement_timeout: 60000
8 6
 
9 7
 development:
10 8
   <<: *default

+ 0
- 1
config/locales/ar.yml View File

@@ -541,7 +541,6 @@ ar:
541 541
     warning_title: توافر المحتوى المنشور و المبعثَر
542 542
   directories:
543 543
     explore_mastodon: استكشف %{title}
544
-    most_popular: المشهورة
545 544
   errors:
546 545
     '403': ليس لك الصلاحيات الكافية لعرض هذه الصفحة.
547 546
     '404': إنّ الصفحة التي تبحث عنها لا وجود لها أصلا.

+ 0
- 2
config/locales/co.yml View File

@@ -531,8 +531,6 @@ co:
531 531
     directory: Annuariu di i prufili
532 532
     explanation: Scopre utilizatori à partesi di i so centri d'interessu
533 533
     explore_mastodon: Scopre à %{title}
534
-    most_popular: I più pupulari
535
-    most_recently_active: Attività a più fresca
536 534
     people:
537 535
       one: "%{count} persona"
538 536
       other: "%{count} persone"

+ 10
- 12
config/locales/cs.yml View File

@@ -31,7 +31,7 @@ cs:
31 31
     privacy_policy: Zásady soukromí
32 32
     source_code: Zdrojový kód
33 33
     status_count_after:
34
-      few: příspěvků
34
+      few: příspěvky
35 35
       one: příspěvek
36 36
       other: příspěvků
37 37
     status_count_before: Kteří napsali
@@ -274,7 +274,7 @@ cs:
274 274
       severity: Přísnost
275 275
       show:
276 276
         affected_accounts:
277
-          few: "%{count} účtů v databázi bylo ovlivněno"
277
+          few: "%{count} účty v databázi byly ovlivněny"
278 278
           one: Jeden účet v databázi byl ovlivněn
279 279
           other: "%{count} účtů v databázi bylo ovlivněno"
280 280
         retroactive:
@@ -536,10 +536,8 @@ cs:
536 536
     directory: Adresář profilů
537 537
     explanation: Objevujte uživatele podle jejich zájmů
538 538
     explore_mastodon: Prozkoumejte %{title}
539
-    most_popular: Nejpopulárnější
540
-    most_recently_active: Naposledy aktivní
541 539
     people:
542
-      few: "%{count} lidí"
540
+      few: "%{count} lidé"
543 541
       one: "%{count} člověk"
544 542
       other: "%{count} lidí"
545 543
   errors:
@@ -590,9 +588,9 @@ cs:
590 588
     lock_link: Zamkněte svůj účet
591 589
     purge: Odstranit ze sledovatelů
592 590
     success:
593
-      few: V průběhu blokování sledovatelů z %{count} domény...
591
+      few: V průběhu blokování sledovatelů ze %{count} domén...
594 592
       one: V průběhu blokování sledovatelů z jedné domény...
595
-      other: V průběhu blokování sledovatelů z %{count} domény...
593
+      other: V průběhu blokování sledovatelů z %{count} domén...
596 594
     true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
597 595
     unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé příspěvky. %{lock_link}, abyste mohl/a zkontrolovat a odmítnout sledovatele.
598 596
     unlocked_warning_title: Váš účet není zamknutý
@@ -605,7 +603,7 @@ cs:
605 603
     copy: Kopírovat
606 604
     save_changes: Uložit změny
607 605
     validation_errors:
608
-      few: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyb níže
606
+      few: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyby níže
609 607
       one: Něco ještě není úplně v pořádku! Prosím zkontrolujte chybu níže
610 608
       other: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyb níže
611 609
   imports:
@@ -660,11 +658,11 @@ cs:
660 658
       body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
661 659
       mention: "%{name} vás zmínil/a v:"
662 660
       new_followers_summary:
663
-        few: Navíc jste získal/a %{count} nových sledovatelů, zatímco jste byl/a pryč! Hurá!
661
+        few: Navíc jste získal/a %{count} nové sledovatele, zatímco jste byl/a pryč! Hurá!
664 662
         one: Navíc jste získal/a jednoho nového sledovatele, zatímco jste byl/a pryč! Hurá!
665 663
         other: Navíc jste získal/a %{count} nových sledovatelů, zatímco jste byl/a pryč! Hurá!
666 664
       subject:
667
-        few: "%{count} nových oznámení od vaší poslední návštěvy \U0001F418"
665
+        few: "%{count} nová oznámení od vaší poslední návštěvy \U0001F418"
668 666
         one: "1 nové oznámení od vaší poslední návštěvy \U0001F418"
669 667
         other: "%{count} nových oznámení od vaší poslední návštěvy \U0001F418"
670 668
       title: Ve vaší nepřítomnosti...
@@ -784,11 +782,11 @@ cs:
784 782
     attached:
785 783
       description: 'Přiloženo: %{attached}'
786 784
       image:
787
-        few: "%{count} obrázků"
785
+        few: "%{count} obrázky"
788 786
         one: "%{count} obrázek"
789 787
         other: "%{count} obrázků"
790 788
       video:
791
-        few: "%{count} videí"
789
+        few: "%{count} videa"
792 790
         one: "%{count} video"
793 791
         other: "%{count} videí"
794 792
     boosted_from_html: Boostnuto z %{acct_link}

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

@@ -78,6 +78,6 @@ cs:
78 78
       not_found: nenalezen
79 79
       not_locked: nebyl uzamčen
80 80
       not_saved:
81
-        few: "%{count} chyb zabránilo uložení tohoto %{resource}:"
81
+        few: "%{count} chyby zabránily uložení tohoto %{resource}:"
82 82
         one: '1 chyba zabránila uložení tohoto %{resource}:'
83 83
         other: "%{count} chyb zabránilo uložení tohoto %{resource}:"

+ 0
- 2
config/locales/el.yml View File

@@ -531,8 +531,6 @@ el:
531 531
     directory: Κατάλογος λογαριασμών
532 532
     explanation: Βρες χρήστες βάσει των ενδιαφερόντων τους
533 533
     explore_mastodon: Εξερεύνησε %{title}
534
-    most_popular: Δημοφιλείς
535
-    most_recently_active: Πρόσφατα ενεργοί
536 534
     people:
537 535
       one: "%{count} άτομο"
538 536
       other: "%{count} άτομα"

+ 0
- 2
config/locales/en.yml View File

@@ -535,8 +535,6 @@ en:
535 535
     directory: Profile directory
536 536
     explanation: Discover users based on their interests
537 537
     explore_mastodon: Explore %{title}
538
-    most_popular: Most popular
539
-    most_recently_active: Most recently active
540 538
     people:
541 539
       one: "%{count} person"
542 540
       other: "%{count} people"

+ 0
- 2
config/locales/eu.yml View File

@@ -531,8 +531,6 @@ eu:
531 531
     directory: Profilen direktorioa
532 532
     explanation: Deskubritu erabiltzaileak interesen arabera
533 533
     explore_mastodon: Esploratu %{title}
534
-    most_popular: Puri-purian
535
-    most_recently_active: Azkenaldian aktibo
536 534
     people:
537 535
       one: pertsona %{count}
538 536
       other: "%{count} pertsona"

+ 0
- 2
config/locales/fr.yml View File

@@ -531,8 +531,6 @@ fr:
531 531
     directory: Annuaire des profils
532 532
     explanation: Découvrir des utilisateurs en se basant sur leurs centres d'intérêt
533 533
     explore_mastodon: Explorer %{title}
534
-    most_popular: Les plus populaires
535
-    most_recently_active: Les actifs les plus récents
536 534
     people:
537 535
       one: "%{count} personne"
538 536
       other: "%{count} personne"

+ 0
- 2
config/locales/gl.yml View File

@@ -531,8 +531,6 @@ gl:
531 531
     directory: Directorio de perfil
532 532
     explanation: Descubra usuarias según o seu interese
533 533
     explore_mastodon: Explorar %{title}
534
-    most_popular: Máis popular
535
-    most_recently_active: Máis activa recentemente
536 534
     people:
537 535
       one: "%{count} persoa"
538 536
       other: "%{count} persoas"

+ 0
- 2
config/locales/ja.yml View File

@@ -530,8 +530,6 @@ ja:
530 530
   directories:
531 531
     directory: ディレクトリ
532 532
     explore_mastodon: "%{title}を探索"
533
-    most_popular: 人気順
534
-    most_recently_active: 直近の活動順
535 533
     people:
536 534
       one: "%{count} 人"
537 535
       other: "%{count} 人"

+ 0
- 2
config/locales/nl.yml View File

@@ -531,8 +531,6 @@ nl:
531 531
     directory: Gebruikersgids
532 532
     explanation: Ontdek gebruikers aan de hand van hun interesses
533 533
     explore_mastodon: "%{title} verkennen"
534
-    most_popular: Meest populair
535
-    most_recently_active: Recentelijk actief
536 534
     people:
537 535
       one: "%{count} gebruikers"
538 536
       other: "%{count} gebruikers"

+ 0
- 2
config/locales/oc.yml View File

@@ -587,8 +587,6 @@ oc:
587 587
     directory: Annuari de perfils
588 588
     explanation: Trobar d’utilizaires segon lor interèsses
589 589
     explore_mastodon: Explorar %{title}
590
-    most_popular: Mai populars
591
-    most_recently_active: Mai actius recentament
592 590
     people:
593 591
       one: "%{count} persona"
594 592
       other: "%{count} personas"

+ 0
- 2
config/locales/pl.yml View File

@@ -541,8 +541,6 @@ pl:
541 541
     directory: Katalog profilów
542 542
     explanation: Poznaj profile na podstawie zainteresowań
543 543
     explore_mastodon: Odkrywaj %{title}
544
-    most_popular: Napopularniejsi
545
-    most_recently_active: Ostatnio aktywni
546 544
     people:
547 545
       few: "%{count} osoby"
548 546
       many: "%{count} osób"

+ 0
- 2
config/locales/sk.yml View File

@@ -536,8 +536,6 @@ sk:
536 536
     directory: Databáza profilov
537 537
     explanation: Pátraj po užívateľoch podľa ich záujmov
538 538
     explore_mastodon: Prebádaj %{title}
539
-    most_popular: Najpopulárnejšie
540
-    most_recently_active: Naposledy aktívni
541 539
     people:
542 540
       few: "%{count} ľudia"
543 541
       one: "%{count} človek"

+ 0
- 2
config/routes.rb View File

@@ -81,9 +81,7 @@ Rails.application.routes.draw do
81 81
   post '/interact/:id', to: 'remote_interaction#create'
82 82
 
83 83
   get '/explore', to: 'directories#index', as: :explore
84
-  get '/explore/popular', to: 'directories#index', as: :explore_popular
85 84
   get '/explore/:id', to: 'directories#show', as: :explore_hashtag
86
-  get '/explore/:id/popular', to: 'directories#show', as: :explore_hashtag_popular
87 85
 
88 86
   namespace :settings do
89 87
     resource :profile, only: [:show, :update]

+ 14
- 0
spec/controllers/admin/action_logs_controller_spec.rb View File

@@ -0,0 +1,14 @@
1
+# frozen_string_literal: true
2
+
3
+require 'rails_helper'
4
+
5
+describe Admin::ActionLogsController, type: :controller do
6
+  describe 'GET #index' do
7
+    it 'returns 200' do
8
+      sign_in Fabricate(:user, admin: true)
9
+      get :index, params: { page: 1 }
10
+
11
+      expect(response).to have_http_status(200)
12
+    end
13
+  end
14
+end

+ 14
- 0
spec/controllers/admin/dashboard_controller_spec.rb View File

@@ -0,0 +1,14 @@
1
+# frozen_string_literal: true
2
+
3
+require 'rails_helper'
4
+
5
+describe Admin::DashboardController, type: :controller do
6
+  describe 'GET #index' do
7
+    it 'returns 200' do
8
+      sign_in Fabricate(:user, admin: true)
9
+      get :index
10
+
11
+      expect(response).to have_http_status(200)
12
+    end
13
+  end
14
+end

+ 46
- 0
spec/controllers/api/v1/accounts/pins_controller_spec.rb View File

@@ -0,0 +1,46 @@
1
+# frozen_string_literal: true
2
+
3
+require 'rails_helper'
4
+
5
+RSpec.describe Api::V1::Accounts::PinsController, type: :controller do
6
+  let(:john)  { Fabricate(:user, account: Fabricate(:account, username: 'john')) }
7
+  let(:kevin) { Fabricate(:user, account: Fabricate(:account, username: 'kevin')) }
8
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: john.id, scopes: 'write:accounts') }
9
+
10
+  before do
11
+    kevin.account.followers << john.account
12
+    allow(controller).to receive(:doorkeeper_token) { token }
13
+  end
14
+
15
+  describe 'POST #create' do
16
+    subject { post :create, params: { account_id: kevin.account.id } }
17
+
18
+    it 'returns 200' do
19
+      expect(response).to have_http_status(200)
20
+    end
21
+
22
+    it 'creates account_pin' do
23
+      expect do
24
+        subject
25
+      end.to change { AccountPin.where(account: john.account, target_account: kevin.account).count }.by(1)
26
+    end
27
+  end
28
+
29
+  describe 'DELETE #destroy' do
30
+    subject { delete :destroy, params: { account_id: kevin.account.id } }
31
+
32
+    before do
33
+      Fabricate(:account_pin, account: john.account, target_account: kevin.account)
34
+    end
35
+
36
+    it 'returns 200' do
37
+      expect(response).to have_http_status(200)
38
+    end
39
+
40
+    it 'destroys account_pin' do
41
+      expect do
42
+        subject
43
+      end.to change { AccountPin.where(account: john.account, target_account: kevin.account).count }.by(-1)
44
+    end
45
+  end
46
+end

+ 39
- 0
spec/controllers/remote_interaction_controller_spec.rb View File

@@ -0,0 +1,39 @@
1
+# frozen_string_literal: true
2
+
3
+require 'rails_helper'
4
+
5
+describe RemoteInteractionController, type: :controller do
6
+  render_views
7
+
8
+  let(:status) { Fabricate(:status) }
9
+
10
+  describe 'GET #new' do
11
+    it 'returns 200' do
12
+      get :new, params: { id: status.id }
13
+      expect(response).to have_http_status(200)
14
+    end
15
+  end
16
+
17
+  describe 'POST #create' do
18
+    context '@remote_follow is valid' do
19
+      it 'returns 302' do
20
+        allow_any_instance_of(RemoteFollow).to receive(:valid?) { true }
21
+        allow_any_instance_of(RemoteFollow).to receive(:addressable_template) do
22
+          Addressable::Template.new('https://hoge.com')
23
+        end
24
+
25
+        post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } }
26
+        expect(response).to have_http_status(302)
27
+      end
28
+    end
29
+
30
+    context '@remote_follow is invalid' do
31
+      it 'returns 200' do
32
+        allow_any_instance_of(RemoteFollow).to receive(:valid?) { false }
33
+        post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } }
34
+
35
+        expect(response).to have_http_status(200)
36
+      end
37
+    end
38
+  end
39
+end

Loading…
Cancel
Save