Remote interactions modal

This commit is contained in:
marcin mikołajczak 2022-01-02 20:43:53 +00:00
parent 18e9da0a1d
commit 9c6f805499
11 changed files with 312 additions and 40 deletions

View file

@ -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,
};
}

View file

@ -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 = () => {

View file

@ -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'),
}));
},
});

View file

@ -242,7 +242,7 @@ class StatusContent extends React.PureComponent {
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} status={status.get('url')} />}
</div>
);
} else if (this.props.onClick) {
@ -265,7 +265,7 @@ class StatusContent extends React.PureComponent {
}
if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
output.push(<PollContainer pollId={status.get('poll')} key='poll' status={status.get('url')} />);
}
return output;
@ -285,7 +285,7 @@ class StatusContent extends React.PureComponent {
];
if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
output.push(<PollContainer pollId={status.get('poll')} key='poll' status={status.get('url')} />);
}
return output;

View file

@ -6,4 +6,5 @@ const mapStateToProps = (state, { pollId }) => ({
me: state.get('me'),
});
export default connect(mapStateToProps)(Poll);

View file

@ -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 });
};

View file

@ -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 (<Button
className='button--follow'
onClick={this.handleRemoteFollow}
>
{intl.formatMessage(messages.follow)}
<Icon src={require('@tabler/icons/icons/plus.svg')} />
</Button>);
}
return (<form method='POST' action='/main/ostatus'>
<input type='hidden' name='nickname' value={account.get('acct')} />
<input type='hidden' name='profile' value='' />

View file

@ -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 = <FormattedMessage id='remote_interaction.follow_title' defaultMessage='Follow {user} remotely' values={{ user: userName }} />;
button = <FormattedMessage id='remote_interaction.follow' defaultMessage='Proceed to follow' />;
} else if (action === 'REPLY') {
header = <FormattedMessage id='remote_interaction.reply_title' defaultMessage='Reply to a post remotely' />;
button = <FormattedMessage id='remote_interaction.reply' defaultMessage='Proceed to reply' />;
} else if (action === 'REBLOG') {
header = <FormattedMessage id='remote_interaction.reblog_title' defaultMessage='Reblog a post remotely' />;
button = <FormattedMessage id='remote_interaction.reblog' defaultMessage='Proceed to repost' />;
} else if (action === 'FAVOURITE') {
header = <FormattedMessage id='remote_interaction.favourite_title' defaultMessage='Like a post remotely' />;
button = <FormattedMessage id='remote_interaction.favourite' defaultMessage='Proceed to like' />;
} else if (action === 'POLL_VOTE') {
header = <FormattedMessage id='remote_interaction.poll_vote_title' defaultMessage='Vote in a poll remotely' />;
button = <FormattedMessage id='remote_interaction.poll_vote' defaultMessage='Proceed to vote' />;
}
return (
<div className='modal-root__modal compose-modal unauthorized-modal remote-interaction-modal'>
<div className='compose-modal__header'>
<h3 className='compose-modal__header__title'>{header}</h3>
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} src={require('@tabler/icons/icons/x.svg')} onClick={this.onClickClose} />
</div>
<div className='remote-interaction-modal__content'>
<form className='simple_form remote-interaction-modal__fields'>
<input
type='text'
placeholder={intl.formatMessage(messages.accountPlaceholder)}
name='remote_follow[acct]'
value={account}
autoCorrect='off'
autoCapitalize='off'
onChange={this.onAccountChange}
required
/>
<button className='button' onClick={this.onClickProceed}>{button}</button>
</form>
<div className='remote-interaction-modal__divider'>
<span>
<FormattedMessage id='remote_interaction.divider' defaultMessage='or' />
</span>
</div>
<h3 className='compose-modal__header__title'><FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} /></h3>
<Link to='/' className='unauthorized-modal-content__button button' onClick={this.onClickClose}>
<FormattedMessage id='account.register' defaultMessage='Sign up' />
</Link>
<Link to='/auth/sign_in' className='unauthorized-modal-content__button button button-secondary' onClick={this.onClickClose}>
<FormattedMessage id='account.login' defaultMessage='Log in' />
</Link>
</div>
</div>
);
}
render() {
const { intl, siteTitle } = this.props;
const { intl, features, siteTitle } = this.props;
if (features.remoteInteractionsAPI && features.federating) return this.renderRemoteInteractions();
return (
<div className='modal-root__modal compose-modal unauthorized-modal'>
@ -61,4 +179,4 @@ class UnauthorizedModal extends ImmutablePureComponent {
}
export default injectIntl(connect(mapStateToProps)(UnauthorizedModal));
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(UnauthorizedModal));

View file

@ -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}",

View file

@ -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'),
};
});

View file

@ -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;