diff --git a/app/javascript/entrypoints/remote_interaction_helper.ts b/app/javascript/entrypoints/remote_interaction_helper.ts index f50203747d..26f9e1f4e0 100644 --- a/app/javascript/entrypoints/remote_interaction_helper.ts +++ b/app/javascript/entrypoints/remote_interaction_helper.ts @@ -39,18 +39,57 @@ const findLink = (rel: string, data: unknown): JRDLink | undefined => { } }; -const findTemplateLink = (data: unknown) => - findLink('http://ostatus.org/schema/1.0/subscribe', data)?.template; +const intentParams = (intent: string) => { + switch (intent) { + case 'follow': + return ['https://w3id.org/fep/3b86/Follow', 'object'] as [string, string]; + case 'reblog': + return ['https://w3id.org/fep/3b86/Announce', 'object'] as [ + string, + string, + ]; + case 'favourite': + return ['https://w3id.org/fep/3b86/Like', 'object'] as [string, string]; + case 'vote': + case 'reply': + return ['https://w3id.org/fep/3b86/Object', 'object'] as [string, string]; + default: + return null; + } +}; + +const findTemplateLink = (data: unknown, intent: string) => { + const [needle, param] = intentParams(intent) ?? [ + 'http://ostatus.org/schema/1.0/subscribe', + 'uri', + ]; + + const match = findLink(needle, data); + + if (match) { + return [match.template, param] as [string, string]; + } + + const fallback = findLink('http://ostatus.org/schema/1.0/subscribe', data); + + if (fallback) { + return [fallback.template, 'uri'] as [string, string]; + } + + return [null, null]; +}; const fetchInteractionURLSuccess = ( uri_or_domain: string, template: string, + param: string, ) => { window.parent.postMessage( { type: 'fetchInteractionURL-success', uri_or_domain, template, + param, }, window.origin, ); @@ -74,7 +113,7 @@ const isValidDomain = (value: unknown) => { }; // Attempt to find a remote interaction URL from a domain -const fromDomain = (domain: string) => { +const fromDomain = (domain: string, intent: string) => { const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`; axios @@ -82,17 +121,21 @@ const fromDomain = (domain: string) => { params: { resource: `https://${domain}` }, }) .then(({ data }) => { - const template = findTemplateLink(data); - fetchInteractionURLSuccess(domain, template ?? fallbackTemplate); + const [template, param] = findTemplateLink(data, intent); + fetchInteractionURLSuccess( + domain, + template ?? fallbackTemplate, + param ?? 'uri', + ); return; }) .catch(() => { - fetchInteractionURLSuccess(domain, fallbackTemplate); + fetchInteractionURLSuccess(domain, fallbackTemplate, 'uri'); }); }; // Attempt to find a remote interaction URL from an arbitrary URL -const fromURL = (url: string) => { +const fromURL = (url: string, intent: string) => { const domain = new URL(url).host; const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`; @@ -101,17 +144,21 @@ const fromURL = (url: string) => { params: { resource: url }, }) .then(({ data }) => { - const template = findTemplateLink(data); - fetchInteractionURLSuccess(url, template ?? fallbackTemplate); + const [template, param] = findTemplateLink(data, intent); + fetchInteractionURLSuccess( + url, + template ?? fallbackTemplate, + param ?? 'uri', + ); return; }) .catch(() => { - fromDomain(domain); + fromDomain(domain, intent); }); }; // Attempt to find a remote interaction URL from a `user@domain` string -const fromAcct = (acct: string) => { +const fromAcct = (acct: string, intent: string) => { acct = acct.replace(/^@/, ''); const segments = acct.split('@'); @@ -134,25 +181,29 @@ const fromAcct = (acct: string) => { params: { resource: `acct:${acct}` }, }) .then(({ data }) => { - const template = findTemplateLink(data); - fetchInteractionURLSuccess(acct, template ?? fallbackTemplate); + const [template, param] = findTemplateLink(data, intent); + fetchInteractionURLSuccess( + acct, + template ?? fallbackTemplate, + param ?? 'uri', + ); return; }) .catch(() => { // TODO: handle host-meta? - fromDomain(domain); + fromDomain(domain, intent); }); }; -const fetchInteractionURL = (uri_or_domain: string) => { +const fetchInteractionURL = (uri_or_domain: string, intent: string) => { if (uri_or_domain === '') { fetchInteractionURLFailure(); } else if (/^https?:\/\//.test(uri_or_domain)) { - fromURL(uri_or_domain); + fromURL(uri_or_domain, intent); } else if (uri_or_domain.includes('@')) { - fromAcct(uri_or_domain); + fromAcct(uri_or_domain, intent); } else { - fromDomain(uri_or_domain); + fromDomain(uri_or_domain, intent); } }; @@ -172,8 +223,10 @@ window.addEventListener('message', (event: MessageEvent) => { 'type' in event.data && event.data.type === 'fetchInteractionURL' && 'uri_or_domain' in event.data && - typeof event.data.uri_or_domain === 'string' + typeof event.data.uri_or_domain === 'string' && + 'intent' in event.data && + typeof event.data.intent === 'string' ) { - fetchInteractionURL(event.data.uri_or_domain); + fetchInteractionURL(event.data.uri_or_domain, event.data.intent); } }); diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index e715de51c8..a682dd9552 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -92,6 +92,7 @@ export const FollowButton: React.FC<{ openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'follow', accountId: accountId, url: account?.url, }, diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index 2b7134185e..b5b5fb3673 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -110,6 +110,7 @@ export const Poll: React.FC = ({ pollId, disabled, status }) => { openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'vote', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, diff --git a/app/javascript/mastodon/components/status/boost_button.tsx b/app/javascript/mastodon/components/status/boost_button.tsx index 023ba8ff19..95d799f81b 100644 --- a/app/javascript/mastodon/components/status/boost_button.tsx +++ b/app/javascript/mastodon/components/status/boost_button.tsx @@ -47,6 +47,7 @@ const StandaloneBoostButton: FC = ({ status, counters }) => { openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'reblog', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, @@ -120,6 +121,7 @@ const BoostOrQuoteMenu: FC = ({ status, counters }) => { openModal({ modalType: 'INTERACTION', modalProps: { + intent: 'reblog', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, diff --git a/app/javascript/mastodon/components/status_action_bar/index.jsx b/app/javascript/mastodon/components/status_action_bar/index.jsx index 5c79970e23..2e0440d012 100644 --- a/app/javascript/mastodon/components/status_action_bar/index.jsx +++ b/app/javascript/mastodon/components/status_action_bar/index.jsx @@ -124,7 +124,7 @@ class StatusActionBar extends ImmutablePureComponent { if (signedIn) { this.props.onReply(this.props.status); } else { - this.props.onInteractionModal(this.props.status); + this.props.onInteractionModal(this.props.status, 'reply'); } }; @@ -146,7 +146,7 @@ class StatusActionBar extends ImmutablePureComponent { if (signedIn) { this.props.onFavourite(this.props.status); } else { - this.props.onInteractionModal(this.props.status); + this.props.onInteractionModal(this.props.status, 'favourite'); } }; @@ -382,7 +382,7 @@ class StatusActionBar extends ImmutablePureComponent { const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark); const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite); const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']); - + const shouldShowQuoteRemovalHint = isQuotingMe && contextType === 'notifications'; return ( diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index baf4157f96..bf49bf3f55 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -77,7 +77,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ onReblog (status, e) { dispatch(toggleReblog(status.get('id'), e.shiftKey)); }, - + onQuote (status) { dispatch(quoteComposeById(status.get('id'))); }, @@ -231,10 +231,11 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps})); }, - onInteractionModal (status) { + onInteractionModal (status, intent) { dispatch(openModal({ modalType: 'INTERACTION', modalProps: { + intent, accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, diff --git a/app/javascript/mastodon/features/interaction_modal/index.tsx b/app/javascript/mastodon/features/interaction_modal/index.tsx index 03cfc2c484..624ecd5613 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.tsx +++ b/app/javascript/mastodon/features/interaction_modal/index.tsx @@ -25,6 +25,8 @@ const messages = defineMessages({ }, }); +type InteractionIntent = 'follow' | 'reblog' | 'favourite' | 'reply' | 'vote'; + interface LoginFormMessage { type: | 'fetchInteractionURL' @@ -32,6 +34,8 @@ interface LoginFormMessage { | 'fetchInteractionURL-success'; uri_or_domain: string; template?: string; + param?: string; + intent?: InteractionIntent; } const PERSISTENCE_KEY = 'mastodon_home'; @@ -110,7 +114,11 @@ const isValueValid = (value: string) => { } }; -const sendToFrame = (frame: HTMLIFrameElement | null, value: string): void => { +const sendToFrame = ( + frame: HTMLIFrameElement | null, + value: string, + intent: string, +): void => { if (valueToDomain(value.trim()) === localDomain) { window.location.href = '/auth/sign_in'; return; @@ -120,6 +128,7 @@ const sendToFrame = (frame: HTMLIFrameElement | null, value: string): void => { { type: 'fetchInteractionURL', uri_or_domain: value.trim(), + intent, }, window.origin, ); @@ -127,7 +136,8 @@ const sendToFrame = (frame: HTMLIFrameElement | null, value: string): void => { const LoginForm: React.FC<{ resourceUrl: string; -}> = ({ resourceUrl }) => { + intent: string; +}> = ({ resourceUrl, intent }) => { const intl = useIntl(); const [value, setValue] = useState( localStorage.getItem(PERSISTENCE_KEY) ?? '', @@ -161,7 +171,7 @@ const LoginForm: React.FC<{ try { const url = new URL( event.data.template.replace( - '{uri}', + `{${event.data.param}}`, encodeURIComponent(resourceUrl), ), ); @@ -242,8 +252,8 @@ const LoginForm: React.FC<{ const handleSubmit = useCallback(() => { setIsSubmitting(true); - sendToFrame(iframeRef.current, value); - }, [setIsSubmitting, value]); + sendToFrame(iframeRef.current, value, intent); + }, [setIsSubmitting, value, intent]); const handleFocus = useCallback(() => { setExpanded(true); @@ -287,7 +297,7 @@ const LoginForm: React.FC<{ setError(false); setValue(selectedOptionValue); setIsSubmitting(true); - sendToFrame(iframeRef.current, selectedOptionValue); + sendToFrame(iframeRef.current, selectedOptionValue, intent); } break; @@ -300,6 +310,7 @@ const LoginForm: React.FC<{ setValue, selectedOption, options, + intent, ], ); @@ -318,9 +329,9 @@ const LoginForm: React.FC<{ setValue(option); setError(false); setIsSubmitting(true); - sendToFrame(iframeRef.current, option); + sendToFrame(iframeRef.current, option, intent); }, - [options, setSelectedOption, setValue, setError], + [options, setSelectedOption, setValue, setError, intent], ); const domain = (valueToDomain(value) ?? '').trim(); @@ -404,7 +415,8 @@ const LoginForm: React.FC<{ const InteractionModal: React.FC<{ accountId: string; url: string; -}> = ({ accountId, url }) => { + intent: string; +}> = ({ accountId, url, intent }) => { const dispatch = useAppDispatch(); const signupUrl = useAppSelector( (state) => @@ -479,7 +491,7 @@ const InteractionModal: React.FC<{

- +

{descendants} - +