diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js index c292abaac..c67789117 100644 --- a/app/soapbox/actions/interactions.js +++ b/app/soapbox/actions/interactions.js @@ -48,6 +48,10 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; +export const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST'; +export const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS'; +export const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL'; + const messages = defineMessages({ bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' }, bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' }, @@ -475,3 +479,46 @@ export function unpinFail(status, error) { skipLoading: true, }; } + +export function remoteInteraction(ap_id, profile) { + return (dispatch, getState) => { + dispatch(remoteInteractionRequest(ap_id, profile)); + + return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => { + if (data.error) throw new Error(data.error); + + dispatch(remoteInteractionSuccess(ap_id, profile, data.url)); + + return data.url; + }).catch(error => { + dispatch(remoteInteractionFail(ap_id, profile, error)); + throw error; + }); + }; +} + +export function remoteInteractionRequest(ap_id, profile) { + return { + type: REMOTE_INTERACTION_REQUEST, + ap_id, + profile, + }; +} + +export function remoteInteractionSuccess(ap_id, profile, url) { + return { + type: REMOTE_INTERACTION_SUCCESS, + ap_id, + profile, + url, + }; +} + +export function remoteInteractionFail(ap_id, profile, error) { + return { + type: REMOTE_INTERACTION_FAIL, + ap_id, + profile, + error, + }; +} diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index 7962ee779..a5b8b038e 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -34,6 +34,7 @@ class Poll extends ImmutablePureComponent { dispatch: PropTypes.func, disabled: PropTypes.bool, me: SoapboxPropTypes.me, + status: PropTypes.string, }; state = { @@ -81,7 +82,11 @@ class Poll extends ImmutablePureComponent { }; openUnauthorizedModal = () => { - this.props.dispatch(openModal('UNAUTHORIZED')); + const { dispatch, status } = this.props; + dispatch(openModal('UNAUTHORIZED', { + action: 'POLL_VOTE', + ap_id: status, + })); } handleRefresh = () => { diff --git a/app/soapbox/components/status_action_bar.js b/app/soapbox/components/status_action_bar.js index d4356c097..d552a82b8 100644 --- a/app/soapbox/components/status_action_bar.js +++ b/app/soapbox/components/status_action_bar.js @@ -117,11 +117,11 @@ class StatusActionBar extends ImmutablePureComponent { ] handleReplyClick = () => { - const { me } = this.props; + const { me, onReply, onOpenUnauthorizedModal, status } = this.props; if (me) { - this.props.onReply(this.props.status, this.context.router.history); + onReply(status, this.context.router.history); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('REPLY'); } } @@ -167,22 +167,22 @@ class StatusActionBar extends ImmutablePureComponent { handleReactClick = emoji => { return e => { - const { me, status } = this.props; + const { me, dispatch, onOpenUnauthorizedModal, status } = this.props; if (me) { - this.props.dispatch(simpleEmojiReact(status, emoji)); + dispatch(simpleEmojiReact(status, emoji)); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('FAVOURITE'); } this.setState({ emojiSelectorVisible: false }); }; } handleFavouriteClick = () => { - const { me } = this.props; + const { me, onFavourite, onOpenUnauthorizedModal, status } = this.props; if (me) { - this.props.onFavourite(this.props.status); + onFavourite(status); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('FAVOURITE'); } } @@ -191,11 +191,11 @@ class StatusActionBar extends ImmutablePureComponent { } handleReblogClick = e => { - const { me } = this.props; + const { me, onReblog, onOpenUnauthorizedModal, status } = this.props; if (me) { - this.props.onReblog(this.props.status, e); + onReblog(status, e); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('REBLOG'); } } @@ -599,10 +599,13 @@ const mapStateToProps = state => { }; }; -const mapDispatchToProps = (dispatch) => ({ +const mapDispatchToProps = (dispatch, { status }) => ({ dispatch, - onOpenUnauthorizedModal() { - dispatch(openModal('UNAUTHORIZED')); + onOpenUnauthorizedModal(action) { + dispatch(openModal('UNAUTHORIZED', { + action, + ap_id: status.get('url'), + })); }, }); diff --git a/app/soapbox/components/status_content.js b/app/soapbox/components/status_content.js index 508e2de1f..efbe81b5d 100644 --- a/app/soapbox/components/status_content.js +++ b/app/soapbox/components/status_content.js @@ -242,7 +242,7 @@ class StatusContent extends React.PureComponent {
- {!hidden && !!status.get('poll') && } + {!hidden && !!status.get('poll') && }
); } else if (this.props.onClick) { @@ -265,7 +265,7 @@ class StatusContent extends React.PureComponent { } if (status.get('poll')) { - output.push(); + output.push(); } return output; @@ -285,7 +285,7 @@ class StatusContent extends React.PureComponent { ]; if (status.get('poll')) { - output.push(); + output.push(); } return output; diff --git a/app/soapbox/containers/poll_container.js b/app/soapbox/containers/poll_container.js index dc35964f5..ad9672120 100644 --- a/app/soapbox/containers/poll_container.js +++ b/app/soapbox/containers/poll_container.js @@ -6,4 +6,5 @@ const mapStateToProps = (state, { pollId }) => ({ me: state.get('me'), }); + export default connect(mapStateToProps)(Poll); diff --git a/app/soapbox/features/status/components/action_bar.js b/app/soapbox/features/status/components/action_bar.js index 3a6da7475..a3f42adb9 100644 --- a/app/soapbox/features/status/components/action_bar.js +++ b/app/soapbox/features/status/components/action_bar.js @@ -66,9 +66,12 @@ const mapStateToProps = state => { }; }; -const mapDispatchToProps = (dispatch) => ({ - onOpenUnauthorizedModal() { - dispatch(openModal('UNAUTHORIZED')); +const mapDispatchToProps = (dispatch, { status }) => ({ + onOpenUnauthorizedModal(action) { + dispatch(openModal('UNAUTHORIZED', { + action, + ap_id: status.get('url'), + })); }, }); @@ -121,20 +124,20 @@ class ActionBar extends React.PureComponent { } handleReplyClick = () => { - const { me } = this.props; + const { me, onReply, onOpenUnauthorizedModal } = this.props; if (me) { - this.props.onReply(this.props.status); + onReply(this.props.status); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('REPLY'); } } handleReblogClick = (e) => { - const { me } = this.props; + const { me, onReblog, onOpenUnauthorizedModal, status } = this.props; if (me) { - this.props.onReblog(this.props.status, e); + onReblog(status, e); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('REBLOG'); } } @@ -143,11 +146,11 @@ class ActionBar extends React.PureComponent { } handleFavouriteClick = () => { - const { me } = this.props; + const { me, onFavourite, onOpenUnauthorizedModal } = this.props; if (me) { - this.props.onFavourite(this.props.status); + onFavourite(status); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('FAVOURITE'); } } @@ -184,11 +187,11 @@ class ActionBar extends React.PureComponent { handleReactClick = emoji => { return e => { - const { me } = this.props; + const { me, onEmojiReact, onOpenUnauthorizedModal, status } = this.props; if (me) { - this.props.onEmojiReact(this.props.status, emoji); + onEmojiReact(status, emoji); } else { - this.props.onOpenUnauthorizedModal(); + onOpenUnauthorizedModal('FAVOURITE'); } this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false }); }; diff --git a/app/soapbox/features/ui/components/action_button.js b/app/soapbox/features/ui/components/action_button.js index e55b41142..152f6f467 100644 --- a/app/soapbox/features/ui/components/action_button.js +++ b/app/soapbox/features/ui/components/action_button.js @@ -13,6 +13,8 @@ import { blockAccount, unblockAccount, } from 'soapbox/actions/accounts'; +import { openModal } from 'soapbox/actions/modal'; +import { getFeatures } from 'soapbox/utils/features'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -26,8 +28,11 @@ const messages = defineMessages({ const mapStateToProps = state => { const me = state.get('me'); + const instance = state.get('instance'); + return { me, + features: getFeatures(instance), }; }; @@ -47,6 +52,14 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(blockAccount(account.get('id'))); } }, + + onOpenUnauthorizedModal(account) { + dispatch(openModal('UNAUTHORIZED', { + action: 'FOLLOW', + account: account.get('id'), + ap_id: account.get('url'), + })); + }, }); export default @connect(mapStateToProps, mapDispatchToProps) @@ -57,8 +70,10 @@ class ActionButton extends ImmutablePureComponent { account: ImmutablePropTypes.map.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, + onOpenUnauthorizedModal: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, small: PropTypes.bool, + features: PropTypes.object.isRequired, }; static defaultProps = { @@ -81,12 +96,26 @@ class ActionButton extends ImmutablePureComponent { this.props.onBlock(this.props.account); } + handleRemoteFollow = () => { + this.props.onOpenUnauthorizedModal(this.props.account); + } + render() { - const { account, intl, me, small } = this.props; + const { account, intl, me, small, features } = this.props; const empty = <>; if (!me) { // Remote follow + if (features.remoteInteractionsAPI) { + return (); + } + return (
diff --git a/app/soapbox/features/ui/components/unauthorized_modal.js b/app/soapbox/features/ui/components/unauthorized_modal.js index 0465a9af6..0e1429d8d 100644 --- a/app/soapbox/features/ui/components/unauthorized_modal.js +++ b/app/soapbox/features/ui/components/unauthorized_modal.js @@ -5,32 +5,150 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; import IconButton from 'soapbox/components/icon_button'; +import snackbar from 'soapbox/actions/snackbar'; +import { remoteInteraction } from 'soapbox/actions/interactions'; +import { getFeatures } from 'soapbox/utils/features'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, + accountPlaceholder: { id: 'remote_interaction.account_placeholder', defaultMessage: 'Enter your username@domain you want to act from' }, + userNotFoundError: { id: 'remote_interaction.user_not_found_error', defaultMessage: 'Couldn\'t find given user' }, }); -const mapStateToProps = state => { - const me = state.get('me'); +const mapStateToProps = (state, props) => { + const instance = state.get('instance'); + const features = getFeatures(instance); + + if (props.action !== 'FOLLOW') { + return { + features, + siteTitle: state.getIn(['instance', 'title']), + remoteInteractionsAPI: features.remoteInteractionsAPI, + }; + } + + const userName = state.getIn(['accounts', props.account, 'display_name']); + return { - account: state.getIn(['accounts', me]), + features, siteTitle: state.getIn(['instance', 'title']), + userName, + remoteInteractionsAPI: features.remoteInteractionsAPI, }; }; +const mapDispatchToProps = dispatch => ({ + dispatch, + onRemoteInteraction(ap_id, account) { + return dispatch(remoteInteraction(ap_id, account)); + }, +}); + class UnauthorizedModal extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, + features: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, + onRemoteInteraction: PropTypes.func.isRequired, + userName: PropTypes.string, + }; + + state = { + account: '', }; + onAccountChange = e => { + this.setState({ account: e.target.value }); + } + onClickClose = () => { this.props.onClose('UNAUTHORIZED'); }; + onClickProceed = e => { + e.preventDefault(); + + const { intl, ap_id, dispatch, onClose, onRemoteInteraction } = this.props; + const { account } = this.state; + + onRemoteInteraction(ap_id, account) + .then(url => { + window.open(url, '_new', 'noopener,noreferrer'); + onClose('UNAUTHORIZED'); + }) + .catch(error => { + if (error.message === 'Couldn\'t find user') { + dispatch(snackbar.error(intl.formatMessage(messages.userNotFoundError))); + } + }); + } + + renderRemoteInteractions() { + const { intl, siteTitle, userName, action } = this.props; + const { account } = this.state; + + let header; + let button; + + if (action === 'FOLLOW') { + header = ; + button = ; + } else if (action === 'REPLY') { + header = ; + button = ; + } else if (action === 'REBLOG') { + header = ; + button = ; + } else if (action === 'FAVOURITE') { + header = ; + button = ; + } else if (action === 'POLL_VOTE') { + header = ; + button = ; + } + + return ( +
+
+

{header}

+ +
+
+ + + + +
+ + + +
+

+ + + + + + +
+
+ ); + } + render() { - const { intl, siteTitle } = this.props; + const { intl, features, siteTitle } = this.props; + + if (features.remoteInteractionsAPI && features.federating) return this.renderRemoteInteractions(); return (
@@ -61,4 +179,4 @@ class UnauthorizedModal extends ImmutablePureComponent { } -export default injectIntl(connect(mapStateToProps)(UnauthorizedModal)); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(UnauthorizedModal)); diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index a907d1219..5837f9e0b 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -740,6 +740,18 @@ "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} nakłada pewne ograniczenia na {host}.", "remote_instance.pin_host": "Przypnij {instance}", "remote_instance.unpin_host": "Odepnij {instance}", + "remote_interaction.account_placeholder": "Wprowadź nazwę@domenę użytkownika, z którego chcesz wykonać działanie", + "remote_interaction.favourite": "Przejdź do polubienia", + "remote_interaction.favourite_title": "Polub wpis zdalnie", + "remote_interaction.follow": "Przejdź do obserwacji", + "remote_interaction.follow_title": "Obserwuj {user} zdalnie", + "remote_interaction.poll_vote": "Przejdź do ankiety", + "remote_interaction.poll_vote_title": "Zagłosuj w ankiecie zdalnie", + "remote_interaction.reblog": "Przejdź do wpisu", + "remote_interaction.reblog_title": "Udostępnij wpis zdalnie", + "remote_interaction.reply": "Przejdź do odpowiedzi", + "remote_interaction.reply_title": "Odpowiedz na wpis zdalnie", + "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", "report.block": "Zablokuj {target}", diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index 6447f53f7..124df7d2d 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -74,6 +74,7 @@ export const getFeatures = createSelector([ v.software === MASTODON && gte(v.compatVersion, '3.4.0'), v.software === PLEROMA && gte(v.version, '2.4.50'), ]), + remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'), }; }); diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 62d44f603..9d46eeb49 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -860,6 +860,59 @@ } } +.remote-interaction-modal { + &__content { + display: flex; + flex-direction: column; + // align-items: center; + row-gap: 10px; + padding: 10px; + + .unauthorized-modal-content__button { + margin: 0 auto; + } + } + + &__fields { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + + .button { + width: auto; + margin: 0; + text-transform: none; + overflow: unset; + } + } + + &__divider { + display: flex; + align-items: center; + gap: 10px; + margin: 0 -10px; + + &::before, + &::after { + content: ""; + flex: 1; + border-bottom: 1px solid hsla(var(--primary-text-color_hsl), 0.2); + } + } + + @media screen and (max-width: 895px) { + margin: 0; + border-radius: 6px; + height: unset !important; + width: 440px !important; + } + + @media screen and (max-width: 480px) { + width: 330px !important; + } +} + .focal-point-modal { max-width: 80vw; max-height: 80vh;