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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
@@ -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> }} />,
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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'>
|
||||
|
Reference in New Issue
Block a user