parent
43acb4f880
commit
3dffc46fc1
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class StatusReplyMentions extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status } = this.props;
|
||||||
|
|
||||||
|
const to = status.get('mentions', []);
|
||||||
|
|
||||||
|
if (!status.get('in_reply_to_id') || !to || to.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='reply-mentions'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='reply_mentions.reply'
|
||||||
|
defaultMessage='Replying to {accounts}{more}'
|
||||||
|
values={{
|
||||||
|
accounts: to.slice(0, 2).map(account => (<>
|
||||||
|
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||||
|
<Link to={`/@${account.get('acct')}`} className='reply-mentions__account'>@{account.get('acct').split('@')[0]}</Link>
|
||||||
|
</HoverRefWrapper>
|
||||||
|
{' '}
|
||||||
|
</>)),
|
||||||
|
more: to.size > 2 && (
|
||||||
|
<Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/mentions`}>
|
||||||
|
<FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class ReplyMentions extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onOpenMentionsModal: PropTypes.func.isRequired,
|
||||||
|
explicitAddressing: PropTypes.bool,
|
||||||
|
to: ImmutablePropTypes.orderedSet,
|
||||||
|
isReply: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.props.onOpenMentionsModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { explicitAddressing, to, isReply } = this.props;
|
||||||
|
|
||||||
|
if (!explicitAddressing || !isReply || !to || to.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href='#' className='reply-mentions' onClick={this.handleClick}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='reply_mentions.reply'
|
||||||
|
defaultMessage='Replying to {accounts}{more}'
|
||||||
|
values={{
|
||||||
|
accounts: to.slice(0, 2).map(acct => <><span className='reply-mentions__account'>@{acct.split('@')[0]}</span>{' '}</>),
|
||||||
|
more: to.size > 2 && <FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetStatus } from 'soapbox/selectors';
|
||||||
|
import { openModal } from 'soapbox/actions/modal';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
import ReplyMentions from '../components/reply_mentions';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
|
return state => {
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const { explicitAddressing } = getFeatures(instance);
|
||||||
|
|
||||||
|
if (!explicitAddressing) {
|
||||||
|
return {
|
||||||
|
explicitAddressing: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) });
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return {
|
||||||
|
isReply: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const to = state.getIn(['compose', 'to']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
to,
|
||||||
|
isReply: true,
|
||||||
|
explicitAddressing: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onOpenMentionsModal() {
|
||||||
|
dispatch(openModal('REPLY_MENTIONS', {
|
||||||
|
onCancel: () => dispatch(openModal('COMPOSE')),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyMentions);
|
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
|
import MissingIndicator from '../../components/missing_indicator';
|
||||||
|
import { fetchStatus } from '../../actions/statuses';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import AccountContainer from '../../containers/account_container';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
|
import { makeGetStatus } from '../../selectors';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.mentions', defaultMessage: 'Mentions' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
const status = getStatus(state, {
|
||||||
|
id: props.params.statusId,
|
||||||
|
username: props.params.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
accountIds: status ? ImmutableOrderedSet(status.get('mentions').map(m => m.get('id'))) : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Mentions extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
params: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
accountIds: ImmutablePropTypes.orderedSet,
|
||||||
|
status: ImmutablePropTypes.map,
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData = () => {
|
||||||
|
const { dispatch, params } = this.props;
|
||||||
|
const { statusId } = params;
|
||||||
|
|
||||||
|
dispatch(fetchStatus(statusId));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { params } = this.props;
|
||||||
|
|
||||||
|
if (params.statusId !== prevProps.params.statusId) {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, accountIds, status } = this.props;
|
||||||
|
|
||||||
|
if (!accountIds) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<MissingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column heading={intl.formatMessage(messages.heading)}>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='reblogs'
|
||||||
|
>
|
||||||
|
{accountIds.map(id =>
|
||||||
|
<AccountContainer key={id} id={id} withNote={false} />,
|
||||||
|
)}
|
||||||
|
</ScrollableList>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import Avatar from 'soapbox/components/avatar';
|
||||||
|
import DisplayName from 'soapbox/components/display_name';
|
||||||
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { addToMentions, removeFromMentions } from 'soapbox/actions/compose';
|
||||||
|
import { fetchAccount } from 'soapbox/actions/accounts';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' },
|
||||||
|
add: { id: 'reply_mentions.account.add', defaultMessage: 'Add to mentions' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { accountId }) => {
|
||||||
|
const account = getAccount(state, accountId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
added: !!account && state.getIn(['compose', 'to']).includes(account.get('acct')),
|
||||||
|
account,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||||
|
onRemove: () => dispatch(removeFromMentions(accountId)),
|
||||||
|
onAdd: () => dispatch(addToMentions(accountId)),
|
||||||
|
fetchAccount: () => dispatch(fetchAccount(accountId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(makeMapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
accountId: PropTypes.string.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
onAdd: PropTypes.func.isRequired,
|
||||||
|
added: PropTypes.bool,
|
||||||
|
author: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
added: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { account, accountId } = this.props;
|
||||||
|
|
||||||
|
if (accountId && !account) {
|
||||||
|
this.props.fetchAccount(accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { account, intl, onRemove, onAdd, added, author } = this.props;
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
let button;
|
||||||
|
|
||||||
|
if (added) {
|
||||||
|
button = <IconButton src={require('@tabler/icons/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||||
|
} else {
|
||||||
|
button = <IconButton src={require('@tabler/icons/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='account'>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<div className='account__display-name'>
|
||||||
|
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='account__relationship'>
|
||||||
|
{!author && button}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
|
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
|
||||||
|
import { makeGetStatus } from 'soapbox/selectors';
|
||||||
|
import Account from '../../reply_mentions/account';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
|
return state => {
|
||||||
|
const status = getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) });
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return {
|
||||||
|
isReply: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const me = state.get('me');
|
||||||
|
const account = state.getIn(['accounts', me]);
|
||||||
|
|
||||||
|
const mentions = statusToMentionsAccountIdsArray(state, status, account);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mentions,
|
||||||
|
author: status.getIn(['account', 'id']),
|
||||||
|
to: state.getIn(['compose', 'to']),
|
||||||
|
isReply: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ComposeModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
author: PropTypes.string,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
inReplyTo: PropTypes.string,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickClose = () => {
|
||||||
|
const { onClose, onCancel } = this.props;
|
||||||
|
onClose('COMPOSE');
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, mentions, author } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal reply-mentions-modal'>
|
||||||
|
<div className='reply-mentions-modal__header'>
|
||||||
|
<IconButton
|
||||||
|
className='reply-mentions-modal__back'
|
||||||
|
src={require('@tabler/icons/icons/arrow-left.svg')}
|
||||||
|
onClick={this.onClickClose}
|
||||||
|
aria-label={intl.formatMessage(messages.close)}
|
||||||
|
title={intl.formatMessage(messages.close)}
|
||||||
|
/>
|
||||||
|
<h3 className='reply-mentions-modal__header__title'>
|
||||||
|
<FormattedMessage id='navigation_bar.in_reply_to' defaultMessage='In reply to' />
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className='reply-mentions-modal__accounts'>
|
||||||
|
{mentions.map(accountId => <Account key={accountId} accountId={accountId} added author={author === accountId} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(connect(makeMapStateToProps)(ComposeModal));
|
@ -0,0 +1,24 @@
|
|||||||
|
.reply-mentions {
|
||||||
|
margin: 0 10px;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-size: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&__account,
|
||||||
|
a {
|
||||||
|
color: var(--highlight-text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__wrapper,
|
||||||
|
.detailed-status {
|
||||||
|
.reply-mentions {
|
||||||
|
display: block;
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue