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

Conflicts:
- `package.json`:
  Not really a conflict, just some glitch-soc-specific dependency
  too close to an upstream-updated one.
This commit is contained in:
Thibaut Girka
2020-07-07 15:34:00 +02:00
40 changed files with 1762 additions and 1363 deletions

View File

@@ -3,99 +3,166 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
import Textarea from 'react-textarea-autosize';
import { is } from 'immutable';
const messages = defineMessages({
placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
});
export default @injectIntl
class Header extends ImmutablePureComponent {
class InlineAlert extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
isEditing: PropTypes.bool,
isSubmitting: PropTypes.bool,
accountNote: PropTypes.string,
onEditAccountNote: PropTypes.func.isRequired,
onCancelAccountNote: PropTypes.func.isRequired,
onSaveAccountNote: PropTypes.func.isRequired,
onChangeAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
show: PropTypes.bool,
};
handleChangeAccountNote = (e) => {
this.props.onChangeAccountNote(e.target.value);
state = {
mountMessage: false,
};
componentWillUnmount () {
if (this.props.isEditing) {
this.props.onCancelAccountNote();
}
}
static TRANSITION_DELAY = 200;
handleKeyDown = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.props.onSaveAccountNote();
} else if (e.keyCode === 27) {
this.props.onCancelAccountNote();
componentWillReceiveProps (nextProps) {
if (!this.props.show && nextProps.show) {
this.setState({ mountMessage: true });
} else if (this.props.show && !nextProps.show) {
setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
}
}
render () {
const { account, accountNote, isEditing, isSubmitting, intl } = this.props;
const { show } = this.props;
const { mountMessage } = this.state;
if (!account || (!accountNote && !isEditing)) {
return (
<span aria-live='polite' role='status' className='inline-alert' style={{ opacity: show ? 1 : 0 }}>
{mountMessage && <FormattedMessage id='generic.saved' defaultMessage='Saved' />}
</span>
);
}
}
export default @injectIntl
class AccountNote extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
value: PropTypes.string,
onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
value: null,
saving: false,
saved: false,
};
componentWillMount () {
this._reset();
}
componentWillReceiveProps (nextProps) {
const accountWillChange = !is(this.props.account, nextProps.account);
const newState = {};
if (accountWillChange && this._isDirty()) {
this._save(false);
}
if (accountWillChange || nextProps.value === this.state.value) {
newState.saving = false;
}
if (this.props.value !== nextProps.value) {
newState.value = nextProps.value;
}
this.setState(newState);
}
componentWillUnmount () {
if (this._isDirty()) {
this._save(false);
}
}
setTextareaRef = c => {
this.textarea = c;
}
handleChange = e => {
this.setState({ value: e.target.value, saving: false });
};
handleKeyDown = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this._save();
if (this.textarea) {
this.textarea.blur();
}
} else if (e.keyCode === 27) {
e.preventDefault();
this._reset(() => {
if (this.textarea) {
this.textarea.blur();
}
});
}
}
handleBlur = () => {
if (this._isDirty()) {
this._save();
}
}
_save (showMessage = true) {
this.setState({ saving: true }, () => this.props.onSave(this.state.value));
if (showMessage) {
this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
}
}
_reset (callback) {
this.setState({ value: this.props.value }, callback);
}
_isDirty () {
return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
}
render () {
const { account, intl } = this.props;
const { value, saved } = this.state;
if (!account) {
return null;
}
let action_buttons = null;
if (isEditing) {
action_buttons = (
<div className='account__header__account-note__buttons'>
<button className='text-btn' tabIndex='0' onClick={this.props.onCancelAccountNote} disabled={isSubmitting}>
<Icon id='times' size={15} /> <FormattedMessage id='account_note.cancel' defaultMessage='Cancel' />
</button>
<div className='flex-spacer' />
<button className='text-btn' tabIndex='0' onClick={this.props.onSaveAccountNote} disabled={isSubmitting}>
<Icon id='check' size={15} /> <FormattedMessage id='account_note.save' defaultMessage='Save' />
</button>
</div>
);
}
let note_container = null;
if (isEditing) {
note_container = (
<Textarea
className='account__header__account-note__content'
disabled={isSubmitting}
placeholder={intl.formatMessage(messages.placeholder)}
value={accountNote}
onChange={this.handleChangeAccountNote}
onKeyDown={this.handleKeyDown}
autoFocus
/>
);
} else {
note_container = (<div className='account__header__account-note__content'>{accountNote}</div>);
}
return (
<div className='account__header__account-note'>
<div className='account__header__account-note__header'>
<strong><FormattedMessage id='account.account_note_header' defaultMessage='Your note for @{name}' values={{ name: account.get('username') }} /></strong>
{!isEditing && (
<div>
<button className='text-btn' tabIndex='0' onClick={this.props.onEditAccountNote} disabled={isSubmitting}>
<Icon id='pencil' size={15} /> <FormattedMessage id='account_note.edit' defaultMessage='Edit' />
</button>
</div>
)}
</div>
{note_container}
{action_buttons}
<label htmlFor={`account-note-${account.get('id')}`}>
<FormattedMessage id='account.account_note_header' defaultMessage='Note' /> <InlineAlert show={saved} />
</label>
<Textarea
id={`account-note-${account.get('id')}`}
className='account__header__account-note__content'
disabled={this.props.value === null || value === null}
placeholder={intl.formatMessage(messages.placeholder)}
value={value || ''}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleBlur}
ref={this.setTextareaRef}
/>
</div>
);
}

