Merge tag 'v2.8.2' into instance_only_statuses
This commit is contained in:
@ -1,62 +1,142 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import { displayMedia } from '../../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
import { isIOS } from 'mastodon/is_mobile';
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
displayWidth: PropTypes.number.isRequired,
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
|
||||
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (!this.state.visible) {
|
||||
this.setState({ visible: true });
|
||||
return true;
|
||||
componentDidMount () {
|
||||
if (this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
_decode () {
|
||||
const hash = this.props.attachment.get('blurhash');
|
||||
const pixels = decode(hash, 32, 32);
|
||||
|
||||
if (pixels) {
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
}
|
||||
|
||||
handleImageLoad = () => {
|
||||
this.setState({ loaded: true });
|
||||
}
|
||||
|
||||
handleMouseEnter = e => {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.play();
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave = e => {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.pause();
|
||||
e.target.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
hoverToPlay () {
|
||||
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.visible) {
|
||||
this.props.onOpenMedia(this.props.attachment);
|
||||
} else {
|
||||
this.setState({ visible: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media } = this.props;
|
||||
const { visible } = this.state;
|
||||
const status = media.get('status');
|
||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
const style = {};
|
||||
const { attachment, displayWidth } = this.props;
|
||||
const { visible, loaded } = this.state;
|
||||
|
||||
let label, icon;
|
||||
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
|
||||
const height = width;
|
||||
const status = attachment.get('status');
|
||||
|
||||
if (media.get('type') === 'gifv') {
|
||||
label = <span className='media-gallery__gifv__label'>GIF</span>;
|
||||
}
|
||||
let thumbnail = '';
|
||||
|
||||
if (visible) {
|
||||
style.backgroundImage = `url(${media.get('preview_url')})`;
|
||||
style.backgroundPosition = `${x}% ${y}%`;
|
||||
} else {
|
||||
icon = (
|
||||
<span className='account-gallery__item__icons'>
|
||||
<Icon id='eye-slash' />
|
||||
</span>
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
// Skip
|
||||
} else if (attachment.get('type') === 'image') {
|
||||
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
|
||||
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
thumbnail = (
|
||||
<img
|
||||
src={attachment.get('preview_url')}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
);
|
||||
} else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
|
||||
const autoPlay = !isIOS() && autoPlayGif;
|
||||
|
||||
thumbnail = (
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
<video
|
||||
className='media-gallery__item-gifv-thumbnail'
|
||||
aria-label={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
role='application'
|
||||
src={attachment.get('url')}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
autoPlay={autoPlay}
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
|
||||
<span className='media-gallery__gifv__label'>GIF</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account-gallery__item'>
|
||||
<Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}>
|
||||
{icon}
|
||||
{label}
|
||||
</Permalink>
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible && thumbnail}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,24 +2,25 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchAccount } from '../../actions/accounts';
|
||||
import { fetchAccount } from 'mastodon/actions/accounts';
|
||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { getAccountGallery } from '../../selectors';
|
||||
import { getAccountGallery } from 'mastodon/selectors';
|
||||
import MediaItem from './components/media_item';
|
||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import { ScrollContainer } from 'react-router-scroll-4';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import LoadMore from 'mastodon/components/load_more';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
medias: getAccountGallery(state, props.params.accountId),
|
||||
attachments: getAccountGallery(state, props.params.accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
});
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
@ -51,12 +52,16 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
medias: ImmutablePropTypes.list.isRequired,
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: 323,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
|
||||
@ -71,11 +76,11 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
handleScrollToBottom = () => {
|
||||
if (this.props.hasMore) {
|
||||
this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined);
|
||||
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll = (e) => {
|
||||
handleScroll = e => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
@ -88,13 +93,31 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
|
||||
};
|
||||
|
||||
handleLoadOlder = (e) => {
|
||||
handleLoadOlder = e => {
|
||||
e.preventDefault();
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
|
||||
handleOpenMedia = attachment => {
|
||||
if (attachment.get('type') === 'video') {
|
||||
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') }));
|
||||
} else {
|
||||
const media = attachment.getIn(['status', 'media_attachments']);
|
||||
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
|
||||
|
||||
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') }));
|
||||
}
|
||||
}
|
||||
|
||||
handleRef = c => {
|
||||
if (c) {
|
||||
this.setState({ width: c.offsetWidth });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { medias, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props;
|
||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
@ -104,9 +127,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (!medias && isLoading) {
|
||||
if (!attachments && isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
@ -114,7 +135,9 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (hasMore && !(isLoading && medias.size === 0)) {
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||
}
|
||||
|
||||
@ -126,23 +149,17 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
|
||||
<div role='feed' className='account-gallery__container'>
|
||||
{medias.map((media, index) => media === null ? (
|
||||
<LoadMoreMedia
|
||||
key={'more:' + medias.getIn(index + 1, 'id')}
|
||||
maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
/>
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem
|
||||
key={media.get('id')}
|
||||
media={media}
|
||||
/>
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && medias.size === 0 && (
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
|
@ -11,7 +11,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import FederationDropdownContainer from '../containers/federation_dropdown_container';
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import PollFormContainer from '../containers/poll_form_container';
|
||||
import UploadFormContainer from '../containers/upload_form_container';
|
||||
@ -41,18 +40,17 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
suggestion_token: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: PropTypes.bool,
|
||||
privacy: PropTypes.string,
|
||||
federation: PropTypes.bool,
|
||||
spoiler_text: PropTypes.string,
|
||||
spoilerText: PropTypes.string,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
caretPosition: PropTypes.number,
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
is_submitting: PropTypes.bool,
|
||||
is_changing_upload: PropTypes.bool,
|
||||
is_uploading: PropTypes.bool,
|
||||
isSubmitting: PropTypes.bool,
|
||||
isChangingUpload: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
@ -87,10 +85,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
// Submit disabled:
|
||||
const { is_submitting, is_changing_upload, is_uploading, anyMedia } = this.props;
|
||||
const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join('');
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
|
||||
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
|
||||
if (is_submitting || is_uploading || is_changing_upload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -135,7 +133,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
} else if(prevProps.is_submitting && !this.props.is_submitting) {
|
||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||
if (this.props.spoiler) {
|
||||
@ -164,9 +162,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
render () {
|
||||
const { intl, onPaste, showSearch, anyMedia } = this.props;
|
||||
const disabled = this.props.is_submitting;
|
||||
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
|
||||
const disabledButton = disabled || this.props.is_uploading || this.props.is_changing_upload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
const disabled = this.props.isSubmitting;
|
||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
let publishText = '';
|
||||
|
||||
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
@ -184,7 +182,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -217,7 +215,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
<PrivacyDropdownContainer />
|
||||
<SensitiveButtonContainer />
|
||||
<SpoilerButtonContainer />
|
||||
<FederationDropdownContainer />
|
||||
</div>
|
||||
|
@ -26,6 +26,7 @@ class Option extends React.PureComponent {
|
||||
isPollMultiple: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -37,13 +38,24 @@ class Option extends React.PureComponent {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<label className='poll__text editable'>
|
||||
<span className={classNames('poll__input', { checkbox: isPollMultiple })} />
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||
onClick={this.handleToggleMultiple}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
/>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
@ -86,6 +98,10 @@ class PollForm extends ImmutablePureComponent {
|
||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
|
||||
|
||||
@ -96,7 +112,7 @@ class PollForm extends ImmutablePureComponent {
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)}
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
|
@ -73,7 +73,7 @@ class Search extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
@ -82,10 +82,6 @@ class Search extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
noop () {
|
||||
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true });
|
||||
this.props.onShow();
|
||||
@ -110,7 +106,7 @@ class Search extends React.PureComponent {
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import UploadContainer from '../containers/upload_container';
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
|
||||
export default class UploadForm extends ImmutablePureComponent {
|
||||
|
||||
@ -22,6 +23,8 @@ export default class UploadForm extends ImmutablePureComponent {
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ComposeForm from '../components/compose_form';
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
@ -9,22 +8,22 @@ import {
|
||||
selectComposeSuggestion,
|
||||
changeComposeSpoilerText,
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
} from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||
spoilerText: state.getIn(['compose', 'spoiler_text']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
federation: state.getIn(['compose', 'federation']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||
is_changing_upload: state.getIn(['compose', 'is_changing_upload']),
|
||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
});
|
||||
@ -47,8 +46,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, accountId) {
|
||||
dispatch(selectComposeSuggestion(position, token, accountId));
|
||||
onSuggestionSelected (position, token, suggestion) {
|
||||
dispatch(selectComposeSuggestion(position, token, suggestion));
|
||||
},
|
||||
|
||||
onChangeSpoilerText (checked) {
|
||||
|
@ -2,11 +2,9 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { changeComposeSensitivity } from '../../../actions/compose';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { changeComposeSensitivity } from 'mastodon/actions/compose';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
|
||||
@ -14,7 +12,6 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
visible: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
active: state.getIn(['compose', 'sensitive']),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
});
|
||||
@ -30,7 +27,6 @@ const mapDispatchToProps = dispatch => ({
|
||||
class SensitiveButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
@ -38,32 +34,14 @@ class SensitiveButton extends React.PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { visible, active, disabled, onClick, intl } = this.props;
|
||||
const { active, disabled, onClick, intl } = this.props;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
|
||||
{({ scale }) => {
|
||||
const icon = active ? 'eye-slash' : 'eye';
|
||||
const className = classNames('compose-form__sensitive-button', {
|
||||
'compose-form__sensitive-button--visible': visible,
|
||||
});
|
||||
return (
|
||||
<div className={className} style={{ transform: `scale(${scale})` }}>
|
||||
<IconButton
|
||||
className='compose-form__sensitive-button__icon'
|
||||
title={intl.formatMessage(active ? messages.marked : messages.unmarked)}
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
size={18}
|
||||
active={active}
|
||||
disabled={disabled}
|
||||
style={{ lineHeight: null, height: null }}
|
||||
inverted
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Motion>
|
||||
<div className='compose-form__sensitive-button'>
|
||||
<button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
|
||||
<Icon id='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -20,18 +20,24 @@ export default class ConversationsList extends ImmutablePureComponent {
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) - 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) + 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, invitesEnabled, version, profile_directory } from '../../initial_state';
|
||||
import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state';
|
||||
import { fetchFollowRequests } from '../../actions/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -172,7 +172,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||
<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='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> (v{version})</span> }}
|
||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -113,18 +113,24 @@ class Notifications extends React.PureComponent {
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.column.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ export default class StatusCheckBox extends React.PureComponent {
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={239}
|
||||
|
@ -5,7 +5,6 @@ import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import MediaGallery from '../../../components/media_gallery';
|
||||
import AttachmentList from '../../../components/attachment_list';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { defineMessages, injectIntl, FormattedDate, FormattedNumber } from 'react-intl';
|
||||
import Card from './card';
|
||||
@ -116,14 +115,13 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||
media = <AttachmentList media={status.get('media_attachments')} />;
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Video
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={300}
|
||||
|
@ -316,15 +316,15 @@ class Status extends ImmutablePureComponent {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size - 1);
|
||||
this._selectChild(ancestorsIds.size - 1, true);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index);
|
||||
this._selectChild(ancestorsIds.size + index, true);
|
||||
} else {
|
||||
this._selectChild(index - 1);
|
||||
this._selectChild(index - 1, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -333,23 +333,29 @@ class Status extends ImmutablePureComponent {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size + 1);
|
||||
this._selectChild(ancestorsIds.size + 1, false);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index + 2);
|
||||
this._selectChild(ancestorsIds.size + index + 2, false);
|
||||
} else {
|
||||
this._selectChild(index + 1);
|
||||
this._selectChild(index + 1, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.querySelectorAll('.focusable')[index];
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node;
|
||||
const element = container.querySelectorAll('.focusable')[index];
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export default class ActionsModal extends ImmutablePureComponent {
|
||||
<div className='modal-root__modal actions-modal'>
|
||||
{status}
|
||||
|
||||
<ul>
|
||||
<ul className={classNames({ 'with-status': !!status })}>
|
||||
{this.props.actions.map(this.renderAction)}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -2,11 +2,11 @@ import React from 'react';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Video from '../../video';
|
||||
import ExtendedVideoPlayer from '../../../components/extended_video_player';
|
||||
import Video from 'mastodon/features/video';
|
||||
import ExtendedVideoPlayer from 'mastodon/components/extended_video_player';
|
||||
import classNames from 'classnames';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImageLoader from './image_loader';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
@ -24,6 +24,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
index: PropTypes.number.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
@ -72,9 +73,12 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('keydown', this.handleKeyDown, false);
|
||||
|
||||
if (this.context.router) {
|
||||
const history = this.context.router.history;
|
||||
|
||||
history.push(history.location.pathname, previewState);
|
||||
|
||||
this.unlistenHistory = history.listen(() => {
|
||||
this.props.onClose();
|
||||
});
|
||||
@ -83,6 +87,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
if (this.context.router) {
|
||||
this.unlistenHistory();
|
||||
|
||||
@ -102,8 +107,15 @@ class MediaModal extends ImmutablePureComponent {
|
||||
}));
|
||||
};
|
||||
|
||||
handleStatusClick = e => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, onClose } = this.props;
|
||||
const { media, status, intl, onClose } = this.props;
|
||||
const { navigationHidden } = this.state;
|
||||
|
||||
const index = this.getIndex();
|
||||
@ -144,6 +156,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
return (
|
||||
<Video
|
||||
preview={image.get('preview_url')}
|
||||
blurhash={image.get('blurhash')}
|
||||
src={image.get('url')}
|
||||
width={image.get('width')}
|
||||
height={image.get('height')}
|
||||
@ -206,10 +219,19 @@ class MediaModal extends ImmutablePureComponent {
|
||||
{content}
|
||||
</ReactSwipeableViews>
|
||||
</div>
|
||||
|
||||
<div className={navigationClassName}>
|
||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
|
||||
|
||||
{leftNav}
|
||||
{rightNav}
|
||||
|
||||
{status && (
|
||||
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul className='media-modal__pagination'>
|
||||
{pagination}
|
||||
</ul>
|
||||
|
@ -1,28 +1,69 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Video from '../../video';
|
||||
import Video from 'mastodon/features/video';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export const previewState = 'previewVideoModal';
|
||||
|
||||
export default class VideoModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
time: PropTypes.number,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
if (this.context.router) {
|
||||
const history = this.context.router.history;
|
||||
|
||||
history.push(history.location.pathname, previewState);
|
||||
|
||||
this.unlistenHistory = history.listen(() => {
|
||||
this.props.onClose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.context.router) {
|
||||
this.unlistenHistory();
|
||||
|
||||
if (this.context.router.history.location.state === previewState) {
|
||||
this.context.router.history.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleStatusClick = e => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, time, onClose } = this.props;
|
||||
const { media, status, time, onClose } = this.props;
|
||||
|
||||
const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal video-modal'>
|
||||
<div>
|
||||
<Video
|
||||
preview={media.get('preview_url')}
|
||||
blurhash={media.get('blurhash')}
|
||||
src={media.get('url')}
|
||||
startTime={time}
|
||||
onCloseVideo={onClose}
|
||||
link={link}
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
/>
|
||||
|
@ -47,7 +47,8 @@ import {
|
||||
Lists,
|
||||
} from './util/async-components';
|
||||
import { me } from '../../initial_state';
|
||||
import { previewState } from './components/media_modal';
|
||||
import { previewState as previewMediaState } from './components/media_modal';
|
||||
import { previewState as previewVideoState } from './components/video_modal';
|
||||
|
||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
@ -121,7 +122,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
}
|
||||
|
||||
shouldUpdateScroll (_, { location }) {
|
||||
return location.state !== previewState;
|
||||
return location.state !== previewMediaState && location.state !== previewVideoState;
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
@ -367,11 +368,16 @@ class UI extends React.PureComponent {
|
||||
handleHotkeyFocusColumn = e => {
|
||||
const index = (e.key * 1) + 1; // First child is drawer, skip that
|
||||
const column = this.node.querySelector(`.column:nth-child(${index})`);
|
||||
if (!column) return;
|
||||
const container = column.querySelector('.scrollable');
|
||||
|
||||
if (column) {
|
||||
const status = column.querySelector('.focusable');
|
||||
if (container) {
|
||||
const status = container.querySelector('.focusable');
|
||||
|
||||
if (status) {
|
||||
if (container.scrollTop > status.offsetTop) {
|
||||
status.scrollIntoView(true);
|
||||
}
|
||||
status.focus();
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
import { displayMedia } from '../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
play: { id: 'video.play', defaultMessage: 'Play' },
|
||||
@ -102,6 +103,8 @@ class Video extends React.PureComponent {
|
||||
inline: PropTypes.bool,
|
||||
cacheWidth: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
blurhash: PropTypes.string,
|
||||
link: PropTypes.node,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -139,6 +142,7 @@ class Video extends React.PureComponent {
|
||||
|
||||
setVideoRef = c => {
|
||||
this.video = c;
|
||||
|
||||
if (this.video) {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
}
|
||||
@ -152,6 +156,10 @@ class Video extends React.PureComponent {
|
||||
this.volume = c;
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
}
|
||||
|
||||
handleClickRoot = e => e.stopPropagation();
|
||||
|
||||
handlePlay = () => {
|
||||
@ -170,7 +178,6 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleVolumeMouseDown = e => {
|
||||
|
||||
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
|
||||
document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
|
||||
document.addEventListener('touchmove', this.handleMouseVolSlide, true);
|
||||
@ -190,7 +197,6 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleMouseVolSlide = throttle(e => {
|
||||
|
||||
const rect = this.volume.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
|
||||
|
||||
@ -261,6 +267,10 @@ class Video extends React.PureComponent {
|
||||
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
|
||||
if (this.props.blurhash) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@ -270,6 +280,24 @@ class Video extends React.PureComponent {
|
||||
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
_decode () {
|
||||
const hash = this.props.blurhash;
|
||||
const pixels = decode(hash, 32, 32);
|
||||
|
||||
if (pixels) {
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
handleFullscreenChange = () => {
|
||||
this.setState({ fullscreen: isFullscreen() });
|
||||
}
|
||||
@ -314,6 +342,7 @@ class Video extends React.PureComponent {
|
||||
|
||||
handleOpenVideo = () => {
|
||||
const { src, preview, width, height, alt } = this.props;
|
||||
|
||||
const media = fromJS({
|
||||
type: 'video',
|
||||
url: src,
|
||||
@ -333,7 +362,7 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive } = this.props;
|
||||
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = (currentTime / duration) * 100;
|
||||
|
||||
@ -351,6 +380,7 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
let preload;
|
||||
|
||||
if (startTime || fullscreen || dragging) {
|
||||
preload = 'auto';
|
||||
} else if (detailed) {
|
||||
@ -360,6 +390,7 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
let warning;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
@ -377,7 +408,9 @@ class Video extends React.PureComponent {
|
||||
onClick={this.handleClickRoot}
|
||||
tabIndex={0}
|
||||
>
|
||||
<video
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} />
|
||||
|
||||
{revealed && <video
|
||||
ref={this.setVideoRef}
|
||||
src={src}
|
||||
poster={preview}
|
||||
@ -397,12 +430,13 @@ class Video extends React.PureComponent {
|
||||
onLoadedData={this.handleLoadedData}
|
||||
onProgress={this.handleProgress}
|
||||
onVolumeChange={this.handleVolumeChange}
|
||||
/>
|
||||
/>}
|
||||
|
||||
<button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
|
||||
<span className='video-player__spoiler__title'>{warning}</span>
|
||||
<span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</button>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
|
||||
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
|
||||
<span className='spoiler-button__overlay__label'>{warning}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={classNames('video-player__controls', { active: paused || hovered })}>
|
||||
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
|
||||
@ -420,6 +454,7 @@ class Video extends React.PureComponent {
|
||||
<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
|
||||
@ -429,17 +464,19 @@ class Video extends React.PureComponent {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(detailed || fullscreen) &&
|
||||
{(detailed || fullscreen) && (
|
||||
<span>
|
||||
<span className='video-player__time-current'>{formatTime(currentTime)}</span>
|
||||
<span className='video-player__time-sep'>/</span>
|
||||
<span className='video-player__time-total'>{formatTime(duration)}</span>
|
||||
</span>
|
||||
}
|
||||
)}
|
||||
|
||||
{link && <span className='video-player__link'>{link}</span>}
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
{!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye' fixedWidth /></button>}
|
||||
{!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||
{(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
|
||||
{onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
|
||||
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
||||
|
Reference in New Issue
Block a user