mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Add support for FEP-3b86 (Activity Intents) (#38120)
This commit is contained in:
@@ -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<unknown>) => {
|
||||
'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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -92,6 +92,7 @@ export const FollowButton: React.FC<{
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'follow',
|
||||
accountId: accountId,
|
||||
url: account?.url,
|
||||
},
|
||||
|
||||
@@ -110,6 +110,7 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'vote',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'reblog',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
@@ -120,6 +121,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'reblog',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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<{
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LoginForm resourceUrl={url} />
|
||||
<LoginForm resourceUrl={url} intent={intent} />
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -86,6 +86,7 @@ export const Footer: React.FC<{
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'reply',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
@@ -106,6 +107,7 @@ export const Footer: React.FC<{
|
||||
openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'favourite',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
|
||||
@@ -190,6 +190,7 @@ class Status extends ImmutablePureComponent {
|
||||
dispatch(openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'favourite',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
@@ -219,6 +220,7 @@ class Status extends ImmutablePureComponent {
|
||||
dispatch(openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'reply',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
@@ -236,6 +238,7 @@ class Status extends ImmutablePureComponent {
|
||||
dispatch(openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
intent: 'reblog',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
@@ -274,13 +277,13 @@ class Status extends ImmutablePureComponent {
|
||||
// Error handling - could show error message
|
||||
});
|
||||
} else {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM_DELETE_STATUS',
|
||||
modalProps: {
|
||||
statusId: status.get('id'),
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM_DELETE_STATUS',
|
||||
modalProps: {
|
||||
statusId: status.get('id'),
|
||||
withRedraft,
|
||||
onDeleteSuccess: handleDeleteSuccess
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -498,7 +501,7 @@ class Status extends ImmutablePureComponent {
|
||||
// Only highlight replies after the initial load
|
||||
if (prevProps.descendantsIds.length && isSameStatus) {
|
||||
const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds);
|
||||
|
||||
|
||||
if (newRepliesIds.length) {
|
||||
this.setState({newRepliesIds});
|
||||
}
|
||||
@@ -631,7 +634,7 @@ class Status extends ImmutablePureComponent {
|
||||
</Hotkeys>
|
||||
|
||||
{descendants}
|
||||
|
||||
|
||||
<RefreshController
|
||||
isLocal={isLocal}
|
||||
statusId={status.get('id')}
|
||||
|
||||
@@ -22,6 +22,8 @@ class WebfingerSerializer < ActiveModel::Serializer
|
||||
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profile_page_href },
|
||||
{ rel: 'self', type: 'application/activity+json', href: self_href },
|
||||
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
|
||||
{ rel: 'https://w3id.org/fep/3b86/Create', template: "#{share_url}?text={content}" },
|
||||
{ rel: 'https://w3id.org/fep/3b86/Object', template: "#{authorize_interaction_url}?uri={object}" },
|
||||
].tap do |x|
|
||||
x << { rel: 'http://webfinger.net/rel/avatar', type: object.avatar.content_type, href: full_asset_url(object.avatar_original_url) } if show_avatar?
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user