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

rebase/4.0.0rc2
Thibaut Girka 2018-08-19 09:27:18 +02:00
commit 88a0395a58
22 changed files with 108 additions and 28 deletions

View File

@ -10,6 +10,7 @@ gem 'rails', '~> 5.2.1'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.0' gem 'pg', '~> 1.0'
gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.1' gem 'pghero', '~> 2.1'
gem 'dotenv-rails', '~> 2.2', '< 2.3' gem 'dotenv-rails', '~> 2.2', '< 2.3'

View File

@ -324,6 +324,8 @@ GEM
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.0) mail (2.7.0)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
makara (0.4.0)
activerecord (>= 3.0.0)
marcel (0.3.2) marcel (0.3.2)
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
@ -700,6 +702,7 @@ DEPENDENCIES
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.10) lograge (~> 0.10)
makara (~> 0.4)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.0) microformats (~> 4.0)

View File

@ -30,6 +30,12 @@ module Admin
redirect_to admin_invites_path redirect_to admin_invites_path
end 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 private
def resource_params def resource_params

View File

@ -19,7 +19,7 @@ module StreamEntriesHelper
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('settings.edit_profile')]) safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('settings.edit_profile')])
end end
elsif current_account.following?(account) || current_account.requested?(account) 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')]) safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
end end
else else

View File

@ -140,7 +140,7 @@ export function redraft(status) {
}; };
}; };
export function deleteStatus(id, withRedraft = false) { export function deleteStatus(id, router, withRedraft = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
const status = getState().getIn(['statuses', id]); const status = getState().getIn(['statuses', id]);
@ -153,6 +153,10 @@ export function deleteStatus(id, withRedraft = false) {
if (withRedraft) { if (withRedraft) {
dispatch(redraft(status)); dispatch(redraft(status));
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
} }
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));

View File

@ -96,11 +96,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
} }
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status); this.props.onDelete(this.props.status, this.context.router.history);
} }
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, true); this.props.onDelete(this.props.status, this.context.router.history, true);
} }
handlePinClick = () => { handlePinClick = () => {

View File

@ -93,14 +93,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
})); }));
}, },
onDelete (status, withRedraft = false) { onDelete (status, history, withRedraft = false) {
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft)); dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)), onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
})); }));
} }
}, },

View File

@ -20,6 +20,7 @@ export default class Upload extends ImmutablePureComponent {
onUndo: PropTypes.func.isRequired, onUndo: PropTypes.func.isRequired,
onDescriptionChange: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired,
onOpenFocalPoint: PropTypes.func.isRequired, onOpenFocalPoint: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
}; };
state = { state = {
@ -28,6 +29,17 @@ export default class Upload extends ImmutablePureComponent {
dirtyDescription: null, dirtyDescription: null,
}; };
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
}
handleSubmit = () => {
this.handleInputBlur();
this.props.onSubmit();
}
handleUndoClick = () => { handleUndoClick = () => {
this.props.onUndo(this.props.media.get('id')); this.props.onUndo(this.props.media.get('id'));
} }
@ -93,6 +105,7 @@ export default class Upload extends ImmutablePureComponent {
onFocus={this.handleInputFocus} onFocus={this.handleInputFocus}
onChange={this.handleInputChange} onChange={this.handleInputChange}
onBlur={this.handleInputBlur} onBlur={this.handleInputBlur}
onKeyDown={this.handleKeyDown}
/> />
</label> </label>
</div> </div>

View File

@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import Upload from '../components/upload'; import Upload from '../components/upload';
import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose'; import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import { submitCompose } from '../../../actions/compose';
const mapStateToProps = (state, { id }) => ({ const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === 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 })); dispatch(openModal('FOCAL_POINT', { id }));
}, },
onSubmit () {
dispatch(submitCompose());
},
}); });
export default connect(mapStateToProps, mapDispatchToProps)(Upload); export default connect(mapStateToProps, mapDispatchToProps)(Upload);

View File

