about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock3
-rw-r--r--app/controllers/admin/invites_controller.rb6
-rw-r--r--app/helpers/stream_entries_helper.rb2
-rw-r--r--app/javascript/mastodon/actions/statuses.js6
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js4
-rw-r--r--app/javascript/mastodon/containers/status_container.js6
-rw-r--r--app/javascript/mastodon/features/compose/components/upload.js13
-rw-r--r--app/javascript/mastodon/features/compose/containers/upload_container.js5
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js1
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js4
-rw-r--r--app/javascript/mastodon/features/status/index.js6
-rw-r--r--app/javascript/styles/mastodon/components.scss11
-rw-r--r--app/javascript/styles/mastodon/stream_entries.scss12
-rw-r--r--app/models/user.rb9
-rw-r--r--app/policies/invite_policy.rb4
-rw-r--r--app/services/process_mentions_service.rb2
-rw-r--r--app/views/admin/invites/index.html.haml28
-rw-r--r--app/views/layouts/public.html.haml2
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/routes.rb7
-rw-r--r--db/migrate/20180812173710_copy_status_stats.rb2
22 files changed, 108 insertions, 28 deletions
diff --git a/Gemfile b/Gemfile
index 1b95426c5..6421693da 100644
--- a/Gemfile
+++ b/Gemfile
@@ -10,6 +10,7 @@ gem 'rails', '~> 5.2.1'
 
 gem 'hamlit-rails', '~> 0.2'
 gem 'pg', '~> 1.0'
+gem 'makara', '~> 0.4'
 gem 'pghero', '~> 2.1'
 gem 'dotenv-rails', '~> 2.2', '< 2.3'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 1bb512480..ed35f4a7b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -324,6 +324,8 @@ GEM
       nokogiri (>= 1.5.9)
     mail (2.7.0)
       mini_mime (>= 0.1.1)
+    makara (0.4.0)
+      activerecord (>= 3.0.0)
     marcel (0.3.2)
       mimemagic (~> 0.3.2)
     mario-redis-lock (1.2.1)
@@ -700,6 +702,7 @@ DEPENDENCIES
   letter_opener_web (~> 1.3)
   link_header (~> 0.0)
   lograge (~> 0.10)
+  makara (~> 0.4)
   mario-redis-lock (~> 1.2)
   memory_profiler
   microformats (~> 4.0)
diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb
index faccaa7c8..44a8eec77 100644
--- a/app/controllers/admin/invites_controller.rb
+++ b/app/controllers/admin/invites_controller.rb
@@ -30,6 +30,12 @@ module Admin
       redirect_to admin_invites_path
     end
 
+    def deactivate_all
+      authorize :invite, :deactivate_all?
+      Invite.available.in_batches.update_all(expires_at: Time.now.utc)
+      redirect_to admin_invites_path
+    end
+
     private
 
     def resource_params
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 121644263..9ded69436 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -19,7 +19,7 @@ module StreamEntriesHelper
           safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('settings.edit_profile')])
         end
       elsif current_account.following?(account) || current_account.requested?(account)
-        link_to account_unfollow_path(account), class: 'button logo-button', data: { method: :post } do
+        link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
           safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
         end
       else
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 3e1e5f270..8d5e72bec 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -140,7 +140,7 @@ export function redraft(status) {
   };
 };
 
