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}
-
+