diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 421df5883..124ef69c4 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -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, + }); + }; +} diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 440d4150e..ec5811cba 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -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, + }); }; } diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index ff7f23f52..ff9a2eb38 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -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 { )} + + + (<> + + @{account.get('acct').split('@')[0]} + + {' '} + )), + more: to.size > 2 && ( + + + + ), + }} + /> + + ); + } + +} \ No newline at end of file diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js index 11a839f63..6698d2232 100644 --- a/app/soapbox/features/compose/components/compose_form.js +++ b/app/soapbox/features/compose/components/compose_form.js @@ -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 { - { !shouldCondense && } + {!shouldCondense && } + + {!shouldCondense && }
{ + e.preventDefault(); + + this.props.onOpenMentionsModal(); + } + + render() { + const { explicitAddressing, to, isReply } = this.props; + + if (!explicitAddressing || !isReply || !to || to.length === 0) { + return null; + } + + return ( + + <>@{acct.split('@')[0]}{' '}), + more: to.size > 2 && , + }} + /> + + ); + } + +} \ No newline at end of file diff --git a/app/soapbox/features/compose/containers/reply_mentions_container.js b/app/soapbox/features/compose/containers/reply_mentions_container.js new file mode 100644 index 000000000..799552a0b --- /dev/null +++ b/app/soapbox/features/compose/containers/reply_mentions_container.js @@ -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); diff --git a/app/soapbox/features/mentions/index.js b/app/soapbox/features/mentions/index.js new file mode 100644 index 000000000..d77e7b8f2 --- /dev/null +++ b/app/soapbox/features/mentions/index.js @@ -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 ( + + + + ); + } + + if (!status) { + return ( + + + + ); + } + + return ( + + + {accountIds.map(id => + , + )} + + + ); + } + +} diff --git a/app/soapbox/features/reply_mentions/account.js b/app/soapbox/features/reply_mentions/account.js new file mode 100644 index 000000000..c9da9b6a4 --- /dev/null +++ b/app/soapbox/features/reply_mentions/account.js @@ -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 = ; + } else { + button = ; + } + + return ( +
+
+
+
+ +
+ +
+ {!author && button} +
+
+
+ ); + } + +} diff --git a/app/soapbox/features/status/components/detailed_status.js b/app/soapbox/features/status/components/detailed_status.js index 91f0376e7..2e8d0944e 100644 --- a/app/soapbox/features/status/components/detailed_status.js +++ b/app/soapbox/features/status/components/detailed_status.js @@ -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 {
)} + + { + 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 ( +
+
+ +

+ +

+
+
+ {mentions.map(accountId => )} +
+
+ ); + } + +} + +export default injectIntl(connect(makeMapStateToProps)(ComposeModal)); diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 85b376d54..2de0dcbfb 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -58,6 +58,7 @@ import { Following, Reblogs, Reactions, + Mentions, Favourites, DirectTimeline, Conversations, @@ -300,6 +301,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 81b29c3a7..69dd8aff6 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -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'); } diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 5837f9e0b..e8f975a80 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -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}", diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.js index 344dad6c1..a9b4e4bed 100644 --- a/app/soapbox/reducers/compose.js +++ b/app/soapbox/reducers/compose.js @@ -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: diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index 124df7d2d..ddab89c73 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -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'), }; }); diff --git a/app/styles/application.scss b/app/styles/application.scss index 69308cd47..2918173a9 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -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'; diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 1b8b810f1..b39da7545 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -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; } } diff --git a/app/styles/components/reply-mentions.scss b/app/styles/components/reply-mentions.scss new file mode 100644 index 000000000..444852026 --- /dev/null +++ b/app/styles/components/reply-mentions.scss @@ -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; + } +}