View File

@@ -8,7 +8,8 @@ import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import Avatar from 'mastodon/components/avatar';
import { shortNumberFormat } from 'mastodon/utils/numbers';
import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';
@@ -66,7 +67,6 @@ class Header extends ImmutablePureComponent {
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
};
@@ -131,8 +131,6 @@ class Header extends ImmutablePureComponent {
return null;
}
const accountNote = account.getIn(['relationship', 'note']);
let info = [];
let actionBtn = '';
let lockedIcon = '';
@@ -183,10 +181,6 @@ class Header extends ImmutablePureComponent {
menu.push(null);
}
if (accountNote === null) {
menu.push({ text: intl.formatMessage(messages.add_account_note, { name: account.get('username') }), action: this.props.onEditAccountNote });
}
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
@@ -293,8 +287,6 @@ class Header extends ImmutablePureComponent {
</h1>
</div>
<AccountNoteContainer account={account} />
<div className='account__header__extra'>
<div className='account__header__bio'>
{ (fields.size > 0 || identity_proofs.size > 0) && (
@@ -323,20 +315,31 @@ class Header extends ImmutablePureComponent {
</div>
)}
{account.get('id') !== me && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
</div>
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' />
<ShortNumber
value={account.get('statuses_count')}
renderer={counterRenderer('statuses')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' />
<ShortNumber
value={account.get('following_count')}
renderer={counterRenderer('following')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' />
<ShortNumber
value={account.get('followers_count')}
renderer={counterRenderer('followers')}
/>
</NavLink>
</div>
</div>

View File

@@ -1,34 +1,17 @@
import { connect } from 'react-redux';
import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'mastodon/actions/account_notes';
import { submitAccountNote } from 'mastodon/actions/account_notes';
import AccountNote from '../components/account_note';
const mapStateToProps = (state, { account }) => {
const isEditing = state.getIn(['account_notes', 'edit', 'account_id']) === account.get('id');
return {
isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']),
accountNote: isEditing ? state.getIn(['account_notes', 'edit', 'comment']) : account.getIn(['relationship', 'note']),
isEditing,
};
};
const mapStateToProps = (state, { account }) => ({
value: account.getIn(['relationship', 'note']),
});
const mapDispatchToProps = (dispatch, { account }) => ({
onEditAccountNote() {
dispatch(initEditAccountNote(account));
onSave (value) {
dispatch(submitAccountNote(account.get('id'), value));
},
onSaveAccountNote() {
dispatch(submitAccountNote());
},
onCancelAccountNote() {
dispatch(cancelAccountNote());
},
onChangeAccountNote(comment) {
dispatch(changeAccountNoteComment(comment));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);

View File

@@ -19,7 +19,6 @@ import { initBlockModal } from '../../../actions/blocks';
import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { initEditAccountNote } from 'mastodon/actions/account_notes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unfollowModal } from '../../../initial_state';
import { List as ImmutableList } from 'immutable';
@@ -103,10 +102,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onEditAccountNote (account) {
dispatch(initEditAccountNote(account));
},
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,

View File

@@ -11,8 +11,14 @@ import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import IconButton from 'mastodon/components/icon_button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
import { shortNumberFormat } from 'mastodon/utils/numbers';
import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts';
import ShortNumber from 'mastodon/components/short_number';
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
@@ -22,7 +28,10 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
unfollowConfirm: {
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
},
});
const makeMapStateToProps = () => {
@@ -36,15 +45,25 @@ const makeMapStateToProps = () => {
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
onFollow(account) {
if (
account.getIn(['relationship', 'following']) ||
account.getIn(['relationship', 'requested'])
) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
}));
dispatch(
openModal('CONFIRM', {
message: (
<FormattedMessage
id='confirmations.unfollow.message'
defaultMessage='Are you sure you want to unfollow {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
),
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
}),
);
} else {
dispatch(unfollowAccount(account.get('id')));
}
@@ -53,7 +72,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onBlock (account) {
onBlock(account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
@@ -61,17 +80,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onMute (account) {
onMute(account) {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},
});
export default @injectIntl
export default
@injectIntl
@connect(makeMapStateToProps, mapDispatchToProps)
class AccountCard extends ImmutablePureComponent {
@@ -83,7 +102,7 @@ class AccountCard extends ImmutablePureComponent {
onMute: PropTypes.func.isRequired,
};
_updateEmojis () {
_updateEmojis() {
const node = this.node;
if (!node || autoPlayGif) {
@@ -104,68 +123,113 @@ class AccountCard extends ImmutablePureComponent {
}
}
componentDidMount () {
componentDidMount() {
this._updateEmojis();
}
componentDidUpdate () {
componentDidUpdate() {
this._updateEmojis();
}
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
};
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
};
handleBlock = () => {
this.props.onBlock(this.props.account);
}
};
handleMute = () => {
this.props.onMute(this.props.account);
}
};
setRef = (c) => {
this.node = c;
}
};
render () {
render() {
const { account, intl } = this.props;
let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) {
if (
account.get('id') !== me &&
account.get('relationship', null) !== null
) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
buttons = (
<IconButton
disabled
icon='hourglass'
title={intl.formatMessage(messages.requested)}
/>
);
} else if (blocking) {
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
buttons = (
<IconButton
active
icon='unlock'
title={intl.formatMessage(messages.unblock, {
name: account.get('username'),
})}
onClick={this.handleBlock}
/>
);
} else if (muting) {
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
buttons = (
<IconButton
active
icon='volume-up'
title={intl.formatMessage(messages.unmute, {
name: account.get('username'),
})}
onClick={this.handleMute}
/>
);
} else if (!account.get('moved') || following) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
buttons = (
<IconButton
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow,
)}
onClick={this.handleFollow}
active={following}
/>
);
}
}
return (
<div className='directory__card'>
<div className='directory__card__img'>
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' />
<img
src={
autoPlayGif ? account.get('header') : account.get('header_static')
}
alt=''
/>
</div>
<div className='directory__card__bar'>
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<Permalink
className='directory__card__bar__name'
href={account.get('url')}
to={`/accounts/${account.get('id')}`}
>
<Avatar account={account} size={48} />
<DisplayName account={account} />
</Permalink>
@@ -176,13 +240,44 @@ class AccountCard extends ImmutablePureComponent {
</div>
<div className='directory__card__extra' ref={this.setRef}>
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
<div
className='account__header__content'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
</div>
<div className='directory__card__extra'>
<div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
<div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
<div className='accounts-table__count'>
<ShortNumber value={account.get('statuses_count')} />
<small>
<FormattedMessage id='account.posts' defaultMessage='Toots' />
</small>
</div>
<div className='accounts-table__count'>
<ShortNumber value={account.get('followers_count')} />{' '}
<small>
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
</small>
</div>
<div className='accounts-table__count'>
{account.get('last_status_at') === null ? (
<FormattedMessage
id='account.never_active'
defaultMessage='Never'
/>
) : (
<RelativeTimestamp timestamp={account.get('last_status_at')} />
)}{' '}
<small>
<FormattedMessage
id='account.last_status'
defaultMessage='Last active'
/>
</small>
</div>
</div>
</div>
);

View File

@@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/async';
import { NonceProvider } from 'react-select';
import SettingToggle from '../../notifications/components/setting_toggle';
const messages = defineMessages({
@@ -58,18 +59,20 @@ class ColumnSettings extends React.PureComponent {
{this.modeLabel(mode)}
</span>
<AsyncSelect
isMulti
autoFocus
value={this.tags(mode)}
onChange={this.onSelect(mode)}
loadOptions={this.props.onLoad}
className='column-select__container'
classNamePrefix='column-select'
name='tags'
placeholder={this.props.intl.formatMessage(messages.placeholder)}
noOptionsMessage={this.noOptionsMessage}
/>
<NonceProvider nonce={document.querySelector('meta[name=style-nonce]').content}>
<AsyncSelect
isMulti
autoFocus
value={this.tags(mode)}
onChange={this.onSelect(mode)}
loadOptions={this.props.onLoad}
className='column-select__container'
classNamePrefix='column-select'
name='tags'
placeholder={this.props.intl.formatMessage(messages.placeholder)}
noOptionsMessage={this.noOptionsMessage}
/>
</NonceProvider>
</div>
);
}

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { changeUploadCompose } from '../../../actions/compose';
import { changeUploadCompose, uploadThumbnail } from '../../../actions/compose';
import { getPointerPosition } from '../../video';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
@@ -23,11 +23,13 @@ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' },
});
const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
account: state.getIn(['accounts', me]),
isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
});
const mapDispatchToProps = (dispatch, { id }) => ({
@@ -36,6 +38,10 @@ const mapDispatchToProps = (dispatch, { id }) => ({
dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
},
onSelectThumbnail: files => {
dispatch(uploadThumbnail(id, files[0]));
},
});
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
@@ -81,6 +87,9 @@ class FocalPointModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
account: ImmutablePropTypes.map.isRequired,
isUploadingThumbnail: PropTypes.bool,
onSave: PropTypes.func.isRequired,
onSelectThumbnail: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
@@ -235,13 +244,29 @@ class FocalPointModal extends ImmutablePureComponent {
}).catch(() => this.setState({ detecting: false }));
}
handleThumbnailChange = e => {
if (e.target.files.length > 0) {
this.setState({ dirty: true });
this.props.onSelectThumbnail(e.target.files);
}
}
setFileInputRef = c => {
this.fileInput = c;
}
handleFileInputClick = () => {
this.fileInput.click();
}
render () {
const { media, intl, account, onClose } = this.props;
const { media, intl, account, onClose, isUploadingThumbnail } = this.props;
const { x, y, dragging, description, dirty, detecting, progress } = this.state;
const width = media.getIn(['meta', 'original', 'width']) || null;
const height = media.getIn(['meta', 'original', 'height']) || null;
const focals = ['image', 'gifv'].includes(media.get('type'));
const thumbnailable = ['audio', 'video'].includes(media.get('type'));
const previewRatio = 16/9;
const previewWidth = 200;
@@ -268,6 +293,30 @@ class FocalPointModal extends ImmutablePureComponent {
<div className='report-modal__comment'>
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
{thumbnailable && (
<React.Fragment>
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
<Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
<input
id='upload-modal__thumbnail'
ref={this.setFileInputRef}
type='file'
accept='image/png,image/jpeg'
onChange={this.handleThumbnailChange}
style={{ display: 'none' }}
disabled={isUploadingThumbnail}
/>
</label>
<hr className='setting-divider' />
</React.Fragment>
)}
<label className='setting-text-label' htmlFor='upload-modal__description'>
{descriptionLabel}
</label>
@@ -293,7 +342,7 @@ class FocalPointModal extends ImmutablePureComponent {
<CharacterCounter max={1500} text={detecting ? '' : description} />
</div>
<Button disabled={!dirty || detecting || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
<Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
</div>
<div className='focal-point-modal__content'>