From 4150cee7a5c027d41540d99631886ef762341999 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Tue, 10 Mar 2026 11:21:24 +0100
Subject: [PATCH] [Glitch] Add support for FEP-3b86 (Activity Intents)
Port 69b1f60f4e46cf58e7240c2bfb81588accc1af6f to glitch-soc
Signed-off-by: Claire
---
.../glitch/components/follow_button.tsx | 1 +
.../flavours/glitch/components/poll.tsx | 1 +
.../glitch/components/status/boost_button.tsx | 2 +
.../components/status_action_bar/index.jsx | 4 +-
.../glitch/containers/status_container.js | 5 +-
.../entrypoints/remote_interaction_helper.ts | 93 +++++++++++++++----
.../features/interaction_modal/index.tsx | 32 +++++--
.../picture_in_picture/components/footer.tsx | 2 +
.../flavours/glitch/features/status/index.jsx | 17 ++--
9 files changed, 116 insertions(+), 41 deletions(-)
diff --git a/app/javascript/flavours/glitch/components/follow_button.tsx b/app/javascript/flavours/glitch/components/follow_button.tsx
index 940f6b0f58..3f40997cd9 100644
--- a/app/javascript/flavours/glitch/components/follow_button.tsx
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/poll.tsx b/app/javascript/flavours/glitch/components/poll.tsx
index a1e08144d1..16ec33e523 100644
--- a/app/javascript/flavours/glitch/components/poll.tsx
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/status/boost_button.tsx b/app/javascript/flavours/glitch/components/status/boost_button.tsx
index cee670833d..0021dbdce5 100644
--- a/app/javascript/flavours/glitch/components/status/boost_button.tsx
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/status_action_bar/index.jsx b/app/javascript/flavours/glitch/components/status_action_bar/index.jsx
index 7f24178a05..7f402eb645 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar/index.jsx
+++ b/app/javascript/flavours/glitch/components/status_action_bar/index.jsx
@@ -121,7 +121,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');
}
};
@@ -143,7 +143,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) {
this.props.onFavourite(this.props.status, e);
} else {
- this.props.onInteractionModal(this.props.status);
+ this.props.onInteractionModal(this.props.status, 'favourite');
}
};
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 10b5d2dc82..58ceb772da 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -82,7 +82,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
}
});
},
-
+
onQuote (status) {
dispatch(quoteComposeById(status.get('id')));
},
@@ -229,10 +229,11 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
});
},
- 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/flavours/glitch/entrypoints/remote_interaction_helper.ts b/app/javascript/flavours/glitch/entrypoints/remote_interaction_helper.ts
index f50203747d..26f9e1f4e0 100644
--- a/app/javascript/flavours/glitch/entrypoints/remote_interaction_helper.ts
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/interaction_modal/index.tsx b/app/javascript/flavours/glitch/features/interaction_modal/index.tsx
index 7059c76897..f112f25063 100644
--- a/app/javascript/flavours/glitch/features/interaction_modal/index.tsx
+++ b/app/javascript/flavours/glitch/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}
-
+