Merge pull request #1208 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
2848c08953
3
Gemfile
3
Gemfile
@ -31,7 +31,7 @@ gem 'charlock_holmes', '~> 0.7.6'
|
|||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'chewy', '~> 5.0'
|
gem 'chewy', '~> 5.0'
|
||||||
gem 'cld3', '~> 3.2.4'
|
gem 'cld3', '~> 3.2.4'
|
||||||
gem 'devise', '~> 4.6'
|
gem 'devise', '~> 4.7'
|
||||||
gem 'devise-two-factor', '~> 3.1'
|
gem 'devise-two-factor', '~> 3.1'
|
||||||
|
|
||||||
group :pam_authentication, optional: true do
|
group :pam_authentication, optional: true do
|
||||||
@ -43,6 +43,7 @@ gem 'omniauth-cas', '~> 1.1'
|
|||||||
gem 'omniauth-saml', '~> 1.10'
|
gem 'omniauth-saml', '~> 1.10'
|
||||||
gem 'omniauth', '~> 1.9'
|
gem 'omniauth', '~> 1.9'
|
||||||
|
|
||||||
|
gem 'discard', '~> 1.1'
|
||||||
gem 'doorkeeper', '~> 5.1'
|
gem 'doorkeeper', '~> 5.1'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
|
13
Gemfile.lock
13
Gemfile.lock
@ -127,7 +127,7 @@ GEM
|
|||||||
brakeman (4.6.1)
|
brakeman (4.6.1)
|
||||||
browser (2.6.1)
|
browser (2.6.1)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (6.0.1)
|
bullet (6.0.2)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
bundler-audit (0.6.1)
|
bundler-audit (0.6.1)
|
||||||
@ -188,10 +188,10 @@ GEM
|
|||||||
rack (>= 1)
|
rack (>= 1)
|
||||||
rake (> 10, < 13)
|
rake (> 10, < 13)
|
||||||
thor (~> 0.19)
|
thor (~> 0.19)
|
||||||
devise (4.6.2)
|
devise (4.7.0)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0, < 6.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (3.1.0)
|
devise-two-factor (3.1.0)
|
||||||
@ -204,6 +204,8 @@ GEM
|
|||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.3)
|
||||||
|
discard (1.1.0)
|
||||||
|
activerecord (>= 4.2, < 7)
|
||||||
docile (1.3.2)
|
docile (1.3.2)
|
||||||
domain_name (0.5.20180417)
|
domain_name (0.5.20180417)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
@ -555,7 +557,7 @@ GEM
|
|||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 1.7)
|
unicode-display_width (>= 1.4.0, < 1.7)
|
||||||
rubocop-rails (2.3.0)
|
rubocop-rails (2.3.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.72.0)
|
rubocop (>= 0.72.0)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
@ -692,9 +694,10 @@ DEPENDENCIES
|
|||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
connection_pool
|
connection_pool
|
||||||
derailed_benchmarks
|
derailed_benchmarks
|
||||||
devise (~> 4.6)
|
devise (~> 4.7)
|
||||||
devise-two-factor (~> 3.1)
|
devise-two-factor (~> 3.1)
|
||||||
devise_pam_authenticatable2 (~> 9.2)
|
devise_pam_authenticatable2 (~> 9.2)
|
||||||
|
discard (~> 1.1)
|
||||||
doorkeeper (~> 5.1)
|
doorkeeper (~> 5.1)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
fabrication (~> 2.20)
|
fabrication (~> 2.20)
|
||||||
|
@ -5,7 +5,7 @@ module Admin
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true)
|
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true, include_statuses: true)
|
||||||
@warning_presets = AccountWarningPreset.all
|
@warning_presets = AccountWarningPreset.all
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification)
|
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -21,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def reported_status_ids
|
def reported_status_ids
|
||||||
reported_account.statuses.find(status_ids).pluck(:id)
|
reported_account.statuses.with_discarded.find(status_ids).pluck(:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_ids
|
def status_ids
|
||||||
|
@ -18,6 +18,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||||||
@reblogs_map = { @status.id => false }
|
@reblogs_map = { @status.id => false }
|
||||||
|
|
||||||
authorize status_for_destroy, :unreblog?
|
authorize status_for_destroy, :unreblog?
|
||||||
|
status_for_destroy.discard
|
||||||
RemovalWorker.perform_async(status_for_destroy.id)
|
RemovalWorker.perform_async(status_for_destroy.id)
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
|
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
|
||||||
@ -30,7 +31,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def status_for_destroy
|
def status_for_destroy
|
||||||
current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
|
@status_for_destroy ||= current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
|
||||||
end
|
end
|
||||||
|
|
||||||
def reblog_params
|
def reblog_params
|
||||||
|
@ -54,7 +54,8 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
@status = Status.where(account_id: current_user.account).find(params[:id])
|
@status = Status.where(account_id: current_user.account).find(params[:id])
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
RemovalWorker.perform_async(@status.id)
|
@status.discard
|
||||||
|
RemovalWorker.perform_async(@status.id, redraft: true)
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
end
|
end
|
||||||
|
@ -3,6 +3,8 @@ import { defineMessages } from 'react-intl';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
||||||
|
rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' },
|
||||||
|
rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ALERT_SHOW = 'ALERT_SHOW';
|
export const ALERT_SHOW = 'ALERT_SHOW';
|
||||||
@ -23,23 +25,29 @@ export function clearAlert() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
|
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
|
||||||
return {
|
return {
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
message_values,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showAlertForError(error) {
|
export function showAlertForError(error) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { data, status, statusText } = error.response;
|
const { data, status, statusText, headers } = error.response;
|
||||||
|
|
||||||
if (status === 404 || status === 410) {
|
if (status === 404 || status === 410) {
|
||||||
// Skip these errors as they are reflected in the UI
|
// Skip these errors as they are reflected in the UI
|
||||||
return { type: ALERT_NOOP };
|
return { type: ALERT_NOOP };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status === 429 && headers['x-ratelimit-reset']) {
|
||||||
|
const reset_date = new Date(headers['x-ratelimit-reset']);
|
||||||
|
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
|
||||||
|
}
|
||||||
|
|
||||||
let message = statusText;
|
let message = statusText;
|
||||||
let title = `${status}`;
|
let title = `${status}`;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import AttachmentList from './attachment_list';
|
|||||||
import Card from '../features/status/components/card';
|
import Card from '../features/status/components/card';
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
|
import { MediaGallery, Video, Audio } from 'flavours/glitch/util/async-components';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
|
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -443,11 +443,15 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLoadingMediaGallery () {
|
renderLoadingMediaGallery () {
|
||||||
return <div className='media_gallery' style={{ height: '110px' }} />;
|
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoadingVideoPlayer () {
|
renderLoadingVideoPlayer () {
|
||||||
return <div className='media-spoiler-video' style={{ height: '110px' }} />;
|
return <div className='video-player' style={{ height: '110px' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoadingAudioPlayer () {
|
||||||
|
return <div className='audio-player' style={{ height: '110px' }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@ -561,7 +565,24 @@ class Status extends ImmutablePureComponent {
|
|||||||
media={status.get('media_attachments')}
|
media={status.get('media_attachments')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (['video', 'audio'].includes(attachments.getIn([0, 'type']))) {
|
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
|
media = (
|
||||||
|
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||||
|
{Component => (
|
||||||
|
<Component
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={attachment.get('description')}
|
||||||
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
peaks={[0]}
|
||||||
|
height={70}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
|
mediaIcon = 'music';
|
||||||
|
} else if (attachments.getIn([0, 'type']) === 'video') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
@ -584,7 +605,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
/>)}
|
/>)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
|
mediaIcon = 'video-camera';
|
||||||
} else { // Media type is 'image' or 'gifv'
|
} else { // Media type is 'image' or 'gifv'
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
|
@ -212,7 +212,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
|
|
||||||
let element = e.target;
|
let element = e.target;
|
||||||
while (element) {
|
while (element) {
|
||||||
if (element.localName === 'button' || element.localName === 'video' || element.localName === 'a' || element.localName === 'label') {
|
if (['button', 'video', 'a', 'label', 'wave'].includes(element.localName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
element = element.parentNode;
|
element = element.parentNode;
|
||||||
|
@ -7,6 +7,7 @@ import MediaGallery from 'flavours/glitch/components/media_gallery';
|
|||||||
import Video from 'flavours/glitch/features/video';
|
import Video from 'flavours/glitch/features/video';
|
||||||
import Card from 'flavours/glitch/features/status/components/card';
|
import Card from 'flavours/glitch/features/status/components/card';
|
||||||
import Poll from 'flavours/glitch/components/poll';
|
import Poll from 'flavours/glitch/components/poll';
|
||||||
|
import Audio from 'flavours/glitch/features/audio';
|
||||||
import ModalRoot from 'flavours/glitch/components/modal_root';
|
import ModalRoot from 'flavours/glitch/components/modal_root';
|
||||||
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
||||||
import { List as ImmutableList, fromJS } from 'immutable';
|
import { List as ImmutableList, fromJS } from 'immutable';
|
||||||
@ -14,7 +15,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
|
|||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
|
||||||
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
|
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Audio };
|
||||||
|
|
||||||
export default class MediaContainer extends PureComponent {
|
export default class MediaContainer extends PureComponent {
|
||||||
|
|
||||||
|
226
app/javascript/flavours/glitch/features/audio/index.js
Normal file
226
app/javascript/flavours/glitch/features/audio/index.js
Normal 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 'flavours/glitch/features/video';
|
||||||
|
import Icon from 'flavours/glitch/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 icon={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||||
|
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon icon={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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -53,8 +53,18 @@ class Header extends ImmutablePureComponent {
|
|||||||
showNotificationsBadge: PropTypes.bool,
|
showNotificationsBadge: PropTypes.bool,
|
||||||
intl: PropTypes.object,
|
intl: PropTypes.object,
|
||||||
onSettingsClick: PropTypes.func,
|
onSettingsClick: PropTypes.func,
|
||||||
|
onLogout: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleLogoutClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.props.onLogout();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props;
|
const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props;
|
||||||
|
|
||||||
@ -114,7 +124,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
><Icon icon='cogs' /></a>
|
><Icon icon='cogs' /></a>
|
||||||
<a
|
<a
|
||||||
aria-label={intl.formatMessage(messages.logout)}
|
aria-label={intl.formatMessage(messages.logout)}
|
||||||
data-method='delete'
|
onClick={this.handleLogoutClick}
|
||||||
href={ signOutLink }
|
href={ signOutLink }
|
||||||
title={intl.formatMessage(messages.logout)}
|
title={intl.formatMessage(messages.logout)}
|
||||||
><Icon icon='sign-out' /></a>
|
><Icon icon='sign-out' /></a>
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
|
import { logOut } from 'flavours/glitch/util/log_out';
|
||||||
|
|
||||||
|
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 {
|
||||||
@ -16,6 +23,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dispatch(openModal('SETTINGS', {}));
|
dispatch(openModal('SETTINGS', {}));
|
||||||
},
|
},
|
||||||
|
onLogout () {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
onConfirm: () => logOut(),
|
||||||
|
}));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Header);
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Header));
|
||||||
|
@ -13,7 +13,7 @@ import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
|||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchLists } from 'flavours/glitch/actions/lists';
|
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||||
import { preferencesLink, signOutLink } from 'flavours/glitch/util/backend_links';
|
import { preferencesLink } from 'flavours/glitch/util/backend_links';
|
||||||
import NavigationBar from '../compose/components/navigation_bar';
|
import NavigationBar from '../compose/components/navigation_bar';
|
||||||
import LinkFooter from 'flavours/glitch/features/ui/components/link_footer';
|
import LinkFooter from 'flavours/glitch/features/ui/components/link_footer';
|
||||||
|
|
||||||
@ -30,7 +30,6 @@ const messages = defineMessages({
|
|||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
|
||||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||||
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
|
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
|
||||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||||
@ -174,7 +173,6 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
|||||||
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
||||||
{ preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> }
|
{ preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> }
|
||||||
<ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} />
|
<ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} />
|
||||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href={signOutLink} method='delete' />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LinkFooter />
|
<LinkFooter />
|
||||||
|
@ -11,6 +11,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl';
|
|||||||
import Card from './card';
|
import Card from './card';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Video from 'flavours/glitch/features/video';
|
import Video from 'flavours/glitch/features/video';
|
||||||
|
import Audio from 'flavours/glitch/features/audio';
|
||||||
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
|
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
|
||||||
import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
|
import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -131,7 +132,20 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
} else if (status.get('media_attachments').size > 0) {
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||||
media = <AttachmentList media={status.get('media_attachments')} />;
|
media = <AttachmentList media={status.get('media_attachments')} />;
|
||||||
} else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
|
} else 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
mediaIcon = 'music';
|
||||||
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
media = (
|
media = (
|
||||||
<Video
|
<Video
|
||||||
@ -150,7 +164,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
|
mediaIcon = 'video-camera';
|
||||||
} else {
|
} else {
|
||||||
media = (
|
media = (
|
||||||
<MediaGallery
|
<MediaGallery
|
||||||
|
@ -10,6 +10,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
|||||||
import IconButton from 'flavours/glitch/components/icon_button';
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
import Button from 'flavours/glitch/components/button';
|
import Button from 'flavours/glitch/components/button';
|
||||||
import Video from 'flavours/glitch/features/video';
|
import Video from 'flavours/glitch/features/video';
|
||||||
|
import Audio from 'flavours/glitch/features/audio';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
import UploadProgress from 'flavours/glitch/features/compose/components/upload_progress';
|
import UploadProgress from 'flavours/glitch/features/compose/components/upload_progress';
|
||||||
import CharacterCounter from 'flavours/glitch/features/compose/components/character_counter';
|
import CharacterCounter from 'flavours/glitch/features/compose/components/character_counter';
|
||||||
@ -244,12 +245,23 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{['audio', 'video'].includes(media.get('type')) && (
|
{media.get('type') === 'video' && (
|
||||||
<Video
|
<Video
|
||||||
preview={media.get('preview_url')}
|
preview={media.get('preview_url')}
|
||||||
blurhash={media.get('blurhash')}
|
blurhash={media.get('blurhash')}
|
||||||
src={media.get('url')}
|
src={media.get('url')}
|
||||||
detailed
|
detailed
|
||||||
|
inline
|
||||||
|
editable
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{media.get('type') === 'audio' && (
|
||||||
|
<Audio
|
||||||
|
src={media.get('url')}
|
||||||
|
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
height={150}
|
||||||
|
preload
|
||||||
editable
|
editable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -1,36 +1,71 @@
|
|||||||
|
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 'flavours/glitch/util/initial_state';
|
import { invitesEnabled, version, repository, source_url } from 'flavours/glitch/util/initial_state';
|
||||||
import { signOutLink } from 'flavours/glitch/util/backend_links';
|
import { signOutLink } from 'flavours/glitch/util/backend_links';
|
||||||
|
import { logOut } from 'flavours/glitch/util/log_out';
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
|
||||||
const LinkFooter = () => (
|
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>}
|
});
|
||||||
<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={signOutLink} data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
<FormattedMessage
|
onLogout () {
|
||||||
id='getting_started.open_source_notice'
|
dispatch(openModal('CONFIRM', {
|
||||||
defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.'
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
values={{
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
github: <span><a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a> (v{version})</span>,
|
onConfirm: () => logOut(),
|
||||||
Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a> }}
|
}));
|
||||||
/>
|
},
|
||||||
</p>
|
});
|
||||||
</div>
|
|
||||||
);
|
export default @injectIntl
|
||||||
|
@connect(null, mapDispatchToProps)
|
||||||
|
class LinkFooter extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onLogout: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLogoutClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.props.onLogout();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='getting-started__footer'>
|
||||||
|
<ul>
|
||||||
|
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></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 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={signOutLink} onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='getting_started.open_source_notice'
|
||||||
|
defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.'
|
||||||
|
values={{
|
||||||
|
github: <span><a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a> (v{version})</span>,
|
||||||
|
Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a> }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
LinkFooter.propTypes = {
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LinkFooter;
|
|
||||||
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { intl }) => {
|
|||||||
const value = notification[key];
|
const value = notification[key];
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
notification[key] = intl.formatMessage(value);
|
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -138,14 +138,24 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize = debounce(() => {
|
handleLayoutChange = debounce(() => {
|
||||||
// The cached heights are no longer accurate, invalidate
|
// The cached heights are no longer accurate, invalidate
|
||||||
this.props.onLayoutChange();
|
this.props.onLayoutChange();
|
||||||
|
|
||||||
this.setState({ mobile: isMobile(window.innerWidth, this.props.layout) });
|
|
||||||
}, 500, {
|
}, 500, {
|
||||||
trailing: true,
|
trailing: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
const mobile = isMobile(window.innerWidth, this.props.layout);
|
||||||
|
|
||||||
|
if (mobile !== this.state.mobile) {
|
||||||
|
this.handleLayoutChange.cancel();
|
||||||
|
this.props.onLayoutChange();
|
||||||
|
this.setState({ mobile });
|
||||||
|
} else {
|
||||||
|
this.handleLayoutChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
this.node = c.getWrappedInstance();
|
this.node = c.getWrappedInstance();
|
||||||
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
|||||||
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatTime = secondsNum => {
|
export const formatTime = secondsNum => {
|
||||||
let hours = Math.floor(secondsNum / 3600);
|
let hours = Math.floor(secondsNum / 3600);
|
||||||
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
|
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
|
||||||
let seconds = secondsNum - (hours * 3600) - (minutes * 60);
|
let seconds = secondsNum - (hours * 3600) - (minutes * 60);
|
||||||
|
@ -14,6 +14,7 @@ export default function alerts(state = initialState, action) {
|
|||||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||||
title: action.title,
|
title: action.title,
|
||||||
message: action.message,
|
message: action.message,
|
||||||
|
message_values: action.message_values,
|
||||||
}));
|
}));
|
||||||
case ALERT_DISMISS:
|
case ALERT_DISMISS:
|
||||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||||
|
@ -157,6 +157,7 @@ export const getAlerts = createSelector([getAlertsBase], (base) => {
|
|||||||
base.forEach(item => {
|
base.forEach(item => {
|
||||||
arr.push({
|
arr.push({
|
||||||
message: item.get('message'),
|
message: item.get('message'),
|
||||||
|
message_values: item.get('message_values'),
|
||||||
title: item.get('title'),
|
title: item.get('title'),
|
||||||
key: item.get('key'),
|
key: item.get('key'),
|
||||||
dismissAfter: 5000,
|
dismissAfter: 5000,
|
||||||
|
@ -333,15 +333,63 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-player {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
background: darken($ui-base-color, 8%);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-bottom: 44px;
|
||||||
|
|
||||||
|
&.editable {
|
||||||
|
border-radius: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__waveform {
|
||||||
|
padding: 15px 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
border-top: 1px solid lighten($ui-base-color, 4%);
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-placeholder {
|
||||||
|
background-color: rgba(lighten($ui-highlight-color, 8%), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wave-placeholder {
|
||||||
|
background-color: lighten($ui-base-color, 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player__controls {
|
||||||
|
padding: 0 15px;
|
||||||
|
padding-top: 10px;
|
||||||
|
background: darken($ui-base-color, 8%);
|
||||||
|
border-top: 1px solid lighten($ui-base-color, 4%);
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $base-shadow-color;
|
background: $base-shadow-color;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
&.editable {
|
&.editable {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
@ -107,7 +107,8 @@
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
.media-gallery,
|
.media-gallery,
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +132,8 @@
|
|||||||
|
|
||||||
.media-gallery,
|
.media-gallery,
|
||||||
&__action-bar,
|
&__action-bar,
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,8 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
animation: fade 150ms linear;
|
animation: fade 150ms linear;
|
||||||
|
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,7 +454,8 @@
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
@ -561,7 +563,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -372,3 +372,10 @@
|
|||||||
.directory__tag > div {
|
.directory__tag > div {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-player .video-player__controls button,
|
||||||
|
.audio-player .video-player__time-sep,
|
||||||
|
.audio-player .video-player__time-current,
|
||||||
|
.audio-player .video-player__time-total {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
@ -138,6 +138,10 @@ export function Video () {
|
|||||||
return import(/* webpackChunkName: "flavours/glitch/async/video" */'flavours/glitch/features/video');
|
return import(/* webpackChunkName: "flavours/glitch/async/video" */'flavours/glitch/features/video');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Audio () {
|
||||||
|
return import(/* webpackChunkName: "features/glitch/async/audio" */'flavours/glitch/features/audio');
|
||||||
|
}
|
||||||
|
|
||||||
export function EmbedModal () {
|
export function EmbedModal () {
|
||||||
return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'flavours/glitch/features/ui/components/embed_modal');
|
return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'flavours/glitch/features/ui/components/embed_modal');
|
||||||
}
|
}
|
||||||
|
34
app/javascript/flavours/glitch/util/log_out.js
Normal file
34
app/javascript/flavours/glitch/util/log_out.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Rails from 'rails-ujs';
|
||||||
|
import { signOutLink } from 'flavours/glitch/util/backend_links';
|
||||||
|
|
||||||
|
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 = signOutLink;
|
||||||
|
form.style.display = 'none';
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
submitButton.click();
|
||||||
|
};
|
@ -3,6 +3,8 @@ import { defineMessages } from 'react-intl';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
||||||
|
rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' },
|
||||||
|
rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ALERT_SHOW = 'ALERT_SHOW';
|
export const ALERT_SHOW = 'ALERT_SHOW';
|
||||||
@ -23,23 +25,29 @@ export function clearAlert() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
|
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
|
||||||
return {
|
return {
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
message_values,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showAlertForError(error) {
|
export function showAlertForError(error) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { data, status, statusText } = error.response;
|
const { data, status, statusText, headers } = error.response;
|
||||||
|
|
||||||
if (status === 404 || status === 410) {
|
if (status === 404 || status === 410) {
|
||||||
// Skip these errors as they are reflected in the UI
|
// Skip these errors as they are reflected in the UI
|
||||||
return { type: ALERT_NOOP };
|
return { type: ALERT_NOOP };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status === 429 && headers['x-ratelimit-reset']) {
|
||||||
|
const reset_date = new Date(headers['x-ratelimit-reset']);
|
||||||
|
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
|
||||||
|
}
|
||||||
|
|
||||||
let message = statusText;
|
let message = statusText;
|
||||||
let title = `${status}`;
|
let title = `${status}`;
|
||||||
|
|
||||||
|
@ -356,6 +356,8 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
|||||||
cancelFetchComposeSuggestionsTags();
|
cancelFetchComposeSuggestionsTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(updateSuggestionTags(token));
|
||||||
|
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
cancelToken: new CancelToken(cancel => {
|
cancelToken: new CancelToken(cancel => {
|
||||||
cancelFetchComposeSuggestionsTags = cancel;
|
cancelFetchComposeSuggestionsTags = cancel;
|
||||||
|
@ -9,18 +9,18 @@ export default class AutosuggestHashtag extends React.PureComponent {
|
|||||||
tag: PropTypes.shape({
|
tag: PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
history: PropTypes.array.isRequired,
|
history: PropTypes.array,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { tag } = this.props;
|
const { tag } = this.props;
|
||||||
const weeklyUses = shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
|
const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-hashtag'>
|
<div className='autosuggest-hashtag'>
|
||||||
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
|
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
|
||||||
<div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>
|
{tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,19 @@ export default class ColumnBackButton extends React.PureComponent {
|
|||||||
if (multiColumn) {
|
if (multiColumn) {
|
||||||
return component;
|
return component;
|
||||||
} else {
|
} else {
|
||||||
return createPortal(component, document.getElementById('tabs-bar__portal'));
|
// The portal container and the component may be rendered to the DOM in
|
||||||
|
// the same React render pass, so the container might not be available at
|
||||||
|
// the time `render()` is called.
|
||||||
|
const container = document.getElementById('tabs-bar__portal');
|
||||||
|
if (container === null) {
|
||||||
|
// The container wasn't available, force a re-render so that the
|
||||||
|
// component can eventually be inserted in the container and not scroll
|
||||||
|
// with the rest of the area.
|
||||||
|
this.forceUpdate();
|
||||||
|
return component;
|
||||||
|
} else {
|
||||||
|
return createPortal(component, container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +178,19 @@ class ColumnHeader extends React.PureComponent {
|
|||||||
if (multiColumn || placeholder) {
|
if (multiColumn || placeholder) {
|
||||||
return component;
|
return component;
|
||||||
} else {
|
} else {
|
||||||
return createPortal(component, document.getElementById('tabs-bar__portal'));
|
// The portal container and the component may be rendered to the DOM in
|
||||||
|
// the same React render pass, so the container might not be available at
|
||||||
|
// the time `render()` is called.
|
||||||
|
const container = document.getElementById('tabs-bar__portal');
|
||||||
|
if (container === null) {
|
||||||
|
// The container wasn't available, force a re-render so that the
|
||||||
|
// component can eventually be inserted in the container and not scroll
|
||||||
|
// with the rest of the area.
|
||||||
|
this.forceUpdate();
|
||||||
|
return component;
|
||||||
|
} else {
|
||||||
|
return createPortal(component, container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import AttachmentList from './attachment_list';
|
|||||||
import Card from '../features/status/components/card';
|
import Card from '../features/status/components/card';
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { MediaGallery, Video } from '../features/ui/util/async-components';
|
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
@ -199,11 +199,15 @@ class Status extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderLoadingMediaGallery () {
|
renderLoadingMediaGallery () {
|
||||||
return <div className='media_gallery' style={{ height: '110px' }} />;
|
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoadingVideoPlayer () {
|
renderLoadingVideoPlayer () {
|
||||||
return <div className='media-spoiler-video' style={{ height: '110px' }} />;
|
return <div className='video-player' style={{ height: '110px' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoadingAudioPlayer () {
|
||||||
|
return <div className='audio-player' style={{ height: '110px' }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenVideo = (media, startTime) => {
|
handleOpenVideo = (media, startTime) => {
|
||||||
@ -348,7 +352,23 @@ class Status extends ImmutablePureComponent {
|
|||||||
media={status.get('media_attachments')}
|
media={status.get('media_attachments')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
|
media = (
|
||||||
|
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||||
|
{Component => (
|
||||||
|
<Component
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={attachment.get('description')}
|
||||||
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
peaks={[0]}
|
||||||
|
height={70}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
|
@ -230,7 +230,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
} else if (this.props.onClick) {
|
} else if (this.props.onClick) {
|
||||||
const output = [
|
const output = [
|
||||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
<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} lang={status.get('language')} />
|
||||||
|
|
||||||
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||||
|
@ -8,6 +8,7 @@ import Video from '../features/video';
|
|||||||
import Card from '../features/status/components/card';
|
import Card from '../features/status/components/card';
|
||||||
import Poll from 'mastodon/components/poll';
|
import Poll from 'mastodon/components/poll';
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
import Hashtag from 'mastodon/components/hashtag';
|
||||||
|
import Audio from 'mastodon/features/audio';
|
||||||
import ModalRoot from '../components/modal_root';
|
import ModalRoot from '../components/modal_root';
|
||||||
import { getScrollbarWidth } from '../features/ui/components/modal_root';
|
import { getScrollbarWidth } from '../features/ui/components/modal_root';
|
||||||
import MediaModal from '../features/ui/components/media_modal';
|
import MediaModal from '../features/ui/components/media_modal';
|
||||||
@ -16,7 +17,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
|
|||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
|
||||||
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag };
|
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
|
||||||
|
|
||||||
export default class MediaContainer extends PureComponent {
|
export default class MediaContainer extends PureComponent {
|
||||||
|
|
||||||
|
226
app/javascript/mastodon/features/audio/index.js
Normal file
226
app/javascript/mastodon/features/audio/index.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,9 +23,14 @@ class ActionBar extends React.PureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
onLogout: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleLogout = () => {
|
||||||
|
this.props.onLogout();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl } = this.props;
|
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.domain_blocks), to: '/domain_blocks' });
|
||||||
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
||||||
menu.push(null);
|
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 (
|
return (
|
||||||
<div className='compose__action-bar'>
|
<div className='compose__action-bar'>
|
||||||
|
@ -12,6 +12,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
onLogout: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<div className='navigation-bar__actions'>
|
<div className='navigation-bar__actions'>
|
||||||
<IconButton className='close' title='' icon='close' onClick={this.props.onClose} />
|
<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>
|
||||||
</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));
|
||||||
|
@ -12,9 +12,11 @@ import Motion from '../ui/util/optional_motion';
|
|||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import SearchResultsContainer from './containers/search_results_container';
|
import SearchResultsContainer from './containers/search_results_container';
|
||||||
import { changeComposing } from '../../actions/compose';
|
import { changeComposing } from '../../actions/compose';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
|
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
|
||||||
import { mascot } from '../../initial_state';
|
import { mascot } from '../../initial_state';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import { logOut } from 'mastodon/utils/log_out';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
@ -25,6 +27,8 @@ const messages = defineMessages({
|
|||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
|
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) => ({
|
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 = () => {
|
onFocus = () => {
|
||||||
this.props.dispatch(changeComposing(true));
|
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>
|
<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='/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>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
import Hashtag from 'mastodon/components/hashtag';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default class Trends extends ImmutablePureComponent {
|
export default class Trends extends ImmutablePureComponent {
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ export default class Trends extends ImmutablePureComponent {
|
|||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.props.fetchTrends();
|
this.props.fetchTrends();
|
||||||
this.refreshInterval = setInterval(() => this.props.fetchTrends(), 36000);
|
this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@ -35,6 +36,8 @@ export default class Trends extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='getting-started__trends'>
|
<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} />)}
|
{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl';
|
|||||||
import Card from './card';
|
import Card from './card';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Video from '../../video';
|
import Video from '../../video';
|
||||||
|
import Audio from '../../audio';
|
||||||
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
@ -107,7 +108,19 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0) {
|
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]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
|
@ -10,6 +10,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
|||||||
import IconButton from 'mastodon/components/icon_button';
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import Button from 'mastodon/components/button';
|
import Button from 'mastodon/components/button';
|
||||||
import Video from 'mastodon/features/video';
|
import Video from 'mastodon/features/video';
|
||||||
|
import Audio from 'mastodon/features/audio';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
|
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
|
||||||
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
|
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
|
||||||
@ -244,12 +245,23 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{['audio', 'video'].includes(media.get('type')) && (
|
{media.get('type') === 'video' && (
|
||||||
<Video
|
<Video
|
||||||
preview={media.get('preview_url')}
|
preview={media.get('preview_url')}
|
||||||
blurhash={media.get('blurhash')}
|
blurhash={media.get('blurhash')}
|
||||||
src={media.get('url')}
|
src={media.get('url')}
|
||||||
detailed
|
detailed
|
||||||
|
inline
|
||||||
|
editable
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{media.get('type') === 'audio' && (
|
||||||
|
<Audio
|
||||||
|
src={media.get('url')}
|
||||||
|
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
height={150}
|
||||||
|
preload
|
||||||
editable
|
editable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<p>
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
<FormattedMessage
|
onLogout () {
|
||||||
id='getting_started.open_source_notice'
|
dispatch(openModal('CONFIRM', {
|
||||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
/>
|
onConfirm: () => logOut(),
|
||||||
</p>
|
}));
|
||||||
</div>
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
|
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;
|
|
||||||
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { intl }) => {
|
|||||||
const value = notification[key];
|
const value = notification[key];
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
notification[key] = intl.formatMessage(value);
|
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -141,14 +141,24 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||||||
return location.state !== previewMediaState && location.state !== previewVideoState;
|
return location.state !== previewMediaState && location.state !== previewVideoState;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize = debounce(() => {
|
handleLayoutChange = debounce(() => {
|
||||||
// The cached heights are no longer accurate, invalidate
|
// The cached heights are no longer accurate, invalidate
|
||||||
this.props.onLayoutChange();
|
this.props.onLayoutChange();
|
||||||
|
|
||||||
this.setState({ mobile: isMobile(window.innerWidth) });
|
|
||||||
}, 500, {
|
}, 500, {
|
||||||
trailing: true,
|
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 => {
|
setRef = c => {
|
||||||
this.node = c.getWrappedInstance();
|
this.node = c.getWrappedInstance();
|
||||||
|
@ -137,3 +137,7 @@ export function Search () {
|
|||||||
export function Tesseract () {
|
export function Tesseract () {
|
||||||
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Audio () {
|
||||||
|
return import(/* webpackChunkName: "features/audio" */'../../audio');
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ const messages = defineMessages({
|
|||||||
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatTime = secondsNum => {
|
export const formatTime = secondsNum => {
|
||||||
let hours = Math.floor(secondsNum / 3600);
|
let hours = Math.floor(secondsNum / 3600);
|
||||||
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
|
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
|
||||||
let seconds = secondsNum - (hours * 3600) - (minutes * 60);
|
let seconds = secondsNum - (hours * 3600) - (minutes * 60);
|
||||||
|
@ -741,6 +741,27 @@
|
|||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/account/components/header.json"
|
"path": "app/javascript/mastodon/features/account/components/header.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Play",
|
||||||
|
"id": "video.play"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Pause",
|
||||||
|
"id": "video.pause"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Mute sound",
|
||||||
|
"id": "video.mute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unmute sound",
|
||||||
|
"id": "video.unmute"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/audio/index.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
@ -1096,15 +1117,6 @@
|
|||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/compose/components/upload_form.json"
|
"path": "app/javascript/mastodon/features/compose/components/upload_form.json"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"descriptors": [
|
|
||||||
{
|
|
||||||
"defaultMessage": "Uploading...",
|
|
||||||
"id": "upload_progress.label"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"path": "app/javascript/mastodon/features/compose/components/upload_progress.json"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
@ -1317,8 +1329,8 @@
|
|||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
"defaultMessage": "Refresh",
|
"defaultMessage": "Trending now",
|
||||||
"id": "trends.refresh"
|
"id": "trends.trending_now"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/getting_started/components/trends.json"
|
"path": "app/javascript/mastodon/features/getting_started/components/trends.json"
|
||||||
@ -1456,6 +1468,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Basic",
|
||||||
|
"id": "home.column_settings.basic"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Show boosts",
|
"defaultMessage": "Show boosts",
|
||||||
"id": "home.column_settings.show_reblogs"
|
"id": "home.column_settings.show_reblogs"
|
||||||
@ -1837,14 +1853,6 @@
|
|||||||
"defaultMessage": "Push notifications",
|
"defaultMessage": "Push notifications",
|
||||||
"id": "notifications.column_settings.push"
|
"id": "notifications.column_settings.push"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"defaultMessage": "Basic",
|
|
||||||
"id": "home.column_settings.basic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Update in real-time",
|
|
||||||
"id": "home.column_settings.update_live"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"defaultMessage": "Quick filter bar",
|
"defaultMessage": "Quick filter bar",
|
||||||
"id": "notifications.column_settings.filter_bar.category"
|
"id": "notifications.column_settings.filter_bar.category"
|
||||||
@ -1903,10 +1911,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
|
||||||
"defaultMessage": "and {count, plural, one {# other} other {# others}}",
|
|
||||||
"id": "notification.and_n_others"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"defaultMessage": "{name} followed you",
|
"defaultMessage": "{name} followed you",
|
||||||
"id": "notification.follow"
|
"id": "notification.follow"
|
||||||
|
@ -162,7 +162,6 @@
|
|||||||
"home.column_settings.basic": "Basic",
|
"home.column_settings.basic": "Basic",
|
||||||
"home.column_settings.show_reblogs": "Show boosts",
|
"home.column_settings.show_reblogs": "Show boosts",
|
||||||
"home.column_settings.show_replies": "Show replies",
|
"home.column_settings.show_replies": "Show replies",
|
||||||
"home.column_settings.update_live": "Update in real-time",
|
|
||||||
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
|
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
|
||||||
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
|
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
|
||||||
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
|
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
|
||||||
@ -258,7 +257,6 @@
|
|||||||
"navigation_bar.profile_directory": "Profile directory",
|
"navigation_bar.profile_directory": "Profile directory",
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"navigation_bar.security": "Security",
|
"navigation_bar.security": "Security",
|
||||||
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
|
|
||||||
"notification.favourite": "{name} favourited your status",
|
"notification.favourite": "{name} favourited your status",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
"notification.mention": "{name} mentioned you",
|
"notification.mention": "{name} mentioned you",
|
||||||
@ -378,7 +376,7 @@
|
|||||||
"time_remaining.moments": "Moments remaining",
|
"time_remaining.moments": "Moments remaining",
|
||||||
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
|
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
|
||||||
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
|
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
|
||||||
"trends.refresh": "Refresh",
|
"trends.trending_now": "Trending now",
|
||||||
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
|
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
|
||||||
"upload_area.title": "Drag & drop to upload",
|
"upload_area.title": "Drag & drop to upload",
|
||||||
"upload_button.label": "Add media ({formats})",
|
"upload_button.label": "Add media ({formats})",
|
||||||
|
@ -14,6 +14,7 @@ export default function alerts(state = initialState, action) {
|
|||||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||||
title: action.title,
|
title: action.title,
|
||||||
message: action.message,
|
message: action.message,
|
||||||
|
message_values: action.message_values,
|
||||||
}));
|
}));
|
||||||
case ALERT_DISMISS:
|
case ALERT_DISMISS:
|
||||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
COMPOSE_SUGGESTIONS_CLEAR,
|
COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
COMPOSE_SUGGESTIONS_READY,
|
COMPOSE_SUGGESTIONS_READY,
|
||||||
COMPOSE_SUGGESTION_SELECT,
|
COMPOSE_SUGGESTION_SELECT,
|
||||||
|
COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||||
COMPOSE_TAG_HISTORY_UPDATE,
|
COMPOSE_TAG_HISTORY_UPDATE,
|
||||||
COMPOSE_SENSITIVITY_CHANGE,
|
COMPOSE_SENSITIVITY_CHANGE,
|
||||||
COMPOSE_SPOILERNESS_CHANGE,
|
COMPOSE_SPOILERNESS_CHANGE,
|
||||||
@ -205,16 +206,36 @@ const expiresInFromExpiresAt = expires_at => {
|
|||||||
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
|
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSuggestions = (state, { accounts, emojis, tags }) => {
|
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||||
|
prefix = prefix.toLowerCase();
|
||||||
|
if (suggestions.length < 4) {
|
||||||
|
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
||||||
|
return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||||
|
} else {
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
||||||
if (accounts) {
|
if (accounts) {
|
||||||
return accounts.map(item => ({ id: item.id, type: 'account' }));
|
return accounts.map(item => ({ id: item.id, type: 'account' }));
|
||||||
} else if (emojis) {
|
} else if (emojis) {
|
||||||
return emojis.map(item => ({ ...item, type: 'emoji' }));
|
return emojis.map(item => ({ ...item, type: 'emoji' }));
|
||||||
} else {
|
} else {
|
||||||
return sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' })));
|
return mergeLocalHashtagResults(sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' }))), token.slice(1), state.get('tagHistory'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSuggestionTags = (state, token) => {
|
||||||
|
const prefix = token.slice(1);
|
||||||
|
|
||||||
|
const suggestions = state.get('suggestions').toJS();
|
||||||
|
return state.merge({
|
||||||
|
suggestions: ImmutableList(mergeLocalHashtagResults(suggestions, prefix, state.get('tagHistory'))),
|
||||||
|
suggestion_token: token,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function compose(state = initialState, action) {
|
export default function compose(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STORE_HYDRATE:
|
case STORE_HYDRATE:
|
||||||
@ -328,6 +349,8 @@ export default function compose(state = initialState, action) {
|
|||||||
return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token);
|
return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token);
|
||||||
case COMPOSE_SUGGESTION_SELECT:
|
case COMPOSE_SUGGESTION_SELECT:
|
||||||
return insertSuggestion(state, action.position, action.token, action.completion, action.path);
|
return insertSuggestion(state, action.position, action.token, action.completion, action.path);
|
||||||
|
case COMPOSE_SUGGESTION_TAGS_UPDATE:
|
||||||
|
return updateSuggestionTags(state, action.token);
|
||||||
case COMPOSE_TAG_HISTORY_UPDATE:
|
case COMPOSE_TAG_HISTORY_UPDATE:
|
||||||
return state.set('tagHistory', fromJS(action.tags));
|
return state.set('tagHistory', fromJS(action.tags));
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
|
@ -128,6 +128,7 @@ export const getAlerts = createSelector([getAlertsBase], (base) => {
|
|||||||
base.forEach(item => {
|
base.forEach(item => {
|
||||||
arr.push({
|
arr.push({
|
||||||
message: item.get('message'),
|
message: item.get('message'),
|
||||||
|
message_values: item.get('message_values'),
|
||||||
title: item.get('title'),
|
title: item.get('title'),
|
||||||
key: item.get('key'),
|
key: item.get('key'),
|
||||||
dismissAfter: 5000,
|
dismissAfter: 5000,
|
||||||
|
33
app/javascript/mastodon/utils/log_out.js
Normal file
33
app/javascript/mastodon/utils/log_out.js
Normal file
@ -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();
|
||||||
|
};
|
@ -457,6 +457,13 @@ h5 {
|
|||||||
.status {
|
.status {
|
||||||
padding-bottom: 32px;
|
padding-bottom: 32px;
|
||||||
|
|
||||||
|
&--highlighted {
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-header {
|
.status-header {
|
||||||
td {
|
td {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -104,7 +104,8 @@ html {
|
|||||||
.box-widget input[type="email"],
|
.box-widget input[type="email"],
|
||||||
.box-widget input[type="password"],
|
.box-widget input[type="password"],
|
||||||
.box-widget textarea,
|
.box-widget textarea,
|
||||||
.statuses-grid .detailed-status {
|
.statuses-grid .detailed-status,
|
||||||
|
.audio-player {
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -700,3 +701,10 @@ html {
|
|||||||
.compose-form .compose-form__warning {
|
.compose-form .compose-form__warning {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-player .video-player__controls button,
|
||||||
|
.audio-player .video-player__time-sep,
|
||||||
|
.audio-player .video-player__time-current,
|
||||||
|
.audio-player .video-player__time-total {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
@ -948,7 +948,8 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
animation: fade 150ms linear;
|
animation: fade 150ms linear;
|
||||||
|
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1043,7 +1044,8 @@
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
@ -1154,7 +1156,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2130,7 +2133,8 @@ a.account__display-name {
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
.media-gallery,
|
.media-gallery,
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2172,7 +2176,8 @@ a.account__display-name {
|
|||||||
|
|
||||||
.media-gallery,
|
.media-gallery,
|
||||||
&__action-bar,
|
&__action-bar,
|
||||||
.video-player {
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2765,6 +2770,15 @@ a.account__display-name {
|
|||||||
animation: fade 150ms linear;
|
animation: fade 150ms linear;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: $darker-text-color;
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-height: 810px) {
|
@media screen and (max-height: 810px) {
|
||||||
.trends__item:nth-child(3) {
|
.trends__item:nth-child(3) {
|
||||||
display: none;
|
display: none;
|
||||||
@ -5034,15 +5048,63 @@ a.status-card.compact:hover {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-player {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
background: darken($ui-base-color, 8%);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-bottom: 44px;
|
||||||
|
|
||||||
|
&.editable {
|
||||||
|
border-radius: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__waveform {
|
||||||
|
padding: 15px 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
border-top: 1px solid lighten($ui-base-color, 4%);
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-placeholder {
|
||||||
|
background-color: rgba(lighten($ui-highlight-color, 8%), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wave-placeholder {
|
||||||
|
background-color: lighten($ui-base-color, 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player__controls {
|
||||||
|
padding: 0 15px;
|
||||||
|
padding-top: 10px;
|
||||||
|
background: darken($ui-base-color, 8%);
|
||||||
|
border-top: 1px solid lighten($ui-base-color, 4%);
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $base-shadow-color;
|
background: $base-shadow-color;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
&.editable {
|
&.editable {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
@ -70,7 +70,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete_now!
|
def delete_now!
|
||||||
RemoveStatusService.new.call(@status)
|
RemoveStatusService.new.call(@status, redraft: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
|
@ -5,6 +5,7 @@ class UserMailer < Devise::Mailer
|
|||||||
|
|
||||||
helper :application
|
helper :application
|
||||||
helper :instance
|
helper :instance
|
||||||
|
helper :statuses
|
||||||
|
|
||||||
add_template_helper RoutingHelper
|
add_template_helper RoutingHelper
|
||||||
|
|
||||||
@ -79,10 +80,11 @@ class UserMailer < Devise::Mailer
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def warning(user, warning)
|
def warning(user, warning, status_ids = nil)
|
||||||
@resource = user
|
@resource = user
|
||||||
@warning = warning
|
@warning = warning
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
@statuses = Status.where(id: status_ids).includes(:account) if status_ids.is_a?(Array)
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email,
|
mail to: @resource.email,
|
||||||
|
@ -19,20 +19,25 @@ class Admin::AccountAction
|
|||||||
:report_id,
|
:report_id,
|
||||||
:warning_preset_id
|
:warning_preset_id
|
||||||
|
|
||||||
attr_reader :warning, :send_email_notification
|
attr_reader :warning, :send_email_notification, :include_statuses
|
||||||
|
|
||||||
def send_email_notification=(value)
|
def send_email_notification=(value)
|
||||||
@send_email_notification = ActiveModel::Type::Boolean.new.cast(value)
|
@send_email_notification = ActiveModel::Type::Boolean.new.cast(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_statuses=(value)
|
||||||
|
@include_statuses = ActiveModel::Type::Boolean.new.cast(value)
|
||||||
|
end
|
||||||
|
|
||||||
def save!
|
def save!
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
process_action!
|
process_action!
|
||||||
process_warning!
|
process_warning!
|
||||||
end
|
end
|
||||||
|
|
||||||
queue_email!
|
process_email!
|
||||||
process_reports!
|
process_reports!
|
||||||
|
process_queue!
|
||||||
end
|
end
|
||||||
|
|
||||||
def report
|
def report
|
||||||
@ -110,7 +115,6 @@ class Admin::AccountAction
|
|||||||
authorize(target_account, :suspend?)
|
authorize(target_account, :suspend?)
|
||||||
log_action(:suspend, target_account)
|
log_action(:suspend, target_account)
|
||||||
target_account.suspend!
|
target_account.suspend!
|
||||||
queue_suspension_worker!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def text_for_warning
|
def text_for_warning
|
||||||
@ -121,16 +125,22 @@ class Admin::AccountAction
|
|||||||
Admin::SuspensionWorker.perform_async(target_account.id)
|
Admin::SuspensionWorker.perform_async(target_account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def queue_email!
|
def process_queue!
|
||||||
return unless warnable?
|
queue_suspension_worker! if type == 'suspend'
|
||||||
|
end
|
||||||
|
|
||||||
UserMailer.warning(target_account.user, warning).deliver_later!
|
def process_email!
|
||||||
|
UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
|
||||||
end
|
end
|
||||||
|
|
||||||
def warnable?
|
def warnable?
|
||||||
send_email_notification && target_account.local?
|
send_email_notification && target_account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_ids
|
||||||
|
@report.status_ids if @report && include_statuses
|
||||||
|
end
|
||||||
|
|
||||||
def warning_preset
|
def warning_preset
|
||||||
@warning_preset ||= AccountWarningPreset.find(warning_preset_id) if warning_preset_id.present?
|
@warning_preset ||= AccountWarningPreset.find(warning_preset_id) if warning_preset_id.present?
|
||||||
end
|
end
|
||||||
|
@ -34,7 +34,8 @@ class Form::StatusBatch
|
|||||||
|
|
||||||
def delete_statuses
|
def delete_statuses
|
||||||
Status.where(id: status_ids).reorder(nil).find_each do |status|
|
Status.where(id: status_ids).reorder(nil).find_each do |status|
|
||||||
RemovalWorker.perform_async(status.id)
|
status.discard
|
||||||
|
RemovalWorker.perform_async(status.id, redraft: false)
|
||||||
Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true)
|
Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true)
|
||||||
log_action :destroy, status
|
log_action :destroy, status
|
||||||
end
|
end
|
||||||
|
@ -43,7 +43,7 @@ class Report < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def statuses
|
def statuses
|
||||||
Status.where(id: status_ids).includes(:account, :media_attachments, :mentions)
|
Status.with_discarded.where(id: status_ids).includes(:account, :media_attachments, :mentions)
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_attachments
|
def media_attachments
|
||||||
|
@ -25,15 +25,19 @@
|
|||||||
# full_status_text :text default(""), not null
|
# full_status_text :text default(""), not null
|
||||||
# poll_id :bigint(8)
|
# poll_id :bigint(8)
|
||||||
# content_type :string
|
# content_type :string
|
||||||
|
# deleted_at :datetime
|
||||||
#
|
#
|
||||||
|
|
||||||
class Status < ApplicationRecord
|
class Status < ApplicationRecord
|
||||||
before_destroy :unlink_from_conversations
|
before_destroy :unlink_from_conversations
|
||||||
|
|
||||||
|
include Discard::Model
|
||||||
include Paginable
|
include Paginable
|
||||||
include Cacheable
|
include Cacheable
|
||||||
include StatusThreadingConcern
|
include StatusThreadingConcern
|
||||||
|
|
||||||
|
self.discard_column = :deleted_at
|
||||||
|
|
||||||
# If `override_timestamps` is set at creation time, Snowflake ID creation
|
# If `override_timestamps` is set at creation time, Snowflake ID creation
|
||||||
# will be based on current time instead of `created_at`
|
# will be based on current time instead of `created_at`
|
||||||
attr_accessor :override_timestamps
|
attr_accessor :override_timestamps
|
||||||
@ -77,7 +81,7 @@ class Status < ApplicationRecord
|
|||||||
|
|
||||||
accepts_nested_attributes_for :poll
|
accepts_nested_attributes_for :poll
|
||||||
|
|
||||||
default_scope { recent }
|
default_scope { recent.kept }
|
||||||
|
|
||||||
scope :recent, -> { reorder(id: :desc) }
|
scope :recent, -> { reorder(id: :desc) }
|
||||||
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
||||||
|
@ -8,7 +8,7 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
# Dispatch Salmon deletes, unique per domain, of the deleted statuses, but only local ones
|
# Dispatch Salmon deletes, unique per domain, of the deleted statuses, but only local ones
|
||||||
# Remove statuses from home feeds
|
# Remove statuses from home feeds
|
||||||
# Push delete events to streaming API for home feeds and public feeds
|
# Push delete events to streaming API for home feeds and public feeds
|
||||||
# @param [Status] statuses A preferably batched array of statuses
|
# @param [Enumerable<Status>] statuses A preferably batched array of statuses
|
||||||
# @param [Hash] options
|
# @param [Hash] options
|
||||||
# @option [Boolean] :skip_side_effects
|
# @option [Boolean] :skip_side_effects
|
||||||
def call(statuses, **options)
|
def call(statuses, **options)
|
||||||
|
@ -4,6 +4,11 @@ class RemoveStatusService < BaseService
|
|||||||
include Redisable
|
include Redisable
|
||||||
include Payloadable
|
include Payloadable
|
||||||
|
|
||||||
|
# Delete a status
|
||||||
|
# @param [Status] status
|
||||||
|
# @param [Hash] options
|
||||||
|
# @option [Boolean] :redraft
|
||||||
|
# @options [Boolean] :original_removed
|
||||||
def call(status, **options)
|
def call(status, **options)
|
||||||
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||||
@status = status
|
@status = status
|
||||||
@ -25,6 +30,7 @@ class RemoveStatusService < BaseService
|
|||||||
remove_from_media if status.media_attachments.any?
|
remove_from_media if status.media_attachments.any?
|
||||||
remove_from_direct if status.direct_visibility?
|
remove_from_direct if status.direct_visibility?
|
||||||
remove_from_spam_check
|
remove_from_spam_check
|
||||||
|
remove_media
|
||||||
|
|
||||||
@status.destroy!
|
@status.destroy!
|
||||||
else
|
else
|
||||||
@ -151,6 +157,12 @@ class RemoveStatusService < BaseService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove_media
|
||||||
|
return if @options[:redraft]
|
||||||
|
|
||||||
|
@status.media_attachments.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
def remove_from_spam_check
|
def remove_from_spam_check
|
||||||
redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id)
|
redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id)
|
||||||
end
|
end
|
||||||
|
@ -13,6 +13,10 @@
|
|||||||
.fields-group
|
.fields-group
|
||||||
= f.input :send_email_notification, as: :boolean, wrapper: :with_label
|
= f.input :send_email_notification, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
|
- if params[:report_id].present?
|
||||||
|
.fields-group
|
||||||
|
= f.input :include_statuses, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
- unless @warning_presets.empty?
|
- unless @warning_presets.empty?
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
%li
|
%li
|
||||||
= feature_hint(t('admin.dashboard.authorized_fetch_mode'), @authorized_fetch)
|
= feature_hint(t('admin.dashboard.authorized_fetch_mode'), @authorized_fetch)
|
||||||
%li
|
%li
|
||||||
= feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_mode)
|
= feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_enabled)
|
||||||
%li
|
%li
|
||||||
= feature_hint('LDAP', @ldap_enabled)
|
= feature_hint('LDAP', @ldap_enabled)
|
||||||
%li
|
%li
|
||||||
|
@ -16,11 +16,14 @@
|
|||||||
- video = status.proper.media_attachments.first
|
- video = status.proper.media_attachments.first
|
||||||
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description
|
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description
|
||||||
- else
|
- else
|
||||||
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
|
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
|
||||||
|
|
||||||
.detailed-status__meta
|
.detailed-status__meta
|
||||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
|
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
|
||||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||||
|
- if status.discarded?
|
||||||
|
·
|
||||||
|
%span.negative-hint= t('admin.statuses.deleted')
|
||||||
·
|
·
|
||||||
- if status.reblog?
|
- if status.reblog?
|
||||||
= fa_icon('retweet fw')
|
= fa_icon('retweet fw')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
- i ||= 0
|
- i ||= 0
|
||||||
|
- highlighted ||= false
|
||||||
|
|
||||||
%table.email-table{ cellspacing: 0, cellpadding: 0, dir: 'ltr' }
|
%table.email-table{ cellspacing: 0, cellpadding: 0, dir: 'ltr' }
|
||||||
%tbody
|
%tbody
|
||||||
@ -14,7 +15,7 @@
|
|||||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
%tbody
|
%tbody
|
||||||
%tr
|
%tr
|
||||||
%td.column-cell.padded.status
|
%td.column-cell.padded.status{ class: highlighted ? 'status--highlighted' : '' }
|
||||||
%table.status-header{ cellspacing: 0, cellpadding: 0 }
|
%table.status-header{ cellspacing: 0, cellpadding: 0 }
|
||||||
%tbody
|
%tbody
|
||||||
%tr
|
%tr
|
||||||
@ -32,5 +33,10 @@
|
|||||||
%div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
|
%div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
|
||||||
= Formatter.instance.format(status)
|
= Formatter.instance.format(status)
|
||||||
|
|
||||||
|
- if status.media_attachments.size > 0
|
||||||
|
%p
|
||||||
|
- status.media_attachments.each do |a|
|
||||||
|
= link_to medium_url(a), medium_url(a)
|
||||||
|
|
||||||
%p.status-footer
|
%p.status-footer
|
||||||
= link_to l(status.created_at), web_url("statuses/#{status.id}")
|
= link_to l(status.created_at), web_url("statuses/#{status.id}")
|
||||||
|
@ -27,10 +27,14 @@
|
|||||||
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
|
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
|
||||||
|
|
||||||
- if !status.media_attachments.empty?
|
- if !status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.audio_or_video?
|
- if status.media_attachments.first.video?
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
|
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
|
- elsif status.media_attachments.first.audio?
|
||||||
|
- audio = status.media_attachments.first
|
||||||
|
= react_component :audio, src: audio.file.url(:original), height: 130, alt: audio.description, preload: true, duration: audio.file.meta.dig(:original, :duration) do
|
||||||
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- else
|
- else
|
||||||
= react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
= react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
|
@ -31,10 +31,14 @@
|
|||||||
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
|
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
|
||||||
|
|
||||||
- if !status.media_attachments.empty?
|
- if !status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.audio_or_video?
|
- if status.media_attachments.first.video?
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
|
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
|
- elsif status.media_attachments.first.audio?
|
||||||
|
- audio = status.media_attachments.first
|
||||||
|
= react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do
|
||||||
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- else
|
- else
|
||||||
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
|
@ -42,6 +42,14 @@
|
|||||||
- unless @warning.text.blank?
|
- unless @warning.text.blank?
|
||||||
= Formatter.instance.linkify(@warning.text)
|
= Formatter.instance.linkify(@warning.text)
|
||||||
|
|
||||||
|
- unless @statuses.empty?
|
||||||
|
%p
|
||||||
|
%strong= t('user_mailer.warning.statuses')
|
||||||
|
|
||||||
|
- unless @statuses.empty?
|
||||||
|
- @statuses.each_with_index do |status, i|
|
||||||
|
= render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
|
||||||
|
|
||||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
%tbody
|
%tbody
|
||||||
%tr
|
%tr
|
||||||
@ -50,7 +58,7 @@
|
|||||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
%tbody
|
%tbody
|
||||||
%tr
|
%tr
|
||||||
%td.content-cell
|
%td.content-cell{ class: @statuses.empty? ? '' : 'content-start' }
|
||||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
%tbody
|
%tbody
|
||||||
%tr
|
%tr
|
||||||
@ -61,3 +69,20 @@
|
|||||||
%td.button-primary
|
%td.button-primary
|
||||||
= link_to about_more_url do
|
= link_to about_more_url do
|
||||||
%span= t 'user_mailer.warning.review_server_policies'
|
%span= t 'user_mailer.warning.review_server_policies'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.warning.get_in_touch', instance: @instance
|
||||||
|
@ -7,3 +7,16 @@
|
|||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= @warning.text %>
|
<%= @warning.text %>
|
||||||
|
<% unless @statuses.empty? %>
|
||||||
|
<%= t('user_mailer.warning.statuses') %>
|
||||||
|
|
||||||
|
<% @statuses.each do |status| %>
|
||||||
|
|
||||||
|
<%= render 'notification_mailer/status', status: status %>
|
||||||
|
---
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
---
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= t 'user_mailer.warning.get_in_touch', instance: @instance %>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
class RemovalWorker
|
class RemovalWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(status_id)
|
def perform(status_id, options = {})
|
||||||
RemoveStatusService.new.call(Status.find(status_id))
|
RemoveStatusService.new.call(Status.with_discarded.find(status_id), **options.symbolize_keys)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -512,6 +512,7 @@ en:
|
|||||||
delete: Delete
|
delete: Delete
|
||||||
nsfw_off: Mark as not sensitive
|
nsfw_off: Mark as not sensitive
|
||||||
nsfw_on: Mark as sensitive
|
nsfw_on: Mark as sensitive
|
||||||
|
deleted: Deleted
|
||||||
failed_to_execute: Failed to execute
|
failed_to_execute: Failed to execute
|
||||||
media:
|
media:
|
||||||
title: Media
|
title: Media
|
||||||
@ -1129,7 +1130,9 @@ en:
|
|||||||
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
|
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
|
||||||
silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
|
silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
|
||||||
suspend: Your account has been suspended, and all of your toots and your uploaded media files have been irreversibly removed from this server, and servers where you had followers.
|
suspend: Your account has been suspended, and all of your toots and your uploaded media files have been irreversibly removed from this server, and servers where you had followers.
|
||||||
|
get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}.
|
||||||
review_server_policies: Review server policies
|
review_server_policies: Review server policies
|
||||||
|
statuses: 'Specifically, for:'
|
||||||
subject:
|
subject:
|
||||||
disable: Your account %{acct} has been frozen
|
disable: Your account %{acct} has been frozen
|
||||||
none: Warning for %{acct}
|
none: Warning for %{acct}
|
||||||
|
@ -5,6 +5,7 @@ en:
|
|||||||
account_warning_preset:
|
account_warning_preset:
|
||||||
text: You can use toot syntax, such as URLs, hashtags and mentions
|
text: You can use toot syntax, such as URLs, hashtags and mentions
|
||||||
admin_account_action:
|
admin_account_action:
|
||||||
|
include_statuses: The user will see which toots have caused the moderation action or warning
|
||||||
send_email_notification: The user will receive an explanation of what happened with their account
|
send_email_notification: The user will receive an explanation of what happened with their account
|
||||||
text_html: Optional. You can use toot syntax. You can <a href="%{path}">add warning presets</a> to save time
|
text_html: Optional. You can use toot syntax. You can <a href="%{path}">add warning presets</a> to save time
|
||||||
type_html: Choose what to do with <strong>%{acct}</strong>
|
type_html: Choose what to do with <strong>%{acct}</strong>
|
||||||
@ -65,6 +66,7 @@ en:
|
|||||||
account_warning_preset:
|
account_warning_preset:
|
||||||
text: Preset text
|
text: Preset text
|
||||||
admin_account_action:
|
admin_account_action:
|
||||||
|
include_statuses: Include reported toots in the e-mail
|
||||||
send_email_notification: Notify the user per e-mail
|
send_email_notification: Notify the user per e-mail
|
||||||
text: Custom warning
|
text: Custom warning
|
||||||
type: Action
|
type: Action
|
||||||
@ -156,6 +158,7 @@ en:
|
|||||||
trending_tag: Send e-mail when an unreviewed hashtag is trending
|
trending_tag: Send e-mail when an unreviewed hashtag is trending
|
||||||
tag:
|
tag:
|
||||||
listable: Allow this hashtag to appear in searches and on the profile directory
|
listable: Allow this hashtag to appear in searches and on the profile directory
|
||||||
|
name: Hashtag
|
||||||
trendable: Allow this hashtag to appear under trends
|
trendable: Allow this hashtag to appear under trends
|
||||||
usable: Allow toots to use this hashtag
|
usable: Allow toots to use this hashtag
|
||||||
'no': 'No'
|
'no': 'No'
|
||||||
|
5
db/migrate/20190819134503_add_deleted_at_to_statuses.rb
Normal file
5
db/migrate/20190819134503_add_deleted_at_to_statuses.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class AddDeletedAtToStatuses < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :statuses, :deleted_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
13
db/migrate/20190820003045_update_statuses_index.rb
Normal file
13
db/migrate/20190820003045_update_statuses_index.rb
Normal file
@ -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
|
11
db/migrate/20190823221802_add_local_index_to_statuses.rb
Normal file
11
db/migrate/20190823221802_add_local_index_to_statuses.rb
Normal file
@ -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
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2019_08_15_225426) do
|
ActiveRecord::Schema.define(version: 2019_08_23_221802) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -657,7 +657,9 @@ ActiveRecord::Schema.define(version: 2019_08_15_225426) do
|
|||||||
t.boolean "local_only"
|
t.boolean "local_only"
|
||||||
t.bigint "poll_id"
|
t.bigint "poll_id"
|
||||||
t.string "content_type"
|
t.string "content_type"
|
||||||
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc }
|
t.datetime "deleted_at"
|
||||||
|
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
||||||
|
t.index ["id", "account_id"], name: "index_statuses_local_20190824", 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)))"
|
||||||
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
|
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
|
||||||
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
|
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
|
||||||
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
|
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.4.5",
|
"@babel/core": "^7.4.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||||
"@babel/plugin-proposal-decorators": "^7.4.4",
|
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
|
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
@ -137,7 +137,7 @@
|
|||||||
"react-motion": "^0.5.2",
|
"react-motion": "^0.5.2",
|
||||||
"react-notification": "^6.8.4",
|
"react-notification": "^6.8.4",
|
||||||
"react-overlays": "^0.8.3",
|
"react-overlays": "^0.8.3",
|
||||||
"react-redux": "^7.1.0",
|
"react-redux": "^7.1.1",
|
||||||
"react-redux-loading-bar": "^4.0.8",
|
"react-redux-loading-bar": "^4.0.8",
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
"react-router-scroll-4": "^1.0.0-beta.1",
|
"react-router-scroll-4": "^1.0.0-beta.1",
|
||||||
@ -163,10 +163,11 @@
|
|||||||
"throng": "^4.0.0",
|
"throng": "^4.0.0",
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
"uuid": "^3.1.0",
|
"uuid": "^3.1.0",
|
||||||
|
"wavesurfer.js": "^3.0.0",
|
||||||
"webpack": "^4.35.3",
|
"webpack": "^4.35.3",
|
||||||
"webpack-assets-manifest": "^3.1.1",
|
"webpack-assets-manifest": "^3.1.1",
|
||||||
"webpack-bundle-analyzer": "^3.3.2",
|
"webpack-bundle-analyzer": "^3.3.2",
|
||||||
"webpack-cli": "^3.3.6",
|
"webpack-cli": "^3.3.7",
|
||||||
"webpack-merge": "^4.2.1",
|
"webpack-merge": "^4.2.1",
|
||||||
"websocket.js": "^0.1.12"
|
"websocket.js": "^0.1.12"
|
||||||
},
|
},
|
||||||
|
@ -47,7 +47,7 @@ describe Admin::ReportedStatusesController do
|
|||||||
it 'removes a status' do
|
it 'removes a status' do
|
||||||
allow(RemovalWorker).to receive(:perform_async)
|
allow(RemovalWorker).to receive(:perform_async)
|
||||||
subject.call
|
subject.call
|
||||||
expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first)
|
expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ describe Admin::StatusesController do
|
|||||||
it 'removes a status' do
|
it 'removes a status' do
|
||||||
allow(RemovalWorker).to receive(:perform_async)
|
allow(RemovalWorker).to receive(:perform_async)
|
||||||
subject.call
|
subject.call
|
||||||
expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first)
|
expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,6 +42,6 @@ class UserMailerPreview < ActionMailer::Preview
|
|||||||
|
|
||||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/warning
|
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/warning
|
||||||
def warning
|
def warning
|
||||||
UserMailer.warning(User.first, AccountWarning.new(text: '', action: :silence))
|
UserMailer.warning(User.first, AccountWarning.new(text: '', action: :silence), [Status.first.id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -58,8 +58,8 @@ RSpec.describe Admin::AccountAction, type: :model do
|
|||||||
end.to change { Admin::ActionLog.count }.by 1
|
end.to change { Admin::ActionLog.count }.by 1
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'calls queue_email!' do
|
it 'calls process_email!' do
|
||||||
expect(account_action).to receive(:queue_email!)
|
expect(account_action).to receive(:process_email!)
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -41,12 +41,12 @@ describe Form::StatusBatch do
|
|||||||
|
|
||||||
it 'call RemovalWorker' do
|
it 'call RemovalWorker' do
|
||||||
form.save
|
form.save
|
||||||
expect(RemovalWorker).to have_received(:perform_async).with(status.id)
|
expect(RemovalWorker).to have_received(:perform_async).with(status.id, redraft: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'do not call RemovalWorker' do
|
it 'do not call RemovalWorker' do
|
||||||
form.save
|
form.save
|
||||||
expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id)
|
expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id, redraft: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
100
yarn.lock
100
yarn.lock
@ -121,16 +121,16 @@
|
|||||||
"@babel/traverse" "^7.4.4"
|
"@babel/traverse" "^7.4.4"
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.4.4"
|
||||||
|
|
||||||
"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.0":
|
"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.5":
|
||||||
version "7.5.0"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.0.tgz#02edb97f512d44ba23b3227f1bf2ed43454edac5"
|
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz#401f302c8ddbc0edd36f7c6b2887d8fa1122e5a4"
|
||||||
integrity sha512-EAoMc3hE5vE5LNhMqDOwB1usHvmRjCDAnH8CD4PVkX9/Yr3W/tcz8xE8QvdZxfsFBDICwZnF2UTHIqslRpvxmA==
|
integrity sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-function-name" "^7.1.0"
|
"@babel/helper-function-name" "^7.1.0"
|
||||||
"@babel/helper-member-expression-to-functions" "^7.0.0"
|
"@babel/helper-member-expression-to-functions" "^7.5.5"
|
||||||
"@babel/helper-optimise-call-expression" "^7.0.0"
|
"@babel/helper-optimise-call-expression" "^7.0.0"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/helper-replace-supers" "^7.4.4"
|
"@babel/helper-replace-supers" "^7.5.5"
|
||||||
"@babel/helper-split-export-declaration" "^7.4.4"
|
"@babel/helper-split-export-declaration" "^7.4.4"
|
||||||
|
|
||||||
"@babel/helper-define-map@^7.5.5":
|
"@babel/helper-define-map@^7.5.5":
|
||||||
@ -173,13 +173,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.4.4"
|
||||||
|
|
||||||
"@babel/helper-member-expression-to-functions@^7.0.0":
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz#8cd14b0a0df7ff00f009e7d7a436945f47c7a16f"
|
|
||||||
integrity sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.0.0"
|
|
||||||
|
|
||||||
"@babel/helper-member-expression-to-functions@^7.5.5":
|
"@babel/helper-member-expression-to-functions@^7.5.5":
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz#1fb5b8ec4453a93c439ee9fe3aeea4a84b76b590"
|
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz#1fb5b8ec4453a93c439ee9fe3aeea4a84b76b590"
|
||||||
@ -236,16 +229,6 @@
|
|||||||
"@babel/traverse" "^7.1.0"
|
"@babel/traverse" "^7.1.0"
|
||||||
"@babel/types" "^7.0.0"
|
"@babel/types" "^7.0.0"
|
||||||
|
|
||||||
"@babel/helper-replace-supers@^7.4.4":
|
|
||||||
version "7.4.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27"
|
|
||||||
integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-member-expression-to-functions" "^7.0.0"
|
|
||||||
"@babel/helper-optimise-call-expression" "^7.0.0"
|
|
||||||
"@babel/traverse" "^7.4.4"
|
|
||||||
"@babel/types" "^7.4.4"
|
|
||||||
|
|
||||||
"@babel/helper-replace-supers@^7.5.5":
|
"@babel/helper-replace-supers@^7.5.5":
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz#f84ce43df031222d2bad068d2626cb5799c34bc2"
|
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz#f84ce43df031222d2bad068d2626cb5799c34bc2"
|
||||||
@ -327,12 +310,12 @@
|
|||||||
"@babel/helper-remap-async-to-generator" "^7.1.0"
|
"@babel/helper-remap-async-to-generator" "^7.1.0"
|
||||||
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
||||||
|
|
||||||
"@babel/plugin-proposal-class-properties@^7.5.0":
|
"@babel/plugin-proposal-class-properties@^7.5.5":
|
||||||
version "7.5.0"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.0.tgz#5bc6a0537d286fcb4fd4e89975adbca334987007"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4"
|
||||||
integrity sha512-9L/JfPCT+kShiiTTzcnBJ8cOwdKVmlC1RcCf9F0F9tERVrM4iWtWnXtjWCRqNm2la2BxO1MPArWNsU9zsSJWSQ==
|
integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-create-class-features-plugin" "^7.5.0"
|
"@babel/helper-create-class-features-plugin" "^7.5.5"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
"@babel/plugin-proposal-decorators@^7.4.4":
|
"@babel/plugin-proposal-decorators@^7.4.4":
|
||||||
@ -811,10 +794,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.12.0"
|
regenerator-runtime "^0.12.0"
|
||||||
|
|
||||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4":
|
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5":
|
||||||
version "7.5.4"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
||||||
integrity sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==
|
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.2"
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
@ -3858,14 +3841,16 @@ eslint-scope@^5.0.0:
|
|||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint-utils@^1.3.1:
|
eslint-utils@^1.3.1:
|
||||||
version "1.3.1"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
|
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
|
||||||
integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==
|
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
|
||||||
|
dependencies:
|
||||||
|
eslint-visitor-keys "^1.0.0"
|
||||||
|
|
||||||
eslint-visitor-keys@^1.0.0:
|
eslint-visitor-keys@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
|
||||||
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
|
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
|
||||||
|
|
||||||
eslint@^2.7.0:
|
eslint@^2.7.0:
|
||||||
version "2.13.1"
|
version "2.13.1"
|
||||||
@ -6764,9 +6749,9 @@ mississippi@^3.0.0:
|
|||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.1"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
|
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
|
||||||
integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
|
integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
|
||||||
dependencies:
|
dependencies:
|
||||||
for-in "^1.0.2"
|
for-in "^1.0.2"
|
||||||
is-extendable "^1.0.1"
|
is-extendable "^1.0.1"
|
||||||
@ -8484,10 +8469,10 @@ react-intl@^2.9.0:
|
|||||||
intl-relativeformat "^2.1.0"
|
intl-relativeformat "^2.1.0"
|
||||||
invariant "^2.1.1"
|
invariant "^2.1.1"
|
||||||
|
|
||||||
react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
|
react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
|
||||||
version "16.8.6"
|
version "16.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"
|
||||||
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
|
integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==
|
||||||
|
|
||||||
react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
|
react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
@ -8539,17 +8524,17 @@ react-redux-loading-bar@^4.0.8:
|
|||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
react-lifecycles-compat "^3.0.2"
|
react-lifecycles-compat "^3.0.2"
|
||||||
|
|
||||||
react-redux@^7.1.0:
|
react-redux@^7.1.1:
|
||||||
version "7.1.0"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.1.tgz#ce6eee1b734a7a76e0788b3309bf78ff6b34fa0a"
|
||||||
integrity sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==
|
integrity sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.5"
|
"@babel/runtime" "^7.5.5"
|
||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-is "^16.8.6"
|
react-is "^16.9.0"
|
||||||
|
|
||||||
react-router-dom@^4.1.1:
|
react-router-dom@^4.1.1:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
@ -10474,6 +10459,11 @@ watchpack@^1.5.0:
|
|||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
neo-async "^2.5.0"
|
neo-async "^2.5.0"
|
||||||
|
|
||||||
|
wavesurfer.js@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-3.0.0.tgz#35f36d76d59c749dca453cf4e10ee0ec49f454f8"
|
||||||
|
integrity sha512-DANu206c6gb9pSUbYFevsSiXMy8+Ri+CNtqm0UsouUdsn9fVQRtYs8uxzBtXK+rUPlIc6FlO54DU8uWeW3lDzw==
|
||||||
|
|
||||||
wbuf@^1.1.0, wbuf@^1.7.3:
|
wbuf@^1.1.0, wbuf@^1.7.3:
|
||||||
version "1.7.3"
|
version "1.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
|
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
|
||||||
@ -10518,10 +10508,10 @@ webpack-bundle-analyzer@^3.3.2:
|
|||||||
opener "^1.5.1"
|
opener "^1.5.1"
|
||||||
ws "^6.0.0"
|
ws "^6.0.0"
|
||||||
|
|
||||||
webpack-cli@^3.3.6:
|
webpack-cli@^3.3.7:
|
||||||
version "3.3.6"
|
version "3.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.6.tgz#2c8c399a2642133f8d736a359007a052e060032c"
|
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.7.tgz#77c8580dd8e92f69d635e0238eaf9d9c15759a91"
|
||||||
integrity sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==
|
integrity sha512-OhTUCttAsr+IZSMVwGROGRHvT+QAs8H6/mHIl4SvhAwYywjiylYjpwybGx7WQ9Hkb45FhjtsymkwiRRbGJ1SZQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "2.4.2"
|
chalk "2.4.2"
|
||||||
cross-spawn "6.0.5"
|
cross-spawn "6.0.5"
|
||||||
|
Loading…
Reference in New Issue
Block a user