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} /> 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 &&
  • ·
  • }
  • ·
  • ·
  • +
  • ·
  • ·
  • ·
  • ·
  • 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