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.main
commit
48b8a1f414
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,11 +1,29 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import NavigationBar from '../components/navigation_bar';
|
import NavigationBar from '../components/navigation_bar';
|
||||||
|
import { logOut } from 'mastodon/utils/log_out';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { me } from '../../../initial_state';
|
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 => {
|
const mapStateToProps = state => {
|
||||||
return {
|
return {
|
||||||
account: state.getIn(['accounts', me]),
|
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));
|
||||||
|
@ -1,35 +1,72 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
|
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 }) => (
|
const messages = defineMessages({
|
||||||
<div className='getting-started__footer'>
|
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||||
<ul>
|
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||||
{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>
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
|
onLogout () {
|
||||||
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
|
dispatch(openModal('CONFIRM', {
|
||||||
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
|
onConfirm: () => logOut(),
|
||||||
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
}));
|
||||||
</ul>
|
},
|
||||||
|
});
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
export default @injectIntl
|
||||||
id='getting_started.open_source_notice'
|
@connect(null, mapDispatchToProps)
|
||||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
class LinkFooter extends React.PureComponent {
|
||||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
|
||||||
/>
|
static propTypes = {
|
||||||
</p>
|
withHotkeys: PropTypes.bool,
|
||||||
</div>
|
onLogout: PropTypes.func.isRequired,
|
||||||
);
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
LinkFooter.propTypes = {
|
|
||||||
withHotkeys: PropTypes.bool,
|
handleLogoutClick = e => {
|
||||||
};
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.props.onLogout();
|
||||||
|
|
||||||
export default LinkFooter;
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import Rails from 'rails-ujs';
|
||||||
|
|
||||||
|
export const logOut = () => {
|
||||||
|
const form = document.createElement('form');
|
||||||
|
|
||||||
|
const methodInput = document.createElement('input');
|
||||||
|
methodInput.setAttribute('name', '_method');
|
||||||
|
methodInput.setAttribute('value', 'delete');
|
||||||
|
methodInput.setAttribute('type', 'hidden');
|
||||||
|
form.appendChild(methodInput);
|
||||||
|
|
||||||
|
const csrfToken = Rails.csrfToken();
|
||||||
|
const csrfParam = Rails.csrfParam();
|
||||||
|
|
||||||
|
if (csrfParam && csrfToken) {
|
||||||
|
const csrfInput = document.createElement('input');
|
||||||
|
csrfInput.setAttribute('name', csrfParam);
|
||||||
|
csrfInput.setAttribute('value', csrfToken);
|
||||||
|
csrfInput.setAttribute('type', 'hidden');
|
||||||
|
form.appendChild(csrfInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitButton = document.createElement('input');
|
||||||
|
submitButton.setAttribute('type', 'submit');
|
||||||
|
form.appendChild(submitButton);
|
||||||
|
|
||||||
|
form.method = 'post';
|
||||||
|
form.action = '/auth/sign_out';
|
||||||
|
form.style.display = 'none';
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
submitButton.click();
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddDeletedAtToStatuses < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :statuses, :deleted_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,13 @@
|
|||||||
|
class UpdateStatusesIndex < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at], where: 'deleted_at IS NULL', order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20190820 }
|
||||||
|
remove_index :statuses, name: :index_statuses_20180106
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106 }
|
||||||
|
remove_index :statuses, name: :index_statuses_20190820
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,11 @@
|
|||||||
|
class AddLocalIndexToStatuses < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_index :statuses, [:id, :account_id], name: :index_statuses_local_20190824, algorithm: :concurrently, order: { id: :desc }, where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_index :statuses, name: :index_statuses_local_20190824
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in new issue