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

Conflicts:
- app/models/status.rb
- app/services/remove_status_service.rb
- db/schema.rb

All conflicts were due to the addition of a `deleted_at` attribute
to Statuses and reworked database indexes.
This commit is contained in:
Thibaut Girka
2019-08-29 12:07:50 +02:00
66 changed files with 848 additions and 186 deletions

View File

@@ -0,0 +1,226 @@
import React from 'react';
import PropTypes from 'prop-types';
import WaveSurfer from 'wavesurfer.js';
import { defineMessages, injectIntl } from 'react-intl';
import { formatTime } from 'mastodon/features/video';
import Icon from 'mastodon/components/icon';
import classNames from 'classnames';
import { throttle } from 'lodash';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
});
export default @injectIntl
class Audio extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
duration: PropTypes.number,
peaks: PropTypes.arrayOf(PropTypes.number),
height: PropTypes.number,
preload: PropTypes.bool,
editable: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
state = {
currentTime: 0,
duration: null,
paused: true,
muted: false,
volume: 0.5,
};
// hard coded in components.scss
// any way to get ::before values programatically?
volWidth = 50;
volOffset = 70;
volHandleOffset = v => {
const offset = v * this.volWidth + this.volOffset;
return (offset > 110) ? 110 : offset;
}
setVolumeRef = c => {
this.volume = c;
}
setWaveformRef = c => {
this.waveform = c;
}
componentDidMount () {
if (this.waveform) {
this._updateWaveform();
}
}
componentDidUpdate (prevProps) {
if (this.waveform && prevProps.src !== this.props.src) {
this._updateWaveform();
}
}
componentWillUnmount () {
if (this.wavesurfer) {
this.wavesurfer.destroy();
this.wavesurfer = null;
}
}
_updateWaveform () {
const { src, height, duration, peaks, preload } = this.props;
const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color');
const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color');
if (this.wavesurfer) {
this.wavesurfer.destroy();
this.loaded = false;
}
const wavesurfer = WaveSurfer.create({
container: this.waveform,
height,
barWidth: 3,
cursorWidth: 0,
progressColor,
waveColor,
backend: 'MediaElement',
interact: preload,
});
wavesurfer.setVolume(this.state.volume);
if (preload) {
wavesurfer.load(src);
this.loaded = true;
} else {
wavesurfer.load(src, peaks, 'none', duration);
this.loaded = false;
}
wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) }));
wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) }));
wavesurfer.on('pause', () => this.setState({ paused: true }));
wavesurfer.on('play', () => this.setState({ paused: false }));
wavesurfer.on('volume', volume => this.setState({ volume }));
wavesurfer.on('mute', muted => this.setState({ muted }));
this.wavesurfer = wavesurfer;
}
togglePlay = () => {
if (this.state.paused) {
if (!this.props.preload && !this.loaded) {
this.wavesurfer.createBackend();
this.wavesurfer.createPeakCache();
this.wavesurfer.load(this.props.src);
this.wavesurfer.toggleInteraction();
this.loaded = true;
}
this.wavesurfer.play();
this.setState({ paused: false });
} else {
this.wavesurfer.pause();
this.setState({ paused: true });
}
}
toggleMute = () => {
this.wavesurfer.setMute(!this.state.muted);
}
handleVolumeMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
document.addEventListener('touchmove', this.handleMouseVolSlide, true);
document.addEventListener('touchend', this.handleVolumeMouseUp, true);
this.handleMouseVolSlide(e);
e.preventDefault();
e.stopPropagation();
}
handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
}
handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect();
const x = (e.clientX - rect.left) / this.volWidth; // x position within the element.
if(!isNaN(x)) {
let slideamt = x;
if (x > 1) {
slideamt = 1;
} else if(x < 0) {
slideamt = 0;
}
this.wavesurfer.setVolume(slideamt);
}
}, 60);
render () {
const { height, intl, alt, editable } = this.props;
const { paused, muted, volume, currentTime } = this.state;
const volumeWidth = muted ? 0 : volume * this.volWidth;
const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
return (
<div className={classNames('audio-player', { editable })}>
<div className='audio-player__progress-placeholder' style={{ display: 'none' }} />
<div className='audio-player__wave-placeholder' style={{ display: 'none' }} />
<div
className='audio-player__waveform'
aria-label={alt}
title={alt}
style={{ height }}
ref={this.setWaveformRef}
/>
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span
className={classNames('video-player__volume__handle')}
tabIndex='0'
style={{ left: `${volumeHandleLoc}px` }}
/>
</div>
<span>
<span className='video-player__time-current'>{formatTime(currentTime)}</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
</span>
</div>
</div>
</div>
</div>
);
}
}

View File

@@ -23,9 +23,14 @@ class ActionBar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onLogout: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleLogout = () => {
this.props.onLogout();
}
render () {
const { intl } = this.props;
@@ -44,7 +49,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', target: null, method: 'delete' });
menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
return (
<div className='compose__action-bar'>

View File

@@ -12,6 +12,7 @@ export default class NavigationBar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func,
};
@@ -33,7 +34,7 @@ export default class NavigationBar extends ImmutablePureComponent {
<div className='navigation-bar__actions'>
<IconButton className='close' title='' icon='close' onClick={this.props.onClose} />
<ActionBar account={this.props.account} />
<ActionBar account={this.props.account} onLogout={this.props.onLogout} />
</div>
</div>
);

View File

@@ -1,11 +1,29 @@
import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import NavigationBar from '../components/navigation_bar';
import { logOut } from 'mastodon/utils/log_out';
import { openModal } from 'mastodon/actions/modal';
import { me } from '../../../initial_state';
const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
const mapStateToProps = state => {
return {
account: state.getIn(['accounts', me]),
};
};
export default connect(mapStateToProps)(NavigationBar);
const mapDispatchToProps = (dispatch, { intl }) => ({
onLogout () {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
onConfirm: () => logOut(),
}));
},
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));

