Skip to content

Commit

Permalink
Merge branch 'master' into glitch-soc/merge-upstream
Browse files Browse the repository at this point in the history
Conflicts:
- `config/routes.rb`:
  Upstream disabled E2EE routes, which we did earlier, but slightly
  differently. Took upstream's version.
  • Loading branch information
ClearlyClaire committed Jul 15, 2020
2 parents 7a23347 + d9cad44 commit 3f60b09
Show file tree
Hide file tree
Showing 156 changed files with 2,840 additions and 1,055 deletions.
2 changes: 1 addition & 1 deletion .env.production.sample
Expand Up @@ -2,7 +2,7 @@
# with the `rake mastodon:setup` interactive setup wizard, but to customize
# your setup even further, you'll need to edit it manually. This sample does
# not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon/admin/config/ for the full documentation.
# https://docs.joinmastodon.org/admin/config/ for the full documentation.

# Federation
# ----------
Expand Down
7 changes: 6 additions & 1 deletion .eslintrc.js
Expand Up @@ -199,6 +199,11 @@ module.exports = {
'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error',

'promise/catch-or-return': 'error',
'promise/catch-or-return': [
'error',
{
allowFinally: true,
},
],
},
};
4 changes: 4 additions & 0 deletions .rubocop.yml
Expand Up @@ -28,6 +28,10 @@ Layout/EmptyLineAfterMagicComment:
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: space

Lint/UselessAccessModifier:
ContextCreatingMethods:
- class_methods

Metrics/AbcSize:
Max: 100

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,13 @@ Changelog

All notable changes to this project will be documented in this file.

## [v3.1.5] - 2020-07-07
### Security

- Fix media attachment enumeration ([ThibG](https://github.com/tootsuite/mastodon/pull/14254))
- Change rate limits for various paths ([Gargron](https://github.com/tootsuite/mastodon/pull/14253))
- Fix other sessions not being logged out on password change ([Gargron](https://github.com/tootsuite/mastodon/pull/14252))

## [v3.1.4] - 2020-05-14
### Added

Expand Down
2 changes: 1 addition & 1 deletion app/chewy/statuses_index.rb
Expand Up @@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index
},
}

define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments), delete_if: ->(status) { status.searchable_by.empty? } do
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do
crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
Expand Down
10 changes: 8 additions & 2 deletions app/controllers/api/v1/statuses/reblogs_controller.rb
Expand Up @@ -5,7 +5,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController

before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user!
before_action :set_reblog
before_action :set_reblog, only: [:create]

override_rate_limit_headers :create, family: :statuses

Expand All @@ -16,15 +16,21 @@ def create
end

def destroy
@status = current_account.statuses.find_by(reblog_of_id: @reblog.id)
@status = current_account.statuses.find_by(reblog_of_id: params[:status_id])

if @status
authorize @status, :unreblog?
@status.discard
RemovalWorker.perform_async(@status.id)
@reblog = @status.reblog
else
@reblog = Status.find(params[:status_id])
authorize @reblog, :show?
end

render json: @reblog, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false })
rescue Mastodon::NotPermittedError
not_found
end

private
Expand Down
1 change: 1 addition & 0 deletions app/controllers/auth/sessions_controller.rb
Expand Up @@ -48,6 +48,7 @@ def find_user
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/tags_controller.rb
Expand Up @@ -28,7 +28,7 @@ def show
expires_in 0, public: true

limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE)
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(limit)
@statuses = cache_collection(@statuses, Status)

render xml: RSS::TagSerializer.render(@tag, @statuses)
Expand Down
Expand Up @@ -61,78 +61,82 @@ export default class MediaItem extends ImmutablePureComponent {
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
const height = width;
const status = attachment.get('status');
const title = status.get('spoiler_text') || attachment.get('description');
const title = status.get('spoiler_text') || attachment.get('description');

let thumbnail = '';
let icon;
let thumbnail, label, icon, content;

if (attachment.get('type') === 'unknown') {
// Skip
} else if (attachment.get('type') === 'audio') {
thumbnail = (
if (!visible) {
icon = (
<span className='account-gallery__item__icons'>
<Icon id='music' />
<Icon id='eye-slash' />
</span>
);
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;

thumbnail = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
title={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
const autoPlay = !isIOS() && autoPlayGif;
const label = attachment.get('type') === 'video' ? <Icon id='play' /> : 'GIF';

thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
} else {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
onLoad={this.handleImageLoad}
/>
);

if (attachment.get('type') === 'audio') {
label = <Icon id='music' />;
} else {
label = <Icon id='play' />;
}
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;

content = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (attachment.get('type') === 'gifv') {
content = (
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
autoPlay={!isIOS() && autoPlayGif}
loop
muted
/>
);

label = 'GIF';
}

thumbnail = (
<div className='media-gallery__gifv'>
{content}

<span className='media-gallery__gifv__label'>{label}</span>
</div>
);
}

if (!visible) {
icon = (
<span className='account-gallery__item__icons'>
<Icon id='eye-slash' />
</span>
);
}

return (
<div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<Blurhash
hash={attachment.get('blurhash')}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': visible && loaded,
})}
className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
dummy={!useBlurhash}
/>
{visible && thumbnail}
{!visible && icon}

{visible ? thumbnail : icon}
</a>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/mastodon/features/account_gallery/index.js
Expand Up @@ -101,9 +101,9 @@ class AccountGallery extends ImmutablePureComponent {

handleOpenMedia = attachment => {
if (attachment.get('type') === 'video') {
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') }));
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
} else if (attachment.get('type') === 'audio') {
this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status') }));
this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
Expand Down
14 changes: 12 additions & 2 deletions app/javascript/mastodon/features/audio/index.js
Expand Up @@ -37,6 +37,7 @@ class Audio extends React.PureComponent {
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
autoPlay: PropTypes.bool,
};

state = {
Expand Down Expand Up @@ -259,6 +260,14 @@ class Audio extends React.PureComponent {
this.setState({ hovered: false });
}

handleLoadedData = () => {
const { autoPlay } = this.props;

if (autoPlay) {
this.audio.play();
}
}

_initAudioContext () {
const context = new AudioContext();
const source = context.createMediaElementSource(this.audio);
Expand Down Expand Up @@ -336,7 +345,7 @@ class Audio extends React.PureComponent {
}

render () {
const { src, intl, alt, editable } = this.props;
const { src, intl, alt, editable, autoPlay } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
const progress = (currentTime / duration) * 100;

Expand All @@ -345,10 +354,11 @@ class Audio extends React.PureComponent {
<audio
src={src}
ref={this.setAudioRef}
preload='none'
preload={autoPlay ? 'auto' : 'none'}
onPlay={this.handlePlay}
onPause={this.handlePause}
onProgress={this.handleProgress}
onLoadedData={this.handleLoadedData}
crossOrigin='anonymous'
/>

Expand Down
18 changes: 15 additions & 3 deletions app/javascript/mastodon/features/ui/components/audio_modal.js
Expand Up @@ -2,17 +2,27 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { previewState } from './video_modal';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';

export default class AudioModal extends ImmutablePureComponent {
const mapStateToProps = (state, { status }) => ({
account: state.getIn(['accounts', status.get('account')]),
});

export default @connect(mapStateToProps)
class AudioModal extends ImmutablePureComponent {

static propTypes = {
media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map,
options: PropTypes.shape({
autoPlay: PropTypes.bool,
}),
account: ImmutablePropTypes.map,
onClose: PropTypes.func.isRequired,
};

Expand Down Expand Up @@ -50,7 +60,8 @@ export default class AudioModal extends ImmutablePureComponent {
}

render () {
const { media, status } = this.props;
const { media, status, account } = this.props;
const options = this.props.options || {};

return (
<div className='modal-root__modal audio-modal'>
Expand All @@ -60,10 +71,11 @@ export default class AudioModal extends ImmutablePureComponent {
alt={media.get('description')}
duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150}
poster={media.get('preview_url') || status.getIn(['account', 'avatar_static'])}
poster={media.get('preview_url') || account.get('avatar_static')}
backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])}
autoPlay={options.autoPlay}
/>
</div>

Expand Down
17 changes: 16 additions & 1 deletion app/javascript/mastodon/features/ui/components/boost_modal.js
Expand Up @@ -10,10 +10,15 @@ import DisplayName from '../../../components/display_name';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
import AttachmentList from 'mastodon/components/attachment_list';
import classNames from 'classnames';

const messages = defineMessages({
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});

export default @injectIntl
Expand Down Expand Up @@ -55,14 +60,24 @@ class BoostModal extends ImmutablePureComponent {
const { status, intl } = this.props;
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;

const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
};

const visibilityIcon = visibilityIconInfo[status.get('visibility')];

return (
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div className='status light'>
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>

<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
Expand Down

0 comments on commit 3f60b09

Please sign in to comment.