pl-fe: Allow checking whether remote instances support emoji reactions when reacting
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -1,4 +1,9 @@
|
||||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||
import toast from 'pl-fe/toast';
|
||||
import { isLoggedIn } from 'pl-fe/utils/auth';
|
||||
import { supportsEmojiReacts } from 'pl-fe/utils/check-instance-capability';
|
||||
|
||||
import { getClient } from '../api';
|
||||
|
||||
@ -11,9 +16,13 @@ const EMOJI_REACT_FAIL = 'EMOJI_REACT_FAIL' as const;
|
||||
|
||||
const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST' as const;
|
||||
|
||||
const messages = defineMessages({
|
||||
unsupported: { id: 'emoji_reactions.unsupported_by_remote', defaultMessage: '@{acct}’s instance most likely doesn’t understand emoji reactions. The user will not get notified of the reaction.' },
|
||||
});
|
||||
|
||||
const noOp = () => () => new Promise(f => f(undefined));
|
||||
|
||||
const emojiReact = (statusId: string, emoji: string, custom?: string) =>
|
||||
const emojiReact = (statusId: string, emoji: string, custom: string | undefined = undefined, intl: IntlShape) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return dispatch(noOp());
|
||||
|
||||
@ -21,6 +30,16 @@ const emojiReact = (statusId: string, emoji: string, custom?: string) =>
|
||||
|
||||
return getClient(getState).statuses.createStatusReaction(statusId, emoji).then((response) => {
|
||||
dispatch(importEntities({ statuses: [response] }));
|
||||
|
||||
const checkEmojiReactsSupport = !response.account.local && useSettingsStore.getState().settings.checkEmojiReactsSupport;
|
||||
|
||||
if (checkEmojiReactsSupport) {
|
||||
supportsEmojiReacts(response.account.ap_id || response.account.url).then((result) => {
|
||||
if (result === 'false') {
|
||||
toast.info(intl.formatMessage(messages.unsupported, { acct: response.account.acct }));
|
||||
}
|
||||
}).catch((e) => {});
|
||||
}
|
||||
}).catch((error) => {
|
||||
dispatch(emojiReactFail(statusId, emoji, error));
|
||||
});
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { getAuthUserUrl, getMeUrl } from 'pl-fe/utils/auth';
|
||||
|
||||
import { getClient } from '../api';
|
||||
import { getClient, staticFetch } from '../api';
|
||||
|
||||
import type { Instance } from 'pl-api';
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS' as const;
|
||||
const INSTANCE_FETCH_FAIL = 'INSTANCE_FETCH_FAIL' as const;
|
||||
const STANDALONE_CHECK_SUCCESS = 'STANDALONE_CHECK_SUCCESS' as const;
|
||||
|
||||
/** Figure out the appropriate instance to fetch depending on the state */
|
||||
const getHost = (state: RootState) => {
|
||||
@ -39,14 +40,27 @@ const fetchInstance = () => async (dispatch: AppDispatch, getState: () => RootSt
|
||||
}
|
||||
};
|
||||
|
||||
interface StandaloneCheckSuccessAction {
|
||||
type: typeof STANDALONE_CHECK_SUCCESS;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
const checkIfStandalone = () => (dispatch: AppDispatch) =>
|
||||
staticFetch('/api/v1/instance')
|
||||
.then(({ ok }) => dispatch<StandaloneCheckSuccessAction>({ type: STANDALONE_CHECK_SUCCESS, ok }))
|
||||
.catch((err) => dispatch<StandaloneCheckSuccessAction>({ type: STANDALONE_CHECK_SUCCESS, ok: err.response?.ok }));
|
||||
|
||||
type InstanceAction =
|
||||
InstanceFetchSuccessAction
|
||||
| InstanceFetchFailAction
|
||||
| StandaloneCheckSuccessAction
|
||||
|
||||
export {
|
||||
INSTANCE_FETCH_SUCCESS,
|
||||
INSTANCE_FETCH_FAIL,
|
||||
STANDALONE_CHECK_SUCCESS,
|
||||
getHost,
|
||||
fetchInstance,
|
||||
checkIfStandalone,
|
||||
type InstanceAction,
|
||||
};
|
||||
|
||||
@ -490,13 +490,13 @@ const WrenchButton: React.FC<IActionButton> = ({
|
||||
if (wrenches?.me) {
|
||||
dispatch(unEmojiReact(status.id, '🔧'));
|
||||
} else {
|
||||
dispatch(emojiReact(status.id, '🔧'));
|
||||
dispatch(emojiReact(status.id, '🔧', undefined, intl));
|
||||
}
|
||||
};
|
||||
|
||||
const handleWrenchLongPress = () => {
|
||||
if (features.customEmojiReacts && hasLongerWrench) {
|
||||
dispatch(emojiReact(status.id, hasLongerWrench.shortcode, hasLongerWrench.url));
|
||||
dispatch(emojiReact(status.id, hasLongerWrench.shortcode, hasLongerWrench.url, intl));
|
||||
} else if (wrenches?.count) {
|
||||
openModal('REACTIONS', { statusId: status.id, reaction: wrenches.name });
|
||||
}
|
||||
@ -524,11 +524,12 @@ const EmojiPickerButton: React.FC<Omit<IActionButton, 'onOpenUnauthorizedModal'>
|
||||
me,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const features = useFeatures();
|
||||
|
||||
const handlePickEmoji = (emoji: EmojiType) => {
|
||||
dispatch(emojiReact(status.id, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
|
||||
dispatch(emojiReact(status.id, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined, intl));
|
||||
};
|
||||
|
||||
return me && !withLabels && features.emojiReacts && (
|
||||
|
||||
@ -65,7 +65,7 @@ const StatusReaction: React.FC<IStatusReaction> = ({ reaction, statusId, obfusca
|
||||
} else if (reaction.me) {
|
||||
dispatch(unEmojiReact(statusId, reaction.name));
|
||||
} else {
|
||||
dispatch(emojiReact(statusId, reaction.name, reaction.url));
|
||||
dispatch(emojiReact(statusId, reaction.name, reaction.url, intl));
|
||||
}
|
||||
};
|
||||
|
||||
@ -112,7 +112,7 @@ const StatusReactionsBar: React.FC<IStatusReactionsBar> = ({ status, collapsed }
|
||||
const features = useFeatures();
|
||||
|
||||
const handlePickEmoji = (emoji: EmojiType) => {
|
||||
dispatch(emojiReact(status.id, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
|
||||
dispatch(emojiReact(status.id, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined, intl));
|
||||
};
|
||||
|
||||
if ((demetricator || status.emoji_reactions.length === 0) && collapsed) return null;
|
||||
|
||||
@ -10,11 +10,13 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||
import { Mutliselect, SelectDropdown } from 'pl-fe/features/forms';
|
||||
import SettingToggle from 'pl-fe/features/settings/components/setting-toggle';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useInstance } from 'pl-fe/hooks/use-instance';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
import colors from 'pl-fe/utils/colors';
|
||||
import { isStandalone } from 'pl-fe/utils/state';
|
||||
|
||||
import { PaletteListItem } from '../theme-editor';
|
||||
import ThemeToggle from '../ui/components/theme-toggle';
|
||||
@ -114,6 +116,7 @@ const Preferences = () => {
|
||||
const settings = useSettings();
|
||||
const plFeConfig = usePlFeConfig();
|
||||
const instance = useInstance();
|
||||
const standalone = useAppSelector(isStandalone);
|
||||
|
||||
const brandColor = settings.theme?.brandColor || plFeConfig.brandColor || '#d80482';
|
||||
|
||||
@ -270,6 +273,15 @@ const Preferences = () => {
|
||||
<SettingToggle settings={settings} settingPath={['showWrenchButton']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{features.emojiReacts && standalone && (
|
||||
<ListItem
|
||||
label={<FormattedMessage id='preferences.fields.check_emoji_react_supports_label' defaultMessage='Check whether remote hosts support emoji reactions when reacting' />}
|
||||
hint={<FormattedMessage id='preferences.fields.check_emoji_react_supports_hint' defaultMessage='This will expose your IP address to the instances you’re interacting with.' />}
|
||||
>
|
||||
<SettingToggle settings={settings} settingPath={['checkEmojiReactsSupport']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<List>
|
||||
|
||||
@ -174,7 +174,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = React.memo(({ chil
|
||||
// Ex: use /login instead of /auth, but redirect /auth to /login
|
||||
return (
|
||||
<Switch>
|
||||
{standalone && <Redirect from='/' to='/login/external' exact />}
|
||||
{standalone && !isLoggedIn && <Redirect from='/' to='/login/external' exact />}
|
||||
|
||||
<WrappedRoute path='/logout' layout={EmptyLayout} component={LogoutPage} publicRoute exact />
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { fetchInstance } from 'pl-fe/actions/instance';
|
||||
import { checkIfStandalone, fetchInstance } from 'pl-fe/actions/instance';
|
||||
import { fetchMe } from 'pl-fe/actions/me';
|
||||
import { loadPlFeConfig } from 'pl-fe/actions/pl-fe';
|
||||
import LoadingScreen from 'pl-fe/components/loading-screen';
|
||||
@ -11,17 +11,17 @@ import { useLocale } from 'pl-fe/hooks/use-locale';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import MESSAGES from 'pl-fe/messages';
|
||||
|
||||
import type { AppDispatch } from 'pl-fe/store';
|
||||
|
||||
/** Load initial data from the backend */
|
||||
const loadInitial = () => {
|
||||
// @ts-ignore
|
||||
return async(dispatch, getState) => {
|
||||
// Await for authenticated fetch
|
||||
await dispatch(fetchMe());
|
||||
// Await for feature detection
|
||||
await dispatch(fetchInstance());
|
||||
// Await for configuration
|
||||
await dispatch(loadPlFeConfig());
|
||||
};
|
||||
const loadInitial = () => async(dispatch: AppDispatch) => {
|
||||
dispatch(checkIfStandalone());
|
||||
// Await for authenticated fetch
|
||||
await dispatch(fetchMe());
|
||||
// Await for feature detection
|
||||
await dispatch(fetchInstance());
|
||||
// Await for configuration
|
||||
await dispatch(loadPlFeConfig());
|
||||
};
|
||||
|
||||
interface IPlFeLoad {
|
||||
|
||||
@ -710,6 +710,7 @@
|
||||
"emoji_button.symbols": "Symbols",
|
||||
"emoji_button.travel": "Travel & Places",
|
||||
"emoji_button.uncategorized": "Uncategorized",
|
||||
"emoji_reactions.unsupported_by_remote": "@{acct}’s instance most likely doesn’t understand emoji reactions. The user will not get notified of the reaction.",
|
||||
"empty_column.account_blocked": "You are blocked by @{accountUsername}.",
|
||||
"empty_column.account_favourited_statuses": "This user doesn't have any liked posts yet.",
|
||||
"empty_column.account_timeline": "No posts here!",
|
||||
@ -1275,6 +1276,8 @@
|
||||
"preferences.fields.autoload_more_label": "Automatically load more items when scrolled to the bottom of the page",
|
||||
"preferences.fields.autoload_timelines_label": "Automatically load new posts when scrolled to the top of the page",
|
||||
"preferences.fields.boost_modal_label": "Show confirmation dialog before reposting",
|
||||
"preferences.fields.check_emoji_react_supports_hint": "This will expose your IP address to the instances you’re interacting with.",
|
||||
"preferences.fields.check_emoji_react_supports_label": "Check whether remote hosts support emoji reactions when reacting",
|
||||
"preferences.fields.content_type_label": "Default post format",
|
||||
"preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post",
|
||||
"preferences.fields.demetricator_label": "Hide social media counters",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { INSTANCE_FETCH_FAIL, type InstanceAction } from 'pl-fe/actions/instance';
|
||||
import { STANDALONE_CHECK_SUCCESS, type InstanceAction } from 'pl-fe/actions/instance';
|
||||
|
||||
const initialState = {
|
||||
/** Whether /api/v1/instance 404'd (and we should display the external auth form). */
|
||||
@ -7,11 +7,8 @@ const initialState = {
|
||||
|
||||
const meta = (state = initialState, action: InstanceAction): typeof initialState => {
|
||||
switch (action.type) {
|
||||
case INSTANCE_FETCH_FAIL:
|
||||
if ((action.error as any)?.response?.status === 404) {
|
||||
return { instance_fetch_failed: true };
|
||||
}
|
||||
return state;
|
||||
case STANDALONE_CHECK_SUCCESS:
|
||||
return { instance_fetch_failed: !action.ok };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ const settingsSchema = v.object({
|
||||
redirectServicesUrl: v.fallback(v.string(), ''),
|
||||
redirectServices: v.fallback(v.record(v.string(), v.string()), {}),
|
||||
}),
|
||||
checkEmojiReactsSupport: v.fallback(v.boolean(), false),
|
||||
|
||||
theme: v.fallback(v.optional(v.object({
|
||||
brandColor: v.fallback(v.string(), ''),
|
||||
@ -99,6 +100,7 @@ const settingsSchema = v.object({
|
||||
saved: v.fallback(v.boolean(), true),
|
||||
|
||||
demo: v.fallback(v.boolean(), false),
|
||||
|
||||
});
|
||||
|
||||
type Settings = v.InferOutput<typeof settingsSchema>;
|
||||
|
||||
83
packages/pl-fe/src/utils/check-instance-capability.ts
Normal file
83
packages/pl-fe/src/utils/check-instance-capability.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import KVStore from 'pl-fe/storage/kv-store';
|
||||
|
||||
type DomainCapabilities = {
|
||||
lastChecked: number;
|
||||
} & ({
|
||||
failed: true;
|
||||
} | {
|
||||
failed: false;
|
||||
emojiReacts: boolean;
|
||||
});
|
||||
|
||||
interface Capabilities {
|
||||
_version: number;
|
||||
domains: Record<string, DomainCapabilities>;
|
||||
}
|
||||
|
||||
let capabilities: Capabilities = {
|
||||
_version: 1,
|
||||
domains: {},
|
||||
};
|
||||
|
||||
const getCapabilitiesFromMemory = () =>
|
||||
KVStore.getItem<Capabilities>('instanceCapabilities').then((capabilitiesFromMemory) => {
|
||||
if (capabilitiesFromMemory) {
|
||||
capabilities = capabilitiesFromMemory;
|
||||
}
|
||||
}).catch(() => { });
|
||||
|
||||
const checkEmojiReactsSupport = (instance: Record<string, any>) => instance.configuration?.reactions?.max_reactions > 0 || instance.pleroma?.metadata?.features?.includes('pleroma_emoji_reactions');
|
||||
|
||||
const setDomainCapabilities = async (domain: string, domainCapabilities: DomainCapabilities) => {
|
||||
await getCapabilitiesFromMemory();
|
||||
capabilities.domains[domain] = domainCapabilities;
|
||||
await KVStore.setItem('instanceCapabilities', capabilities);
|
||||
};
|
||||
|
||||
const fetchCapabilities = async (domain: string): Promise<DomainCapabilities> => {
|
||||
try {
|
||||
const response = await fetch(`https://${domain}/api/v1/instance`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
|
||||
const emojiReacts = checkEmojiReactsSupport(response);
|
||||
|
||||
const domainCapabilities: DomainCapabilities = {
|
||||
lastChecked: Date.now(),
|
||||
failed: false,
|
||||
emojiReacts,
|
||||
};
|
||||
await setDomainCapabilities(domain, domainCapabilities);
|
||||
return domainCapabilities;
|
||||
} catch (e) {
|
||||
const domainCapabilities: DomainCapabilities = {
|
||||
lastChecked: Date.now(),
|
||||
failed: true,
|
||||
};
|
||||
await setDomainCapabilities(domain, domainCapabilities);
|
||||
return domainCapabilities;
|
||||
}
|
||||
};
|
||||
|
||||
const supportsEmojiReacts = async (accountUrl: string): Promise<'true' | 'false' | 'unknown'> => {
|
||||
const domain = new URL(accountUrl).hostname;
|
||||
let domainCapabilities = capabilities.domains[domain];
|
||||
|
||||
if (!domainCapabilities || domainCapabilities.lastChecked > Date.now() - 1000 * 60 * 60 * 24 * (domainCapabilities.failed ? 1 : 7)) {
|
||||
domainCapabilities = await fetchCapabilities(domain);
|
||||
}
|
||||
|
||||
if (domainCapabilities.failed) {
|
||||
return 'unknown';
|
||||
}
|
||||
if (domainCapabilities.emojiReacts) {
|
||||
return 'true';
|
||||
}
|
||||
return 'false';
|
||||
};
|
||||
|
||||
getCapabilitiesFromMemory();
|
||||
|
||||
export { supportsEmojiReacts };
|
||||
Reference in New Issue
Block a user