Support explicit addressing

merge-requests/955/merge
marcin mikołajczak 3 years ago committed by Alex Gleason
parent 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;
}
}
Loading…
Cancel
Save