Commit 60b10c77 authored by khws4v1's avatar khws4v1

Merge tag 'v3.0.1' into ef67.myhome.cx

parents 4fed5ff9 c4118ba7
This diff is collapsed.
......@@ -3,6 +3,39 @@ Changelog
All notable changes to this project will be documented in this file.
## [3.0.1] - 2019-10-10
### Added
- Add `tootctl media usage` command ([Gargron](https://github.com/tootsuite/mastodon/pull/12115))
- Add admin setting to auto-approve trending hashtags ([Gargron](https://github.com/tootsuite/mastodon/pull/12122), [Gargron](https://github.com/tootsuite/mastodon/pull/12130))
### Changed
- Change `tootctl media refresh` to skip already downloaded attachments ([Gargron](https://github.com/tootsuite/mastodon/pull/12118))
### Removed
- Remove auto-silence behaviour from spam check ([Gargron](https://github.com/tootsuite/mastodon/pull/12117))
- Remove HTML `lang` attribute from individual statuses in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12124))
- Remove fallback to long description on sidebar and meta description ([Gargron](https://github.com/tootsuite/mastodon/pull/12119))
### Fixed
- Fix preloaded JSON-LD context for identity not being used ([Gargron](https://github.com/tootsuite/mastodon/pull/12138))
- Fix media editing modal changing dimensions once the image loads ([Gargron](https://github.com/tootsuite/mastodon/pull/12131))
- Fix not showing whether a custom emoji has a local counterpart in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12135))
- Fix attachment not being re-downloaded even if file is not stored ([Gargron](https://github.com/tootsuite/mastodon/pull/12125))
- Fix old migration trying to use new column due to default status scope ([Gargron](https://github.com/tootsuite/mastodon/pull/12095))
- Fix column back button missing for not found accounts ([trwnh](https://github.com/tootsuite/mastodon/pull/12094))
- Fix issues with tootctl's parallelization and progress reporting ([Gargron](https://github.com/tootsuite/mastodon/pull/12093), [Gargron](https://github.com/tootsuite/mastodon/pull/12097))
- Fix existing user records with now-renamed `pt` locale ([Gargron](https://github.com/tootsuite/mastodon/pull/12092))
- Fix hashtag timeline REST API accepting too many hashtags ([Gargron](https://github.com/tootsuite/mastodon/pull/12091))
- Fix `GET /api/v1/instance` REST APIs being unavailable in secure mode ([Gargron](https://github.com/tootsuite/mastodon/pull/12089))
- Fix performance of home feed regeneration and merging ([Gargron](https://github.com/tootsuite/mastodon/pull/12084))
- Fix ffmpeg performance issues due to stdout buffer overflow ([hugogameiro](https://github.com/tootsuite/mastodon/pull/12088))
- Fix S3 adapter retrying failing uploads with exponential backoff ([Gargron](https://github.com/tootsuite/mastodon/pull/12085))
- Fix `tootctl accounts cull` advertising unused option flag ([Kjwon15](https://github.com/tootsuite/mastodon/pull/12074))
## [3.0.0] - 2019-10-03
### Added
......
......@@ -89,7 +89,7 @@ gem 'simple_form', '~> 4.1'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations', '~> 0.4'
gem 'tty-command', '~> 0.8', require: false
gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.19', require: false
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2019'
......@@ -116,7 +116,7 @@ end
group :test do
gem 'capybara', '~> 3.29'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.4'
gem 'faker', '~> 2.5'
gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
......
......@@ -93,7 +93,7 @@ GEM
tzinfo (~> 1.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.3.3)
airbrussh (1.3.4)
sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.5)
activerecord (>= 3.2, < 7.0)
......@@ -142,7 +142,7 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.0.0)
capistrano (3.11.1)
capistrano (3.11.2)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
......@@ -188,13 +188,14 @@ GEM
css_parser (1.7.0)
addressable
debug_inspector (0.0.3)
derailed_benchmarks (1.3.6)
derailed_benchmarks (1.4.0)
benchmark-ips (~> 2)
get_process_mem (~> 0)
heapy (~> 0)
memory_profiler (~> 0)
rack (>= 1)
rake (> 10, < 13)
ruby-statistics (>= 2.1)
thor (~> 0.19)
devise (4.7.1)
bcrypt (~> 3.0)
......@@ -233,13 +234,13 @@ GEM
faraday
multi_json
encryptor (3.0.0)
equatable (0.5.0)
equatable (0.6.1)
erubi (1.8.0)
et-orbi (1.1.6)
tzinfo
excon (0.62.0)
fabrication (2.20.2)
faker (2.4.0)
faker (2.5.0)
i18n (~> 1.6.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
......@@ -265,7 +266,8 @@ GEM
fuubar (2.4.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
get_process_mem (0.2.3)
get_process_mem (0.2.4)
ffi (~> 1.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
goldfinger (2.1.0)
......@@ -427,13 +429,13 @@ GEM
parser (2.6.4.0)
ast (~> 2.4.0)
parslet (1.8.2)
pastel (0.7.2)
equatable (~> 0.5.0)
tty-color (~> 0.4.0)
pastel (0.7.3)
equatable (~> 0.6)
tty-color (~> 0.5)
pg (1.1.4)
pghero (2.3.0)
activerecord (>= 5)
pkg-config (1.3.8)
pkg-config (1.3.9)
premailer (1.11.1)
addressable
css_parser (>= 1.6.0)
......@@ -568,6 +570,7 @@ GEM
ruby-progressbar (1.10.1)
ruby-saml (1.9.0)
nokogiri (>= 1.5.10)
ruby-statistics (2.1.1)
rufus-scheduler (3.5.2)
fugit (~> 1.1, >= 1.1.5)
safe_yaml (1.0.5)
......@@ -626,8 +629,8 @@ GEM
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.9)
tty-color (0.4.3)
tty-command (0.8.2)
tty-color (0.5.0)
tty-command (0.9.0)
pastel (~> 0.7.0)
tty-cursor (0.7.0)
tty-prompt (0.19.0)
......@@ -652,7 +655,7 @@ GEM
uniform_notifier (1.12.1)
warden (1.2.8)
rack (>= 2.0.6)
webmock (3.7.5)
webmock (3.7.6)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
......@@ -706,7 +709,7 @@ DEPENDENCIES
doorkeeper (~> 5.2)
dotenv-rails (~> 2.7)
fabrication (~> 2.20)
faker (~> 2.4)
faker (~> 2.5)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
......@@ -791,7 +794,7 @@ DEPENDENCIES
streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.4)
thor (~> 0.20)
tty-command (~> 0.8)
tty-command (~> 0.9)
tty-prompt (~> 0.19)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2019)
......
......@@ -55,7 +55,7 @@ Private posts, locked accounts, phrase filtering, muting, blocking and all sorts
**OAuth2 and a straightforward REST API**
Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Streaming APIs, resulting in a rich app ecosystem with a lot of choice!
Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Streaming APIs, resulting in a rich app ecosystem with a lot of choices!
## Deployment
......
......@@ -4,6 +4,7 @@ class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json
......
......@@ -4,6 +4,7 @@ class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json
......
......@@ -4,6 +4,7 @@ class Api::V1::InstancesController < Api::BaseController
respond_to :json
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
def show
expires_in 3.minutes, public: true
......
......@@ -5,11 +5,17 @@ class Api::V1::StreamingController < Api::BaseController
def index
if Rails.configuration.x.streaming_api_base_url != request.host
uri = URI.parse(request.url)
uri.host = URI.parse(Rails.configuration.x.streaming_api_base_url).host
redirect_to uri.to_s, status: 301
redirect_to streaming_api_url, status: 301
else
raise ActiveRecord::RecordNotFound
not_found
end
end
private
def streaming_api_url
Addressable::URI.parse(request.url).tap do |uri|
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
end.to_s
end
end
......@@ -13,7 +13,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
status: regeneration_in_progress? ? 206 : 200
status: account_home_feed.regenerating? ? 206 : 200
end
private
......@@ -62,8 +62,4 @@ class Api::V1::Timelines::HomeController < Api::BaseController
def pagination_since_id
@statuses.first.id
end
def regeneration_in_progress?
Redis.current.exists("account:#{current_account.id}:regeneration")
end
end
......@@ -97,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
done();
}).catch(error => {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
......
import React from 'react';
import PropTypes from 'prop-types';
export default class ExtendedVideoPlayer extends React.PureComponent {
export default class GIFV extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
time: PropTypes.number,
controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
onClick: PropTypes.func,
};
handleLoadedData = () => {
if (this.props.time) {
this.video.currentTime = this.props.time;
}
}
componentDidMount () {
this.video.addEventListener('loadeddata', this.handleLoadedData);
}
state = {
loading: true,
};
componentWillUnmount () {
this.video.removeEventListener('loadeddata', this.handleLoadedData);
handleLoadedData = () => {
this.setState({ loading: false });
}
setRef = (c) => {
this.video = c;
componentWillReceiveProps (nextProps) {
if (nextProps.src !== this.props.src) {
this.setState({ loading: true });
}
}
handleClick = e => {
e.stopPropagation();
const handler = this.props.onClick;
if (handler) handler();
const { onClick } = this.props;
if (onClick) {
e.stopPropagation();
onClick();
}
}
render () {
const { src, muted, controls, alt } = this.props;
const { src, width, height, alt } = this.props;
const { loading } = this.state;
return (
<div className='extended-video-player'>
<div className='gifv' style={{ position: 'relative' }}>
{loading && (
<canvas
width={width}
height={height}
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
onClick={this.handleClick}
/>
)}
<video
ref={this.setRef}
src={src}
autoPlay
width={width}
height={height}
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
muted={muted}
controls={controls}
loop={!controls}
muted
loop
autoPlay
playsInline
onClick={this.handleClick}
onLoadedData={this.handleLoadedData}
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
/>
</div>
);
......
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import illustration from 'mastodon/../images/elephant_ui_disappointed.svg';
import classNames from 'classnames';
const MissingIndicator = () => (
<div className='regeneration-indicator missing-indicator'>
<div>
<div className='regeneration-indicator__figure' />
const MissingIndicator = ({ fullPage }) => (
<div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}>
<div className='regeneration-indicator__figure'>
<img src={illustration} alt='' />
</div>
<div className='regeneration-indicator__label'>
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
</div>
<div className='regeneration-indicator__label'>
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
</div>
</div>
);
MissingIndicator.propTypes = {
fullPage: PropTypes.bool,
};
export default MissingIndicator;
import React from 'react';
import { FormattedMessage } from 'react-intl';
import illustration from 'mastodon/../images/elephant_ui_working.svg';
const MissingIndicator = () => (
<div className='regeneration-indicator'>
<div className='regeneration-indicator__figure'>
<img src={illustration} alt='' />
</div>
<div className='regeneration-indicator__label'>
<FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading&hellip;' />
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
</div>
</div>
);
export default MissingIndicator;
......@@ -216,14 +216,14 @@ export default class StatusContent extends React.PureComponent {
return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} />
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
</p>
{mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div>
......@@ -231,7 +231,7 @@ export default class StatusContent extends React.PureComponent {
} else if (this.props.onClick) {
const output = [
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div>,
......@@ -245,7 +245,7 @@ export default class StatusContent extends React.PureComponent {
} else {
return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div>
......
import { debounce } from 'lodash';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import LoadGap from './load_gap';
import ScrollableList from './scrollable_list';
import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
export default class StatusList extends ImmutablePureComponent {
......@@ -81,18 +81,7 @@ export default class StatusList extends ImmutablePureComponent {
const { isLoading, isPartial } = other;
if (isPartial) {
return (
<div className='regeneration-indicator'>
<div>
<div className='regeneration-indicator__figure' />
<div className='regeneration-indicator__label'>
<FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading&hellip;' />
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
</div>
</div>
</div>
);
return <RegenerationIndicator />;
}
let scrollableContent = (isLoading || statusIds.size > 0) ? (
......
......@@ -83,6 +83,7 @@ class AccountTimeline extends ImmutablePureComponent {
if (!isAccount) {
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<MissingIndicator />
</Column>
);
......
......@@ -4,7 +4,7 @@ import MissingIndicator from '../../components/missing_indicator';
const GenericNotFound = () => (
<Column>
<MissingIndicator />
<MissingIndicator fullPage />
</Column>
);
......
......@@ -16,6 +16,7 @@ import UploadProgress from 'mastodon/features/compose/components/upload_progress
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
import { length } from 'stringz';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import GIFV from 'mastodon/components/gifv';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
......@@ -41,6 +42,36 @@ const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
const assetHost = process.env.CDN_HOST || '';
class ImageLoader extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
};
state = {
loading: true,
};
componentDidMount() {
const image = new Image();
image.addEventListener('load', () => this.setState({ loading: false }));
image.src = this.props.src;
}
render () {
const { loading } = this.state;
if (loading) {
return <canvas width={this.props.width} height={this.props.height} />;
} else {
return <img {...this.props} alt='' />;
}
}
}
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class FocalPointModal extends ImmutablePureComponent {
......@@ -60,6 +91,7 @@ class FocalPointModal extends ImmutablePureComponent {
description: '',
dirty: false,
progress: 0,
loading: true,
};
componentWillMount () {
......@@ -242,8 +274,8 @@ class FocalPointModal extends ImmutablePureComponent {
<div className='focal-point-modal__content'>
{focals && (
<div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
{media.get('type') === 'image' && <img src={media.get('url')} width={width} height={height} alt='' />}
{media.get('type') === 'gifv' && <video src={media.get('url')} width={width} height={height} loop muted autoPlay />}
{media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
{media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />}
<div className='focal-point__preview'>
<strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
......
......@@ -3,13 +3,13 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Video from 'mastodon/features/video';
import ExtendedVideoPlayer from 'mastodon/components/extended_video_player';
import classNames from 'classnames';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImageLoader from './image_loader';
import Icon from 'mastodon/components/icon';
import GIFV from 'mastodon/components/gifv';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
......@@ -169,10 +169,8 @@ class MediaModal extends ImmutablePureComponent {
);
} else if (image.get('type') === 'gifv') {
return (
<ExtendedVideoPlayer
<GIFV
src={image.get('url')}
muted
controls={false}
width={width}
height={height}
key={image.get('preview_url')}
......
......@@ -115,7 +115,7 @@
"conversation.mark_as_read": "اعتبرها كمقروءة",
"conversation.open": "اعرض المحادثة",
"conversation.with": "بـ {names}",
"directory.federated": "From known fediverse",
"directory.federated": "مِن الفديفرس المعروف",
"directory.local": "مِن {domain} فقط",
"directory.new_arrivals": "الوافدون الجُدد",
"directory.recently_active": "نشط مؤخرا",
......@@ -155,7 +155,7 @@
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue",
"errors.unexpected_crash.report_issue": "الإبلاغ عن خلل",
"follow_request.authorize": "ترخيص",
"follow_request.reject": "رفض",
"getting_started.developers": "المُطوِّرون",
......@@ -178,7 +178,6 @@
"home.column_settings.basic": "الأساسية",
"home.column_settings.show_reblogs": "عرض الترقيات",
"home.column_settings.show_replies": "اعرض الردود",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
"intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
"intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}",