-export function deleteStatus(id, withRedraft = false) {
+export function deleteStatus(id, router, withRedraft = false) {
   return (dispatch, getState) => {
     const status = getState().getIn(['statuses', id]);
 
@@ -153,6 +153,10 @@ export function deleteStatus(id, withRedraft = false) {
 
       if (withRedraft) {
         dispatch(redraft(status));
+
+        if (!getState().getIn(['compose', 'mounted'])) {
+          router.push('/statuses/new');
+        }
       }
     }).catch(error => {
       dispatch(deleteStatusFail(id, error));
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index c799d4e98..6d44a4b45 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -96,11 +96,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
   }
 
   handleDeleteClick = () => {
-    this.props.onDelete(this.props.status);
+    this.props.onDelete(this.props.status, this.context.router.history);
   }
 
   handleRedraftClick = () => {
-    this.props.onDelete(this.props.status, true);
+    this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
   handlePinClick = () => {
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index eb6329fdc..ed375c3e5 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -93,14 +93,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }));
   },
 
-  onDelete (status, withRedraft = false) {
+  onDelete (status, history, withRedraft = false) {
     if (!deleteModal) {
-      dispatch(deleteStatus(status.get('id'), withRedraft));
+      dispatch(deleteStatus(status.get('id'), history, withRedraft));
     } else {
       dispatch(openModal('CONFIRM', {
         message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
         confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
-        onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
+        onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
       }));
     }
   },
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index bfa2b4727..3d09217dc 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -20,6 +20,7 @@ export default class Upload extends ImmutablePureComponent {
     onUndo: PropTypes.func.isRequired,
     onDescriptionChange: PropTypes.func.isRequired,
     onOpenFocalPoint: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
   };
 
   state = {
@@ -28,6 +29,17 @@ export default class Upload extends ImmutablePureComponent {
     dirtyDescription: null,
   };
 
+  handleKeyDown = (e) => {
+    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+      this.handleSubmit();
+    }
+  }
+
+  handleSubmit = () => {
+    this.handleInputBlur();
+    this.props.onSubmit();
+  }
+
   handleUndoClick = () => {
     this.props.onUndo(this.props.media.get('id'));
   }
@@ -93,6 +105,7 @@ export default class Upload extends ImmutablePureComponent {
                     onFocus={this.handleInputFocus}
                     onChange={this.handleInputChange}
                     onBlur={this.handleInputBlur}
+                    onKeyDown={this.handleKeyDown}
                   />
                 </label>
               </div>
diff --git a/app/javascript/mastodon/features/compose/containers/upload_container.js b/app/javascript/mastodon/features/compose/containers/upload_container.js
index d6b57e5ff..9f3aab4bc 100644
--- a/app/javascript/mastodon/features/compose/containers/upload_container.js
+++ b/app/javascript/mastodon/features/compose/containers/upload_container.js
@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
 import Upload from '../components/upload';
 import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose';
 import { openModal } from '../../../actions/modal';
+import { submitCompose } from '../../../actions/compose';
 
 const mapStateToProps = (state, { id }) => ({
   media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
@@ -21,6 +22,10 @@ const mapDispatchToProps = dispatch => ({
     dispatch(openModal('FOCAL_POINT', { id }));
   },
 
+  onSubmit () {
+    dispatch(submitCompose());
+  },
+
 });
 
 export default connect(mapStateToProps, mapDispatchToProps)(Upload);
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 074ab01c8..95af8997e 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -139,6 +139,7 @@ export default class GettingStarted extends ImmutablePureComponent {
             {multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
             <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
             <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li>
+            <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
             <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
             <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
             <li><a href='https://github.com/tootsuite/documentation#documentation' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index 541499668..f5977c02c 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -65,11 +65,11 @@ export default class ActionBar extends React.PureComponent {
   }
 
   handleDeleteClick = () => {
-    this.props.onDelete(this.props.status);
+    this.props.onDelete(this.props.status, this.context.router.history);
   }
 
   handleRedraftClick = () => {
-    this.props.onDelete(this.props.status, true);
+    this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
   handleDirectClick = () => {
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 0ffeaa4dc..e506733b4 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -174,16 +174,16 @@ export default class Status extends ImmutablePureComponent {
     }
   }
 
-  handleDeleteClick = (status, withRedraft = false) => {
+  handleDeleteClick = (status, history, withRedraft = false) => {
     const { dispatch, intl } = this.props;
 
     if (!deleteModal) {
-      dispatch(deleteStatus(status.get('id'), withRedraft));
+      dispatch(deleteStatus(status.get('id'), history, withRedraft));
     } else {
       dispatch(openModal('CONFIRM', {
         message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
         confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
-        onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
+        onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
       }));
     }
   }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 57237d245..64a00c2c3 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -35,6 +35,17 @@
     transition: all 200ms ease-out;
   }
 
+  &--destructive {
+    transition: none;
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: $error-red;
+      transition: none;
+    }
+  }
+
   &:disabled {
     background-color: $ui-primary-color;
     cursor: default;
diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index 5aa809f76..14306c8bd 100644
--- a/app/javascript/styles/mastodon/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
@@ -110,6 +110,18 @@
     }
   }
 
+  &.button--destructive {
+    &:active,
+    &:focus,
+    &:hover {
+      background: $error-red;
+
+      svg path:last-child {
+        fill: $error-red;
+      }
+    }
+  }
+
   @media screen and (max-width: $no-gap-breakpoint) {
     svg {
       display: none;
diff --git a/app/models/user.rb b/app/models/user.rb
index 3e1b82962..8b65a900c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -42,7 +42,14 @@ class User < ApplicationRecord
   include Settings::Extend
   include Omniauthable
 
-  ACTIVE_DURATION = 7.days
+  # The home and list feeds will be stored in Redis for this amount
+  # of time, and status fan-out to followers will include only people
+  # within this time frame. Lowering the duration may improve performance
+  # if lots of people sign up, but not a lot of them check their feed
+  # every day. Raising the duration reduces the amount of expensive
+  # RegenerationWorker jobs that need to be run when those people come
+  # to check their feed
+  ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days
 
   devise :two_factor_authenticatable,
          otp_secret_encryption_key: Rails.configuration.x.otp_secret
diff --git a/app/policies/invite_policy.rb b/app/policies/invite_policy.rb
index a2a65f934..14236f78b 100644
--- a/app/policies/invite_policy.rb
+++ b/app/policies/invite_policy.rb
@@ -9,6 +9,10 @@ class InvitePolicy < ApplicationPolicy
     min_required_role?
   end
 
+  def deactivate_all?
+    admin?
+  end
+
   def destroy?
     owner? || (Setting.min_invite_role == 'admin' ? admin? : staff?)
   end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index 2ed6698cf..b4641c4b4 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -25,7 +25,7 @@ class ProcessMentionsService < BaseService
         end
       end
 
-      next match if mention_undeliverable?(mentioned_account)
+      next match if mention_undeliverable?(mentioned_account) || mentioned_account&.suspended
 
       mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status)
 
diff --git a/app/views/admin/invites/index.html.haml b/app/views/admin/invites/index.html.haml
index 944a60471..42159e9f3 100644
--- a/app/views/admin/invites/index.html.haml
+++ b/app/views/admin/invites/index.html.haml
@@ -9,22 +9,28 @@
       %li= filter_link_to t('admin.invites.filter.available'), available: 1, expired: nil
       %li= filter_link_to t('admin.invites.filter.expired'), available: nil, expired: 1
 
+%hr.spacer/
+
 - if policy(:invite).create?
   %p= t('invites.prompt')
 
   = render 'invites/form'
 
-  %hr/
+  %hr.spacer/
 
-%table.table
-  %thead
-    %tr
-      %th
-      %th= t('invites.table.uses')
-      %th= t('invites.table.expires_at')
-      %th
-      %th
-  %tbody
-    = render @invites
+.table-wrapper
+  %table.table
+    %thead
+      %tr
+        %th
+        %th= t('invites.table.uses')
+        %th= t('invites.table.expires_at')
+        %th
+        %th
+    %tbody
+      = render @invites
 
 = paginate @invites
+
+- if policy(:invite).deactivate_all?
+  = link_to t('admin.invites.deactivate_all'), deactivate_all_admin_invites_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 098262b2e..24911bb1e 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -42,6 +42,6 @@
             %h4= t 'footer.more'
             %ul
               %li= link_to t('about.source_code'), Mastodon::Version.source_url
-              %li= link_to 'joinmastodon.org', 'https://joinmastodon.org'
+              %li= link_to t('about.apps'), 'https://joinmastodon.org/apps'
 
 = render template: 'layouts/application'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3028ba5fc..1bffb309b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -6,6 +6,7 @@ en:
     about_this: About
     administered_by: 'Administered by:'
     api: API
+    apps: Mobile apps
     closed_registrations: Registrations are currently closed on this instance. However! You can find a different instance to make an account on and get access to the very same network from there.
     contact: Contact
     contact_missing: Not set
@@ -281,6 +282,7 @@ en:
       search: Search
       title: Known instances
     invites:
+      deactivate_all: Deactivate all
       filter:
         all: All
         available: Available
diff --git a/config/routes.rb b/config/routes.rb
index e06b2c1d2..3e0be9380 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -137,7 +137,12 @@ Rails.application.routes.draw do
     resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
     resources :action_logs, only: [:index]
     resource :settings, only: [:edit, :update]
-    resources :invites, only: [:index, :create, :destroy]
+
+    resources :invites, only: [:index, :create, :destroy] do
+      collection do
+        post :deactivate_all
+      end
+    end
 
     resources :relays, only: [:index, :new, :create, :destroy] do
       member do
diff --git a/db/migrate/20180812173710_copy_status_stats.rb b/db/migrate/20180812173710_copy_status_stats.rb
index 0c5907c30..850aa9c13 100644
--- a/db/migrate/20180812173710_copy_status_stats.rb
+++ b/db/migrate/20180812173710_copy_status_stats.rb
@@ -3,7 +3,7 @@ class CopyStatusStats < ActiveRecord::Migration[5.2]
 
   def up
     safety_assured do
-      Status.where.not(id: StatusStat.select('status_id')).select('id').find_in_batches do |statuses|
+      Status.unscoped.select('id').find_in_batches(batch_size: 5_000) do |statuses|
         execute <<-SQL.squish
           INSERT INTO status_stats (status_id, reblogs_count, favourites_count, created_at, updated_at)
           SELECT id, reblogs_count, favourites_count, created_at, updated_at