@ -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>} {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='/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='/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='/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='/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> <li><a href='https://github.com/tootsuite/documentation#documentation' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>

View File

@ -65,11 +65,11 @@ export default class ActionBar extends React.PureComponent {
} }
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status); this.props.onDelete(this.props.status, this.context.router.history);
} }
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, true); this.props.onDelete(this.props.status, this.context.router.history, true);
} }
handleDirectClick = () => { handleDirectClick = () => {

View File

@ -174,16 +174,16 @@ export default class Status extends ImmutablePureComponent {
} }
} }
handleDeleteClick = (status, withRedraft = false) => { handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props; const { dispatch, intl } = this.props;
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft)); dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)), onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
})); }));
} }
} }

View File

@ -35,6 +35,17 @@
transition: all 200ms ease-out; transition: all 200ms ease-out;
} }
&--destructive {
transition: none;
&:active,
&:focus,
&:hover {
background-color: $error-red;
transition: none;
}
}
&:disabled { &:disabled {
background-color: $ui-primary-color; background-color: $ui-primary-color;
cursor: default; cursor: default;

View File

@ -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) { @media screen and (max-width: $no-gap-breakpoint) {
svg { svg {
display: none; display: none;

View File

@ -42,7 +42,14 @@ class User < ApplicationRecord
include Settings::Extend include Settings::Extend
include Omniauthable 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, devise :two_factor_authenticatable,
otp_secret_encryption_key: Rails.configuration.x.otp_secret otp_secret_encryption_key: Rails.configuration.x.otp_secret

View File

@ -9,6 +9,10 @@ class InvitePolicy < ApplicationPolicy
min_required_role? min_required_role?
end end
def deactivate_all?
admin?
end
def destroy? def destroy?
owner? || (Setting.min_invite_role == 'admin' ? admin? : staff?) owner? || (Setting.min_invite_role == 'admin' ? admin? : staff?)
end end

View File

@ -25,7 +25,7 @@ class ProcessMentionsService < BaseService
end end
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) mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status)

View File

@ -9,14 +9,17 @@
%li= filter_link_to t('admin.invites.filter.available'), available: 1, expired: nil %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 %li= filter_link_to t('admin.invites.filter.expired'), available: nil, expired: 1
%hr.spacer/
- if policy(:invite).create? - if policy(:invite).create?
%p= t('invites.prompt') %p= t('invites.prompt')
= render 'invites/form' = render 'invites/form'
%hr/ %hr.spacer/
%table.table .table-wrapper
%table.table
%thead %thead
%tr %tr
%th %th
@ -28,3 +31,6 @@
= render @invites = render @invites
= paginate @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'

View File

@ -42,6 +42,6 @@
%h4= t 'footer.more' %h4= t 'footer.more'
%ul %ul
%li= link_to t('about.source_code'), Mastodon::Version.source_url %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' = render template: 'layouts/application'

View File

@ -6,6 +6,7 @@ en:
about_this: About about_this: About
administered_by: 'Administered by:' administered_by: 'Administered by:'
api: API 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. 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: Contact
contact_missing: Not set contact_missing: Not set
@ -281,6 +282,7 @@ en:
search: Search search: Search
title: Known instances title: Known instances
invites: invites:
deactivate_all: Deactivate all
filter: filter:
all: All all: All
available: Available available: Available

View File

@ -137,7 +137,12 @@ Rails.application.routes.draw do
resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
resources :action_logs, only: [:index] resources :action_logs, only: [:index]
resource :settings, only: [:edit, :update] 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 resources :relays, only: [:index, :new, :create, :destroy] do
member do member do

View File

@ -3,7 +3,7 @@ class CopyStatusStats < ActiveRecord::Migration[5.2]
def up def up
safety_assured do 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 execute <<-SQL.squish
INSERT INTO status_stats (status_id, reblogs_count, favourites_count, created_at, updated_at) INSERT INTO status_stats (status_id, reblogs_count, favourites_count, created_at, updated_at)
SELECT id, reblogs_count, favourites_count, created_at, updated_at SELECT id, reblogs_count, favourites_count, created_at, updated_at