View File

@@ -12,9 +12,11 @@ import Motion from '../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import SearchResultsContainer from './containers/search_results_container';
import { changeComposing } from '../../actions/compose';
import { openModal } from 'mastodon/actions/modal';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { mascot } from '../../initial_state';
import Icon from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -25,6 +27,8 @@ const messages = defineMessages({
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
const mapStateToProps = (state, ownProps) => ({
@@ -61,6 +65,21 @@ class Compose extends React.PureComponent {
}
}
handleLogoutClick = e => {
const { dispatch, intl } = this.props;
e.preventDefault();
e.stopPropagation();
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
onConfirm: () => logOut(),
}));
return false;
}
onFocus = () => {
this.props.dispatch(changeComposing(true));
}
@@ -92,7 +111,7 @@ class Compose extends React.PureComponent {
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
)}
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a>
<a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a>
</nav>
);
}

View File

@@ -3,6 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Hashtag from 'mastodon/components/hashtag';
import { FormattedMessage } from 'react-intl';
export default class Trends extends ImmutablePureComponent {
@@ -17,7 +18,7 @@ export default class Trends extends ImmutablePureComponent {
componentDidMount () {
this.props.fetchTrends();
this.refreshInterval = setInterval(() => this.props.fetchTrends(), 36000);
this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000);
}
componentWillUnmount () {
@@ -35,6 +36,8 @@ export default class Trends extends ImmutablePureComponent {
return (
<div className='getting-started__trends'>
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div>
);

View File

@@ -10,6 +10,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl';
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
@@ -107,7 +108,19 @@ export default class DetailedStatus extends ImmutablePureComponent {
}
if (status.get('media_attachments').size > 0) {
if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Audio
src={attachment.get('url')}
alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
height={110}
preload
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (

View File

@@ -10,6 +10,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import Button from 'mastodon/components/button';
import Video from 'mastodon/features/video';
import Audio from 'mastodon/features/audio';
import Textarea from 'react-textarea-autosize';
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
@@ -244,12 +245,23 @@ class FocalPointModal extends ImmutablePureComponent {
</div>
)}
{['audio', 'video'].includes(media.get('type')) && (
{media.get('type') === 'video' && (
<Video
preview={media.get('preview_url')}
blurhash={media.get('blurhash')}
src={media.get('url')}
detailed
inline
editable
/>
)}
{media.get('type') === 'audio' && (
<Audio
src={media.get('url')}
duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150}
preload
editable
/>
)}

View File

@@ -1,35 +1,72 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
import { logOut } from 'mastodon/utils/log_out';
import { openModal } from 'mastodon/actions/modal';
const LinkFooter = ({ withHotkeys }) => (
<div className='getting-started__footer'>
<ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{withHotkeys && <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 server' /></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://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
</ul>
const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
<p>
<FormattedMessage
id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
/>
</p>
</div>
);
const mapDispatchToProps = (dispatch, { intl }) => ({
onLogout () {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
onConfirm: () => logOut(),
}));
},
});
export default @injectIntl
@connect(null, mapDispatchToProps)
class LinkFooter extends React.PureComponent {
static propTypes = {
withHotkeys: PropTypes.bool,
onLogout: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleLogoutClick = e => {
e.preventDefault();
e.stopPropagation();
this.props.onLogout();
return false;
}
render () {
const { withHotkeys } = this.props;
return (
<div className='getting-started__footer'>
<ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{withHotkeys && <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 server' /></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://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
</ul>
<p>
<FormattedMessage
id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
/>
</p>
</div>
);
}
LinkFooter.propTypes = {
withHotkeys: PropTypes.bool,
};
export default LinkFooter;

View File

@@ -11,7 +11,7 @@ const mapStateToProps = (state, { intl }) => {
const value = notification[key];
if (typeof value === 'object') {
notification[key] = intl.formatMessage(value);
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
}
}));

View File

@@ -141,14 +141,24 @@ class SwitchingColumnsArea extends React.PureComponent {
return location.state !== previewMediaState && location.state !== previewVideoState;
}
handleResize = debounce(() => {
handleLayoutChange = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.onLayoutChange();
this.setState({ mobile: isMobile(window.innerWidth) });
}, 500, {
trailing: true,
});
})
handleResize = () => {
const mobile = isMobile(window.innerWidth);
if (mobile !== this.state.mobile) {
this.handleLayoutChange.cancel();
this.props.onLayoutChange();
this.setState({ mobile });
} else {
this.handleLayoutChange();
}
}
setRef = c => {
this.node = c.getWrappedInstance();

View File

@@ -137,3 +137,7 @@ export function Search () {
export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
}
export function Audio () {
return import(/* webpackChunkName: "features/audio" */'../../audio');
}

View File

@@ -21,7 +21,7 @@ const messages = defineMessages({
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
});
const formatTime = secondsNum => {
export const formatTime = secondsNum => {
let hours = Math.floor(secondsNum / 3600);
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
let seconds = secondsNum - (hours * 3600) - (minutes * 60);