sforkowany z mirror/soapbox
Support explicit addressing
rodzic
43acb4f880
commit
3dffc46fc1
|
@ -68,6 +68,9 @@ export const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
|
|||
export const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
|
||||
export const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
|
||||
|
||||
export const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
|
||||
export const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
|
||||
|
||||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
|
@ -93,10 +96,14 @@ export function changeCompose(text) {
|
|||
export function replyCompose(status, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const instance = state.get('instance');
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_REPLY,
|
||||
status: status,
|
||||
account: state.getIn(['accounts', state.get('me')]),
|
||||
explicitAddressing,
|
||||
});
|
||||
|
||||
dispatch(openModal('COMPOSE'));
|
||||
|
@ -183,6 +190,7 @@ export function submitCompose(routerHistory, force = false) {
|
|||
|
||||
const status = state.getIn(['compose', 'text'], '');
|
||||
const media = state.getIn(['compose', 'media_attachments']);
|
||||
let to = state.getIn(['compose', 'to'], null);
|
||||
|
||||
if (!validateSchedule(state)) {
|
||||
dispatch(snackbar.error(messages.scheduleError));
|
||||
|
@ -200,6 +208,13 @@ export function submitCompose(routerHistory, force = false) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (to && status) {
|
||||
const mentions = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/g); // not a perfect regex
|
||||
|
||||
if (mentions)
|
||||
to = to.union(mentions.map(mention => mention.trim().slice(1)));
|
||||
}
|
||||
|
||||
dispatch(submitComposeRequest());
|
||||
dispatch(closeModal());
|
||||
|
||||
|
@ -215,6 +230,7 @@ export function submitCompose(routerHistory, force = false) {
|
|||
content_type: state.getIn(['compose', 'content_type']),
|
||||
poll: state.getIn(['compose', 'poll'], null),
|
||||
scheduled_at: state.getIn(['compose', 'schedule'], null),
|
||||
to,
|
||||
};
|
||||
|
||||
dispatch(createStatus(params, idempotencyKey)).then(function(data) {
|
||||
|
@ -643,3 +659,27 @@ export function openComposeWithText(text = '') {
|
|||
dispatch(changeCompose(text));
|
||||
};
|
||||
}
|
||||
|
||||
export function addToMentions(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const acct = state.getIn(['accounts', accountId, 'acct']);
|
||||
|
||||
return dispatch({
|
||||
type: COMPOSE_ADD_TO_MENTIONS,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeFromMentions(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const acct = state.getIn(['accounts', accountId, 'acct']);
|
||||
|
||||
return dispatch({
|
||||
type: COMPOSE_REMOVE_FROM_MENTIONS,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { deleteFromTimelines } from './timelines';
|
|||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { openModal } from './modal';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { shouldHaveCard } from 'soapbox/utils/status';
|
||||
|
||||
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
|
||||
|
@ -95,10 +96,17 @@ export function fetchStatus(id) {
|
|||
}
|
||||
|
||||
export function redraft(status, raw_text) {
|
||||
return {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const instance = state.get('instance');
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
explicitAddressing,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import RelativeTimestamp from './relative_timestamp';
|
|||
import DisplayName from './display_name';
|
||||
import StatusContent from './status_content';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusReplyMentions from './status_reply_mentions';
|
||||
import AttachmentThumbs from './attachment_thumbs';
|
||||
import Card from '../features/status/components/card';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
@ -538,6 +539,8 @@ class Status extends ImmutablePureComponent {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<StatusReplyMentions status={this._properStatus()} />
|
||||
|
||||
<StatusContent
|
||||
status={status}
|
||||
reblogContent={reblogContent}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import ReplyMentions from '../containers/reply_mentions_container';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
|
@ -308,7 +309,9 @@ export default class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
<WarningContainer />
|
||||
|
||||
{ !shouldCondense && <ReplyIndicatorContainer /> }
|
||||
{!shouldCondense && <ReplyIndicatorContainer />}
|
||||
|
||||
{!shouldCondense && <ReplyMentions />}
|
||||
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||
<AutosuggestInput
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import StatusReplyMentions from '../../../components/status_reply_mentions';
|
||||
import MediaGallery from '../../../components/media_gallery';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { FormattedDate } from 'react-intl';
|
||||
|
@ -18,7 +20,8 @@ import StatusInteractionBar from './status_interaction_bar';
|
|||
import { getDomain } from 'soapbox/utils/accounts';
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
|
||||
export default class DetailedStatus extends ImmutablePureComponent {
|
||||
export default @injectIntl
|
||||
class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
|
@ -82,6 +85,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||
const outerStyle = { boxSizing: 'border-box' };
|
||||
|
@ -185,6 +189,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<StatusReplyMentions status={status} />
|
||||
|
||||
<StatusContent
|
||||
status={status}
|
||||
expanded={!status.get('hidden')}
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
FocalPointModal,
|
||||
HotkeysModal,
|
||||
ComposeModal,
|
||||
ReplyMentionsModal,
|
||||
UnauthorizedModal,
|
||||
EditFederationModal,
|
||||
ComponentModal,
|
||||
|
@ -41,6 +42,7 @@ const MODAL_COMPONENTS = {
|
|||
'LIST_ADDER': ListAdder,
|
||||
'HOTKEYS': HotkeysModal,
|
||||
'COMPOSE': ComposeModal,
|
||||
'REPLY_MENTIONS': ReplyMentionsModal,
|
||||
'UNAUTHORIZED': UnauthorizedModal,
|
||||
'CRYPTO_DONATE': CryptoDonateModal,
|
||||
'EDIT_FEDERATION': EditFederationModal,
|
||||
|
|
|
@ -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));
|
|
@ -58,6 +58,7 @@ import {
|
|||
Following,
|
||||
Reblogs,
|
||||
Reactions,
|
||||
Mentions,
|
||||
Favourites,
|
||||
DirectTimeline,
|
||||
Conversations,
|
||||
|
@ -300,6 +301,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/@:username/posts/:statusId/reblogs' page={DefaultPage} component={Reblogs} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/likes' page={DefaultPage} component={Favourites} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/reactions/:reaction?' page={DefaultPage} component={Reactions} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/mentions' page={DefaultPage} component={Mentions} content={children} />
|
||||
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
|
||||
|
||||
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
|
||||
|
|
|
@ -102,6 +102,10 @@ export function Reactions() {
|
|||
return import(/* webpackChunkName: "features/reactions" */'../../reactions');
|
||||
}
|
||||
|
||||
export function Mentions() {
|
||||
return import(/* webpackChunkName: "features/mentions" */'../../mentions');
|
||||
}
|
||||
|
||||
export function Favourites() {
|
||||
return import(/* webpackChunkName: "features/favourites" */'../../favourites');
|
||||
}
|
||||
|
@ -190,6 +194,10 @@ export function ComposeModal() {
|
|||
return import(/* webpackChunkName: "features/ui" */'../components/compose_modal');
|
||||
}
|
||||
|
||||
export function ReplyMentionsModal() {
|
||||
return import(/* webpackChunkName: "features/ui" */'../components/reply_mentions_modal');
|
||||
}
|
||||
|
||||
export function UnauthorizedModal() {
|
||||
return import(/* webpackChunkName: "features/ui" */'../components/unauthorized_modal');
|
||||
}
|
||||
|
|
|
@ -209,6 +209,7 @@
|
|||
"column.import_data": "Importuj dane",
|
||||
"column.info": "Informacje o serwerze",
|
||||
"column.lists": "Listy",
|
||||
"column.mentions": "W odpowiedzi do",
|
||||
"column.mfa": "Uwierzytelnianie wieloetapowe",
|
||||
"column.mfa_cancel": "Anuluj",
|
||||
"column.mfa_confirm_button": "Potwierdź",
|
||||
|
@ -610,6 +611,7 @@
|
|||
"navigation_bar.mutes": "Wyciszeni użytkownicy",
|
||||
"navigation_bar.pins": "Przypięte wpisy",
|
||||
"navigation_bar.preferences": "Preferencje",
|
||||
"navigation_bar.in_reply_to": "W odpowiedzi do",
|
||||
"navigation_bar.security": "Bezpieczeństwo",
|
||||
"navigation_bar.soapbox_config": "Konfiguracja Soapbox",
|
||||
"notification.chat_mention": "{name} wysłał(a) Ci wiadomośść",
|
||||
|
@ -754,6 +756,10 @@
|
|||
"remote_interaction.user_not_found_error": "Nie można odnaleźć podanego użytkownika",
|
||||
"remote_timeline.filter_message": "Przeglądasz oś czasu {instance}",
|
||||
"reply_indicator.cancel": "Anuluj",
|
||||
"reply_mentions.account.add": "Dodaj do wspomnianych",
|
||||
"reply_mentions.account.remove": "Usuń z wspomnianych",
|
||||
"reply_mentions.more": "i {count} więcej",
|
||||
"reply_mentions.reply": "W odpowiedzi do {accounts}{more}",
|
||||
"report.block": "Zablokuj {target}",
|
||||
"report.block_hint": "Czy chcesz też zablokować to konto?",
|
||||
"report.forward": "Przekaż na {target}",
|
||||
|
|
|
@ -39,6 +39,8 @@ import {
|
|||
COMPOSE_POLL_OPTION_CHANGE,
|
||||
COMPOSE_POLL_OPTION_REMOVE,
|
||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
COMPOSE_ADD_TO_MENTIONS,
|
||||
COMPOSE_REMOVE_FROM_MENTIONS,
|
||||
} from '../actions/compose';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { REDRAFT } from '../actions/statuses';
|
||||
|
@ -84,7 +86,7 @@ const initialPoll = ImmutableMap({
|
|||
multiple: false,
|
||||
});
|
||||
|
||||
function statusToTextMentions(state, status, account) {
|
||||
const statusToTextMentions = (state, status, account) => {
|
||||
const author = status.getIn(['account', 'acct']);
|
||||
const mentions = status.get('mentions', []).map(m => m.get('acct'));
|
||||
|
||||
|
@ -93,12 +95,31 @@ function statusToTextMentions(state, status, account) {
|
|||
.delete(account.get('acct'))
|
||||
.map(m => `@${m} `)
|
||||
.join('');
|
||||
}
|
||||
};
|
||||
|
||||
export const statusToMentionsArray = (state, status, account) => {
|
||||
const author = status.getIn(['account', 'acct']);
|
||||
const mentions = status.get('mentions', []).map(m => m.get('acct'));
|
||||
|
||||
return ImmutableOrderedSet([author])
|
||||
.concat(mentions)
|
||||
.delete(account.get('acct'));
|
||||
};
|
||||
|
||||
export const statusToMentionsAccountIdsArray = (state, status, account) => {
|
||||
const author = status.getIn(['account', 'id']);
|
||||
const mentions = status.get('mentions', []).map(m => m.get('id'));
|
||||
|
||||
return ImmutableOrderedSet([author])
|
||||
.concat(mentions)
|
||||
.delete(account.get('id'));
|
||||
};
|
||||
|
||||
function clearAll(state) {
|
||||
return state.withMutations(map => {
|
||||
map.set('id', null);
|
||||
map.set('text', '');
|
||||
map.set('to', ImmutableOrderedSet());
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('content_type', state.get('default_content_type'));
|
||||
|
@ -197,6 +218,17 @@ const expandMentions = status => {
|
|||
return fragment.innerHTML;
|
||||
};
|
||||
|
||||
const getExplicitMentions = (me, status) => {
|
||||
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
|
||||
|
||||
const mentions = status
|
||||
.get('mentions')
|
||||
.filter(mention => !(fragment.querySelector(`a[href="${mention.get('url')}"]`) || mention.get('id') === me))
|
||||
.map(m => m.get('acct'));
|
||||
|
||||
return ImmutableOrderedSet(mentions);
|
||||
};
|
||||
|
||||
const getAccountSettings = account => {
|
||||
return account.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap());
|
||||
};
|
||||
|
@ -290,7 +322,8 @@ export default function compose(state = initialState, action) {
|
|||
case COMPOSE_REPLY:
|
||||
return state.withMutations(map => {
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('text', statusToTextMentions(state, action.status, action.account));
|
||||
map.set('to', action.explicitAddressing ? statusToMentionsArray(state, action.status, action.account) : undefined);
|
||||
map.set('text', !action.explicitAddressing ? statusToTextMentions(state, action.status, action.account) : '');
|
||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
|
@ -373,6 +406,7 @@ export default function compose(state = initialState, action) {
|
|||
case REDRAFT:
|
||||
return state.withMutations(map => {
|
||||
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
|
||||
map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.get('account', 'id'), action.status) : null);
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||
map.set('privacy', action.status.get('visibility'));
|
||||
// TODO: Actually fix this rather than just removing it
|
||||
|
@ -416,6 +450,10 @@ export default function compose(state = initialState, action) {
|
|||
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
|
||||
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||
case COMPOSE_ADD_TO_MENTIONS:
|
||||
return state.update('to', mentions => mentions.add(action.account));
|
||||
case COMPOSE_REMOVE_FROM_MENTIONS:
|
||||
return state.update('to', mentions => mentions.delete(action.account));
|
||||
case ME_FETCH_SUCCESS:
|
||||
return importAccount(state, action.me);
|
||||
case ME_PATCH_SUCCESS:
|
||||
|
|
|
@ -75,6 +75,7 @@ export const getFeatures = createSelector([
|
|||
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||
]),
|
||||
remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||
explicitAddressing: v.software === PLEROMA && gte(v.version, '1.0.0'),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
@import 'components/emoji-reacts';
|
||||
@import 'components/status';
|
||||
@import 'components/reply-indicator';
|
||||
@import 'components/reply-mentions';
|
||||
@import 'components/detailed-status';
|
||||
@import 'components/list-forms';
|
||||
@import 'components/media-gallery';
|
||||
|
|
|
@ -725,7 +725,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.compose-modal {
|
||||
.compose-modal,
|
||||
.reply-mentions-modal {
|
||||
overflow: hidden;
|
||||
background-color: var(--background-color);
|
||||
border-radius: 6px;
|
||||
|
@ -755,6 +756,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 895px) {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
.compose-modal {
|
||||
&__close {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
@ -808,12 +818,27 @@
|
|||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 895px) {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
.reply-mentions-modal {
|
||||
&__back {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
left: max(10px, env(safe-area-inset-right));
|
||||
color: var(--primary-text-color--faint);
|
||||
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__accounts {
|
||||
display: block;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue