Merge remote-tracking branch 'origin/main' into frontend-rw
Signed-off-by: Marcin Mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
ca262bcbb1
19 changed files with 162 additions and 151 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
import { favourite, unfavourite } from './interactions';
|
import { favourite, unfavourite } from './interactions';
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { APIEntity, Status } from 'soapbox/types/entities';
|
import type { APIEntity, EmojiReaction, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
|
const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
|
||||||
const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
|
const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
|
||||||
|
@ -26,17 +26,17 @@ const noOp = () => () => new Promise(f => f(undefined));
|
||||||
|
|
||||||
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||||
(dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
const emojiReacts: ImmutableList<EmojiReaction> = status.reactions || ImmutableList();
|
||||||
|
|
||||||
if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status));
|
if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status));
|
||||||
|
|
||||||
const undo = emojiReacts.filter(e => e.get('me') === true && e.get('name') === emoji).count() > 0;
|
const undo = emojiReacts.filter(e => e.me === true && e.name === emoji).count() > 0;
|
||||||
if (undo) return dispatch(unEmojiReact(status, emoji));
|
if (undo) return dispatch(unEmojiReact(status, emoji));
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
...emojiReacts
|
...emojiReacts
|
||||||
.filter((emojiReact) => emojiReact.get('me') === true)
|
.filter((emojiReact) => emojiReact.me === true)
|
||||||
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))).toArray(),
|
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.name))).toArray(),
|
||||||
status.favourited && dispatch(unfavourite(status)),
|
status.favourited && dispatch(unfavourite(status)),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
if (emoji === '👍') {
|
if (emoji === '👍') {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
import { Set as ImmutableSet } from 'immutable';
|
||||||
|
|
||||||
import ConfigDB from 'soapbox/utils/config-db';
|
import ConfigDB from 'soapbox/utils/config-db';
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ import { fetchConfig, updateConfig } from './admin';
|
||||||
import type { MRFSimple } from 'soapbox/schemas/pleroma';
|
import type { MRFSimple } from 'soapbox/schemas/pleroma';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
|
||||||
const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: ImmutableMap<string, any>) => {
|
const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: Record<string, any>) => {
|
||||||
const entries = Object.entries(simplePolicy).map(([key, hosts]) => {
|
const entries = Object.entries(simplePolicy).map(([key, hosts]) => {
|
||||||
const isRestricted = restrictions.get(key);
|
const isRestricted = restrictions[key];
|
||||||
|
|
||||||
if (isRestricted) {
|
if (isRestricted) {
|
||||||
return [key, ImmutableSet(hosts).add(host).toJS()];
|
return [key, ImmutableSet(hosts).add(host).toJS()];
|
||||||
|
@ -21,7 +21,7 @@ const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions:
|
||||||
return Object.fromEntries(entries);
|
return Object.fromEntries(entries);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMrf = (host: string, restrictions: ImmutableMap<string, any>) =>
|
const updateMrf = (host: string, restrictions: Record<string, any>) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
dispatch(fetchConfig())
|
dispatch(fetchConfig())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -122,7 +122,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||||
return (
|
return (
|
||||||
<div key={option.title}>
|
<div key={option.title}>
|
||||||
{showResults ? (
|
{showResults ? (
|
||||||
<div title={voted ? message : undefined}>
|
<div title={message}>
|
||||||
<HStack
|
<HStack
|
||||||
justifyContent='between'
|
justifyContent='between'
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
|
|
|
@ -35,6 +35,7 @@ const messages = defineMessages({
|
||||||
invites: { id: 'navigation_bar.invites', defaultMessage: 'Invites' },
|
invites: { id: 'navigation_bar.invites', defaultMessage: 'Invites' },
|
||||||
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
||||||
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
||||||
|
addExternalAccount: { id: 'profile_dropdown.add_external_account', defaultMessage: 'Add account from external instance' },
|
||||||
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
});
|
});
|
||||||
|
@ -331,6 +332,11 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
<Icon className='h-4 w-4 text-primary-500' src={require('@tabler/icons/plus.svg')} />
|
<Icon className='h-4 w-4 text-primary-500' src={require('@tabler/icons/plus.svg')} />
|
||||||
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addAccount)}</Text>
|
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addAccount)}</Text>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink className='flex items-center space-x-1 py-2' to='/login/external' onClick={handleClose}>
|
||||||
|
<Icon className='h-4 w-4 text-primary-500' src={require('@tabler/icons/plus.svg')} />
|
||||||
|
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addExternalAccount)}</Text>
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
|
@ -659,15 +658,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
const reblogCount = status.reblogs_count;
|
const reblogCount = status.reblogs_count;
|
||||||
const favouriteCount = status.favourites_count;
|
const favouriteCount = status.favourites_count;
|
||||||
|
|
||||||
const emojiReactCount = reduceEmoji(
|
const emojiReactCount = status.reactions ? reduceEmoji(
|
||||||
(status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList<any>,
|
status.reactions,
|
||||||
favouriteCount,
|
favouriteCount,
|
||||||
status.favourited,
|
status.favourited,
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
).reduce((acc, cur) => acc + cur.get('count'), 0);
|
).reduce((acc, cur) => acc + (cur.count || 0), 0) : undefined;
|
||||||
|
|
||||||
const meEmojiReact = getReactForStatus(status, allowedEmoji);
|
const meEmojiReact = getReactForStatus(status, allowedEmoji);
|
||||||
const meEmojiName = meEmojiReact?.get('name') as keyof typeof reactMessages | undefined;
|
const meEmojiName = meEmojiReact?.name as keyof typeof reactMessages | undefined;
|
||||||
|
|
||||||
const reactMessages = {
|
const reactMessages = {
|
||||||
'👍': messages.reactionLike,
|
'👍': messages.reactionLike,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||||
import { Text, Icon, Emoji } from 'soapbox/components/ui';
|
import { Text, Icon, Emoji } from 'soapbox/components/ui';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { EmojiReaction } from 'soapbox/schemas';
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
accent: 'accent',
|
accent: 'accent',
|
||||||
|
@ -33,7 +33,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonEleme
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
color?: Color;
|
color?: Color;
|
||||||
filled?: boolean;
|
filled?: boolean;
|
||||||
emoji?: ImmutableMap<string, any>;
|
emoji?: EmojiReaction;
|
||||||
text?: React.ReactNode;
|
text?: React.ReactNode;
|
||||||
theme?: 'default' | 'inverse';
|
theme?: 'default' | 'inverse';
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
|
||||||
if (emoji) {
|
if (emoji) {
|
||||||
return (
|
return (
|
||||||
<span className='flex h-6 w-6 items-center justify-center'>
|
<span className='flex h-6 w-6 items-center justify-center'>
|
||||||
<Emoji className='h-full w-full p-0.5' emoji={emoji.get('name')} src={emoji.get('url')} />
|
<Emoji className='h-full w-full p-0.5' emoji={emoji.name} src={emoji.url} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -71,7 +71,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||||
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍';
|
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.name || '👍';
|
||||||
|
|
||||||
if (isUserTouching()) {
|
if (isUserTouching()) {
|
||||||
if (ownAccount) {
|
if (ownAccount) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useHashtagStream } from 'soapbox/api/hooks';
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { Column, Toggle } from 'soapbox/components/ui';
|
import { Column, Toggle } from 'soapbox/components/ui';
|
||||||
import Timeline from 'soapbox/features/ui/components/timeline';
|
import Timeline from 'soapbox/features/ui/components/timeline';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||||
|
|
||||||
interface IHashtagTimeline {
|
interface IHashtagTimeline {
|
||||||
params?: {
|
params?: {
|
||||||
|
@ -23,6 +23,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const tag = useAppSelector((state) => state.tags.get(id));
|
const tag = useAppSelector((state) => state.tags.get(id));
|
||||||
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
||||||
|
const { isLoggedIn } = useLoggedIn();
|
||||||
|
|
||||||
const handleLoadMore = (maxId: string) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
dispatch(expandHashtagTimeline(id, { url: next, maxId }, intl));
|
dispatch(expandHashtagTimeline(id, { url: next, maxId }, intl));
|
||||||
|
@ -50,7 +51,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={`#${id}`} transparent>
|
<Column label={`#${id}`} transparent>
|
||||||
{features.followHashtags && (
|
{features.followHashtags && isLoggedIn && (
|
||||||
<List>
|
<List>
|
||||||
<ListItem
|
<ListItem
|
||||||
label={<FormattedMessage id='hashtag.follow' defaultMessage='Follow hashtag' />}
|
label={<FormattedMessage id='hashtag.follow' defaultMessage='Follow hashtag' />}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';import React from 'react';
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import React from 'react';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -59,7 +57,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
||||||
|
|
||||||
const getNormalizedReacts = () => {
|
const getNormalizedReacts = () => {
|
||||||
return reduceEmoji(
|
return reduceEmoji(
|
||||||
ImmutableList(status.pleroma.get('emoji_reactions') as any),
|
status.reactions,
|
||||||
status.favourites_count,
|
status.favourites_count,
|
||||||
status.favourited,
|
status.favourited,
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
|
@ -164,20 +162,22 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
||||||
const getEmojiReacts = () => {
|
const getEmojiReacts = () => {
|
||||||
const emojiReacts = getNormalizedReacts();
|
const emojiReacts = getNormalizedReacts();
|
||||||
const count = emojiReacts.reduce((acc, cur) => (
|
const count = emojiReacts.reduce((acc, cur) => (
|
||||||
acc + cur.get('count')
|
acc + (cur.count || 0)
|
||||||
), 0);
|
), 0);
|
||||||
|
|
||||||
|
const handleClick = features.emojiReacts ? handleOpenReactionsModal : handleOpenFavouritesModal;
|
||||||
|
|
||||||
if (count) {
|
if (count) {
|
||||||
return (
|
return (
|
||||||
<InteractionCounter count={count} onClick={features.exposableReactions ? handleOpenReactionsModal : undefined}>
|
<InteractionCounter count={count} onClick={features.exposableReactions ? handleClick : undefined}>
|
||||||
<HStack space={0.5} alignItems='center'>
|
<HStack space={0.5} alignItems='center'>
|
||||||
{emojiReacts.take(3).map((e, i) => {
|
{emojiReacts.take(3).map((e, i) => {
|
||||||
return (
|
return (
|
||||||
<Emoji
|
<Emoji
|
||||||
key={i}
|
key={i}
|
||||||
className='h-4.5 w-4.5 flex-none'
|
className='h-4.5 w-4.5 flex-none'
|
||||||
emoji={e.get('name')}
|
emoji={e.name}
|
||||||
src={e.get('url')}
|
src={e.url}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -193,7 +193,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
||||||
<HStack space={3}>
|
<HStack space={3}>
|
||||||
{getReposts()}
|
{getReposts()}
|
||||||
{getQuotes()}
|
{getQuotes()}
|
||||||
{features.emojiReacts ? getEmojiReacts() : getFavourites()}
|
{(features.emojiReacts || features.emojiReactsMastodon) ? getEmojiReacts() : getFavourites()}
|
||||||
{getDislikes()}
|
{getDislikes()}
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,24 +30,25 @@ const EditFederationModal: React.FC<IEditFederationModal> = ({ host, onClose })
|
||||||
const getRemoteInstance = useCallback(makeGetRemoteInstance(), []);
|
const getRemoteInstance = useCallback(makeGetRemoteInstance(), []);
|
||||||
const remoteInstance = useAppSelector(state => getRemoteInstance(state, host));
|
const remoteInstance = useAppSelector(state => getRemoteInstance(state, host));
|
||||||
|
|
||||||
const [data, setData] = useState({} as any);
|
const [data, setData] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(remoteInstance.get('federation'));
|
setData(remoteInstance.get('federation') as Record<string, any>);
|
||||||
}, [remoteInstance]);
|
}, [remoteInstance]);
|
||||||
|
|
||||||
const handleDataChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => {
|
const handleDataChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => {
|
||||||
return ({ target }) => {
|
return ({ target }) => {
|
||||||
setData(data.set(key, target.checked));
|
setData({ ...data, [key]: target.checked });
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaRemoval: React.ChangeEventHandler<HTMLInputElement> = ({ target: { checked } }) => {
|
const handleMediaRemoval: React.ChangeEventHandler<HTMLInputElement> = ({ target: { checked } }) => {
|
||||||
const newData = data.merge({
|
const newData = {
|
||||||
|
...data,
|
||||||
avatar_removal: checked,
|
avatar_removal: checked,
|
||||||
banner_removal: checked,
|
banner_removal: checked,
|
||||||
media_removal: checked,
|
media_removal: checked,
|
||||||
});
|
};
|
||||||
|
|
||||||
setData(newData);
|
setData(newData);
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
||||||
|
addExternal: { id: 'profile_dropdown.add_external_account', defaultMessage: 'Add account from external instance' },
|
||||||
theme: { id: 'profile_dropdown.theme', defaultMessage: 'Theme' },
|
theme: { id: 'profile_dropdown.theme', defaultMessage: 'Theme' },
|
||||||
logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' },
|
logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' },
|
||||||
});
|
});
|
||||||
|
@ -90,6 +91,12 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
||||||
icon: require('@tabler/icons/plus.svg'),
|
icon: require('@tabler/icons/plus.svg'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
menu.push({
|
||||||
|
text: intl.formatMessage(messages.addExternal),
|
||||||
|
to: '/login/external',
|
||||||
|
icon: require('@tabler/icons/plus.svg'),
|
||||||
|
});
|
||||||
|
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.logout, { acct: account.acct }),
|
text: intl.formatMessage(messages.logout, { acct: account.acct }),
|
||||||
to: '/logout',
|
to: '/logout',
|
||||||
|
|
|
@ -13,10 +13,10 @@ import {
|
||||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
import { normalizeMention } from 'soapbox/normalizers/mention';
|
import { normalizeMention } from 'soapbox/normalizers/mention';
|
||||||
import { accountSchema, cardSchema, groupSchema, pollSchema, tombstoneSchema } from 'soapbox/schemas';
|
import { accountSchema, cardSchema, emojiReactionSchema, groupSchema, pollSchema, tombstoneSchema } from 'soapbox/schemas';
|
||||||
import { maybeFromJS } from 'soapbox/utils/normalizers';
|
import { maybeFromJS } from 'soapbox/utils/normalizers';
|
||||||
|
|
||||||
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
|
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity, EmojiReaction } from 'soapbox/types/entities';
|
||||||
|
|
||||||
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
|
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
|
||||||
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self' | 'group';
|
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self' | 'group';
|
||||||
|
@ -69,6 +69,7 @@ export const StatusRecord = ImmutableRecord({
|
||||||
poll: null as EmbeddedEntity<Poll>,
|
poll: null as EmbeddedEntity<Poll>,
|
||||||
quote: null as EmbeddedEntity<any>,
|
quote: null as EmbeddedEntity<any>,
|
||||||
quotes_count: 0,
|
quotes_count: 0,
|
||||||
|
reactions: null as ImmutableList<EmojiReaction> | null,
|
||||||
reblog: null as EmbeddedEntity<any>,
|
reblog: null as EmbeddedEntity<any>,
|
||||||
reblogged: false,
|
reblogged: false,
|
||||||
reblogs_count: 0,
|
reblogs_count: 0,
|
||||||
|
@ -105,8 +106,8 @@ const normalizeMentions = (status: ImmutableMap<string, any>) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Normalize emojis
|
// Normalize emoji reactions
|
||||||
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
const normalizeReactions = (entity: ImmutableMap<string, any>) => {
|
||||||
return entity.update('emojis', ImmutableList(), emojis => {
|
return entity.update('emojis', ImmutableList(), emojis => {
|
||||||
return emojis.map(normalizeEmoji);
|
return emojis.map(normalizeEmoji);
|
||||||
});
|
});
|
||||||
|
@ -227,6 +228,14 @@ const normalizeEvent = (status: ImmutableMap<string, any>) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Normalize emojis
|
||||||
|
const normalizeEmojis = (status: ImmutableMap<string, any>) => {
|
||||||
|
const reactions = status.getIn(['pleroma', 'emoji_reactions'], status.get('reactions')) as ImmutableList<ImmutableMap<string, any>>;
|
||||||
|
if (reactions) {
|
||||||
|
status.set('reactions', ImmutableList(reactions.map(((reaction: ImmutableMap<string, any>) => emojiReactionSchema.parse(reaction.toJS())))));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** Rewrite `<p></p>` to empty string. */
|
/** Rewrite `<p></p>` to empty string. */
|
||||||
const fixContent = (status: ImmutableMap<string, any>) => {
|
const fixContent = (status: ImmutableMap<string, any>) => {
|
||||||
if (status.get('content') === '<p></p>') {
|
if (status.get('content') === '<p></p>') {
|
||||||
|
@ -285,6 +294,7 @@ export const normalizeStatus = (status: Record<string, any>) => {
|
||||||
fixTranslation(status);
|
fixTranslation(status);
|
||||||
fixSensitivity(status);
|
fixSensitivity(status);
|
||||||
normalizeEvent(status);
|
normalizeEvent(status);
|
||||||
|
normalizeReactions(status);
|
||||||
fixContent(status);
|
fixContent(status);
|
||||||
normalizeFilterResults(status);
|
normalizeFilterResults(status);
|
||||||
normalizeDislikes(status);
|
normalizeDislikes(status);
|
||||||
|
|
|
@ -281,13 +281,13 @@ export default function statuses(state = initialState, action: AnyAction): State
|
||||||
case EMOJI_REACT_REQUEST:
|
case EMOJI_REACT_REQUEST:
|
||||||
return state
|
return state
|
||||||
.updateIn(
|
.updateIn(
|
||||||
[action.status.id, 'pleroma', 'emoji_reactions'],
|
[action.status.id, 'reactions'],
|
||||||
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom),
|
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom),
|
||||||
);
|
);
|
||||||
case UNEMOJI_REACT_REQUEST:
|
case UNEMOJI_REACT_REQUEST:
|
||||||
return state
|
return state
|
||||||
.updateIn(
|
.updateIn(
|
||||||
[action.status.id, 'pleroma', 'emoji_reactions'],
|
[action.status.id, 'reactions'],
|
||||||
emojiReacts => simulateUnEmojiReact(emojiReacts as any, action.emoji),
|
emojiReacts => simulateUnEmojiReact(emojiReacts as any, action.emoji),
|
||||||
);
|
);
|
||||||
case FAVOURITE_FAIL:
|
case FAVOURITE_FAIL:
|
||||||
|
|
|
@ -49,6 +49,9 @@ const configurationSchema = coerceObject({
|
||||||
max_options: z.number().optional().catch(undefined),
|
max_options: z.number().optional().catch(undefined),
|
||||||
min_expiration: z.number().optional().catch(undefined),
|
min_expiration: z.number().optional().catch(undefined),
|
||||||
}),
|
}),
|
||||||
|
reactions: coerceObject({
|
||||||
|
max_reactions: z.number().catch(0),
|
||||||
|
}),
|
||||||
statuses: coerceObject({
|
statuses: coerceObject({
|
||||||
max_characters: z.number().optional().catch(undefined),
|
max_characters: z.number().optional().catch(undefined),
|
||||||
max_media_attachments: z.number().optional().catch(undefined),
|
max_media_attachments: z.number().optional().catch(undefined),
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { contentSchema, dateSchema, filteredArray, makeCustomEmojiMap } from './
|
||||||
import type { Resolve } from 'soapbox/utils/types';
|
import type { Resolve } from 'soapbox/utils/types';
|
||||||
|
|
||||||
const statusPleromaSchema = z.object({
|
const statusPleromaSchema = z.object({
|
||||||
emoji_reactions: filteredArray(emojiReactionSchema),
|
|
||||||
event: eventSchema.nullish().catch(undefined),
|
event: eventSchema.nullish().catch(undefined),
|
||||||
quote: z.literal(null).catch(null),
|
quote: z.literal(null).catch(null),
|
||||||
quote_visible: z.boolean().catch(true),
|
quote_visible: z.boolean().catch(true),
|
||||||
|
@ -51,6 +50,7 @@ const baseStatusSchema = z.object({
|
||||||
muted: z.coerce.boolean(),
|
muted: z.coerce.boolean(),
|
||||||
pinned: z.coerce.boolean(),
|
pinned: z.coerce.boolean(),
|
||||||
pleroma: statusPleromaSchema.optional().catch(undefined),
|
pleroma: statusPleromaSchema.optional().catch(undefined),
|
||||||
|
reactions: filteredArray(emojiReactionSchema),
|
||||||
poll: pollSchema.nullable().catch(null),
|
poll: pollSchema.nullable().catch(null),
|
||||||
quote: z.literal(null).catch(null),
|
quote: z.literal(null).catch(null),
|
||||||
quotes_count: z.number().catch(0),
|
quotes_count: z.number().catch(0),
|
||||||
|
@ -131,16 +131,18 @@ const statusSchema = baseStatusSchema.extend({
|
||||||
reblog: embeddedStatusSchema,
|
reblog: embeddedStatusSchema,
|
||||||
pleroma: statusPleromaSchema.extend({
|
pleroma: statusPleromaSchema.extend({
|
||||||
quote: embeddedStatusSchema,
|
quote: embeddedStatusSchema,
|
||||||
|
emoji_reactions: filteredArray(emojiReactionSchema),
|
||||||
}).optional().catch(undefined),
|
}).optional().catch(undefined),
|
||||||
}).transform(({ pleroma, ...status }) => {
|
}).transform(({ pleroma, ...status }) => {
|
||||||
return {
|
return {
|
||||||
...status,
|
...status,
|
||||||
event: pleroma?.event,
|
event: pleroma?.event,
|
||||||
quote: pleroma?.quote || status.quote || null,
|
quote: pleroma?.quote || status.quote || null,
|
||||||
|
reactions: pleroma?.emoji_reactions || status.reactions || null,
|
||||||
// There's apparently no better way to do this...
|
// There's apparently no better way to do this...
|
||||||
// Just trying to remove the `event` and `quote` keys from the object.
|
// Just trying to remove the `event` and `quote` keys from the object.
|
||||||
pleroma: pleroma ? (() => {
|
pleroma: pleroma ? (() => {
|
||||||
const { event, quote, ...rest } = pleroma;
|
const { event, quote, emoji_reactions, ...rest } = pleroma;
|
||||||
return rest;
|
return rest;
|
||||||
})() : undefined,
|
})() : undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,7 @@ import trimStart from 'lodash/trimStart';
|
||||||
import { type MRFSimple, mrfSimpleSchema } from 'soapbox/schemas/pleroma';
|
import { type MRFSimple, mrfSimpleSchema } from 'soapbox/schemas/pleroma';
|
||||||
|
|
||||||
export type Config = ImmutableMap<string, any>;
|
export type Config = ImmutableMap<string, any>;
|
||||||
export type Policy = ImmutableMap<string, any>;
|
export type Policy = Record<string, any>;
|
||||||
|
|
||||||
const find = (
|
const find = (
|
||||||
configs: ImmutableList<Config>,
|
configs: ImmutableList<Config>,
|
||||||
|
@ -40,15 +40,15 @@ const toSimplePolicy = (configs: ImmutableList<Config>): MRFSimple => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromSimplePolicy = (simplePolicy: Policy): ImmutableList<Config> => {
|
const fromSimplePolicy = (simplePolicy: Policy): ImmutableList<Config> => {
|
||||||
const mapper = (hosts: ImmutableList<string>, key: string) => fromJS({ tuple: [`:${key}`, hosts.toJS()] });
|
const mapper = ([key, hosts]: [key: string, hosts: ImmutableList<string>]) => fromJS({ tuple: [`:${key}`, hosts] });
|
||||||
|
|
||||||
const value = simplePolicy.map(mapper).toList();
|
const value = Object.entries(simplePolicy).map(mapper);
|
||||||
|
|
||||||
return ImmutableList([
|
return ImmutableList([
|
||||||
ImmutableMap({
|
ImmutableMap({
|
||||||
group: ':pleroma',
|
group: ':pleroma',
|
||||||
key: ':mrf_simple',
|
key: ':mrf_simple',
|
||||||
value,
|
value: ImmutableList(value),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
|
import { List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { normalizeStatus } from 'soapbox/normalizers';
|
import { normalizeStatus } from 'soapbox/normalizers';
|
||||||
|
import { emojiReactionSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sortEmoji,
|
sortEmoji,
|
||||||
mergeEmojiFavourites,
|
mergeEmojiFavourites,
|
||||||
oneEmojiPerAccount,
|
|
||||||
reduceEmoji,
|
reduceEmoji,
|
||||||
getReactForStatus,
|
getReactForStatus,
|
||||||
simulateEmojiReact,
|
simulateEmojiReact,
|
||||||
|
@ -23,7 +23,7 @@ const ALLOWED_EMOJI = ImmutableList([
|
||||||
|
|
||||||
describe('sortEmoji', () => {
|
describe('sortEmoji', () => {
|
||||||
describe('with an unsorted list of emoji', () => {
|
describe('with an unsorted list of emoji', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 7, 'me': true, 'name': '😃' },
|
{ 'count': 7, 'me': true, 'name': '😃' },
|
||||||
{ 'count': 7, 'me': true, 'name': '😯' },
|
{ 'count': 7, 'me': true, 'name': '😯' },
|
||||||
{ 'count': 3, 'me': true, 'name': '😢' },
|
{ 'count': 3, 'me': true, 'name': '😢' },
|
||||||
|
@ -31,7 +31,7 @@ describe('sortEmoji', () => {
|
||||||
{ 'count': 20, 'me': true, 'name': '👍' },
|
{ 'count': 20, 'me': true, 'name': '👍' },
|
||||||
{ 'count': 7, 'me': true, 'name': '😂' },
|
{ 'count': 7, 'me': true, 'name': '😂' },
|
||||||
{ 'count': 15, 'me': true, 'name': '❤' },
|
{ 'count': 15, 'me': true, 'name': '❤' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('sorts the emoji by count', () => {
|
it('sorts the emoji by count', () => {
|
||||||
expect(sortEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([
|
expect(sortEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([
|
||||||
{ 'count': 20, 'me': true, 'name': '👍' },
|
{ 'count': 20, 'me': true, 'name': '👍' },
|
||||||
|
@ -51,11 +51,11 @@ describe('mergeEmojiFavourites', () => {
|
||||||
const favourited = true;
|
const favourited = true;
|
||||||
|
|
||||||
describe('with existing 👍 reacts', () => {
|
describe('with existing 👍 reacts', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 20, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 20, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 15, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 15, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
{ 'count': 7, 'me': false, 'name': '😯', 'url': undefined },
|
{ 'count': 7, 'me': false, 'name': '😯', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('combines 👍 reacts with favourites', () => {
|
it('combines 👍 reacts with favourites', () => {
|
||||||
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
||||||
{ 'count': 32, 'me': true, 'name': '👍', 'url': undefined },
|
{ 'count': 32, 'me': true, 'name': '👍', 'url': undefined },
|
||||||
|
@ -66,10 +66,10 @@ describe('mergeEmojiFavourites', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('without existing 👍 reacts', () => {
|
describe('without existing 👍 reacts', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 15, 'me': false, 'name': '❤' },
|
{ 'count': 15, 'me': false, 'name': '❤' },
|
||||||
{ 'count': 7, 'me': false, 'name': '😯' },
|
{ 'count': 7, 'me': false, 'name': '😯' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('adds 👍 reacts to the map equaling favourite count', () => {
|
it('adds 👍 reacts to the map equaling favourite count', () => {
|
||||||
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
||||||
{ 'count': 15, 'me': false, 'name': '❤' },
|
{ 'count': 15, 'me': false, 'name': '❤' },
|
||||||
|
@ -88,7 +88,7 @@ describe('mergeEmojiFavourites', () => {
|
||||||
|
|
||||||
describe('reduceEmoji', () => {
|
describe('reduceEmoji', () => {
|
||||||
describe('with a clusterfuck of emoji', () => {
|
describe('with a clusterfuck of emoji', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 1, 'me': false, 'name': '😡' },
|
{ 'count': 1, 'me': false, 'name': '😡' },
|
||||||
{ 'count': 1, 'me': true, 'name': '🔪' },
|
{ 'count': 1, 'me': true, 'name': '🔪' },
|
||||||
{ 'count': 7, 'me': true, 'name': '😯' },
|
{ 'count': 7, 'me': true, 'name': '😯' },
|
||||||
|
@ -99,7 +99,7 @@ describe('reduceEmoji', () => {
|
||||||
{ 'count': 15, 'me': true, 'name': '❤' },
|
{ 'count': 15, 'me': true, 'name': '❤' },
|
||||||
{ 'count': 1, 'me': false, 'name': '👀' },
|
{ 'count': 1, 'me': false, 'name': '👀' },
|
||||||
{ 'count': 1, 'me': false, 'name': '🍩' },
|
{ 'count': 1, 'me': false, 'name': '🍩' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('sorts, filters, and combines emoji and favourites', () => {
|
it('sorts, filters, and combines emoji and favourites', () => {
|
||||||
expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([
|
expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([
|
||||||
{ 'count': 27, 'me': true, 'name': '👍' },
|
{ 'count': 27, 'me': true, 'name': '👍' },
|
||||||
|
@ -117,22 +117,6 @@ describe('reduceEmoji', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('oneEmojiPerAccount', () => {
|
|
||||||
it('reduces to one react per account', () => {
|
|
||||||
const emojiReacts = fromJS([
|
|
||||||
// Sorted
|
|
||||||
{ 'count': 2, 'me': true, 'name': '👍', accounts: [{ id: '1' }, { id: '2' }] },
|
|
||||||
{ 'count': 2, 'me': true, 'name': '❤', accounts: [{ id: '1' }, { id: '2' }] },
|
|
||||||
{ 'count': 1, 'me': true, 'name': '😯', accounts: [{ id: '1' }] },
|
|
||||||
{ 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] },
|
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
|
||||||
expect(oneEmojiPerAccount(emojiReacts, '1')).toEqual(fromJS([
|
|
||||||
{ 'count': 2, 'me': true, 'name': '👍', accounts: [{ id: '1' }, { id: '2' }] },
|
|
||||||
{ 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] },
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getReactForStatus', () => {
|
describe('getReactForStatus', () => {
|
||||||
it('returns a single owned react (including favourite) for the status', () => {
|
it('returns a single owned react (including favourite) for the status', () => {
|
||||||
const status = normalizeStatus(fromJS({
|
const status = normalizeStatus(fromJS({
|
||||||
|
@ -146,12 +130,12 @@ describe('getReactForStatus', () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
expect(getReactForStatus(status, ALLOWED_EMOJI)?.get('name')).toEqual('❤');
|
expect(getReactForStatus(status, ALLOWED_EMOJI)?.name).toEqual('❤');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a thumbs-up for a favourite', () => {
|
it('returns a thumbs-up for a favourite', () => {
|
||||||
const status = normalizeStatus(fromJS({ favourites_count: 1, favourited: true }));
|
const status = normalizeStatus(fromJS({ favourites_count: 1, favourited: true }));
|
||||||
expect(getReactForStatus(status)?.get('name')).toEqual('👍');
|
expect(getReactForStatus(status)?.name).toEqual('👍');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined when a status has no reacts (or favourites)', () => {
|
it('returns undefined when a status has no reacts (or favourites)', () => {
|
||||||
|
@ -172,10 +156,10 @@ describe('getReactForStatus', () => {
|
||||||
|
|
||||||
describe('simulateEmojiReact', () => {
|
describe('simulateEmojiReact', () => {
|
||||||
it('adds the emoji to the list', () => {
|
it('adds the emoji to the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 3, 'me': true, 'name': '❤', 'url': undefined },
|
{ 'count': 3, 'me': true, 'name': '❤', 'url': undefined },
|
||||||
|
@ -183,10 +167,10 @@ describe('simulateEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates the emoji if it didn\'t already exist', () => {
|
it('creates the emoji if it didn\'t already exist', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
|
@ -195,10 +179,10 @@ describe('simulateEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds a custom emoji to the list', () => {
|
it('adds a custom emoji to the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateEmojiReact(emojiReacts, 'soapbox', 'https://gleasonator.com/emoji/Gleasonator/soapbox.png')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, 'soapbox', 'https://gleasonator.com/emoji/Gleasonator/soapbox.png')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
|
@ -209,10 +193,10 @@ describe('simulateEmojiReact', () => {
|
||||||
|
|
||||||
describe('simulateUnEmojiReact', () => {
|
describe('simulateUnEmojiReact', () => {
|
||||||
it('removes the emoji from the list', () => {
|
it('removes the emoji from the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 3, 'me': true, 'name': '❤' },
|
{ 'count': 3, 'me': true, 'name': '❤' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
|
@ -220,11 +204,11 @@ describe('simulateUnEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes the emoji if it\'s the last one in the list', () => {
|
it('removes the emoji if it\'s the last one in the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
{ 'count': 1, 'me': true, 'name': '😯' },
|
{ 'count': 1, 'me': true, 'name': '😯' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
|
@ -232,11 +216,11 @@ describe('simulateUnEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('removes custom emoji from the list', () => {
|
it ('removes custom emoji from the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
{ 'count': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' },
|
{ 'count': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateUnEmojiReact(emojiReacts, 'soapbox')).toEqual(fromJS([
|
expect(simulateUnEmojiReact(emojiReacts, 'soapbox')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import {
|
import { List as ImmutableList } from 'immutable';
|
||||||
Map as ImmutableMap,
|
|
||||||
List as ImmutableList,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import type { Me } from 'soapbox/types/soapbox';
|
import { EmojiReaction, emojiReactionSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
// https://emojipedia.org/facebook
|
// https://emojipedia.org/facebook
|
||||||
// I've customized them.
|
// I've customized them.
|
||||||
|
@ -16,18 +13,16 @@ export const ALLOWED_EMOJI = ImmutableList([
|
||||||
'😩',
|
'😩',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type Account = ImmutableMap<string, any>;
|
export const sortEmoji = (emojiReacts: ImmutableList<EmojiReaction>, allowedEmoji: ImmutableList<string>): ImmutableList<EmojiReaction> => (
|
||||||
type EmojiReact = ImmutableMap<string, any>;
|
|
||||||
|
|
||||||
export const sortEmoji = (emojiReacts: ImmutableList<EmojiReact>, allowedEmoji: ImmutableList<string>): ImmutableList<EmojiReact> => (
|
|
||||||
emojiReacts
|
emojiReacts
|
||||||
.sortBy(emojiReact =>
|
.sortBy(emojiReact =>
|
||||||
-(emojiReact.get('count') + Number(allowedEmoji.includes(emojiReact.get('name')))))
|
-((emojiReact.count || 0) + Number(allowedEmoji.includes(emojiReact.name))))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const mergeEmojiFavourites = (emojiReacts = ImmutableList<EmojiReact>(), favouritesCount: number, favourited: boolean) => {
|
export const mergeEmojiFavourites = (emojiReacts: ImmutableList<EmojiReaction> | null, favouritesCount: number, favourited: boolean) => {
|
||||||
|
if (!emojiReacts) return ImmutableList([emojiReactionSchema.parse({ count: favouritesCount, me: favourited, name: '👍' })]);
|
||||||
if (!favouritesCount) return emojiReacts;
|
if (!favouritesCount) return emojiReacts;
|
||||||
const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.get('name') === '👍');
|
const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.name === '👍');
|
||||||
if (likeIndex > -1) {
|
if (likeIndex > -1) {
|
||||||
const likeCount = Number(emojiReacts.getIn([likeIndex, 'count']));
|
const likeCount = Number(emojiReacts.getIn([likeIndex, 'count']));
|
||||||
favourited = favourited || Boolean(emojiReacts.getIn([likeIndex, 'me'], false));
|
favourited = favourited || Boolean(emojiReacts.getIn([likeIndex, 'me'], false));
|
||||||
|
@ -35,69 +30,43 @@ export const mergeEmojiFavourites = (emojiReacts = ImmutableList<EmojiReact>(),
|
||||||
.setIn([likeIndex, 'count'], likeCount + favouritesCount)
|
.setIn([likeIndex, 'count'], likeCount + favouritesCount)
|
||||||
.setIn([likeIndex, 'me'], favourited);
|
.setIn([likeIndex, 'me'], favourited);
|
||||||
} else {
|
} else {
|
||||||
return emojiReacts.push(ImmutableMap({ count: favouritesCount, me: favourited, name: '👍' }));
|
return emojiReacts.push(emojiReactionSchema.parse({ count: favouritesCount, me: favourited, name: '👍' }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMultiReactions = (emojiReacts: ImmutableList<EmojiReact>, account: Account): boolean => (
|
export const reduceEmoji = (emojiReacts: ImmutableList<EmojiReaction> | null, favouritesCount: number, favourited: boolean, allowedEmoji = ALLOWED_EMOJI): ImmutableList<EmojiReaction> => (
|
||||||
emojiReacts.filter(
|
|
||||||
e => e.get('accounts').filter(
|
|
||||||
(a: Account) => a.get('id') === account.get('id'),
|
|
||||||
).count() > 0,
|
|
||||||
).count() > 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const inAccounts = (accounts: ImmutableList<Account>, id: string): boolean => (
|
|
||||||
accounts.filter(a => a.get('id') === id).count() > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
export const oneEmojiPerAccount = (emojiReacts: ImmutableList<EmojiReact>, me: Me) => {
|
|
||||||
emojiReacts = emojiReacts.reverse();
|
|
||||||
|
|
||||||
return emojiReacts.reduce((acc, cur, idx) => {
|
|
||||||
const accounts = cur.get('accounts', ImmutableList())
|
|
||||||
.filter((a: Account) => !hasMultiReactions(acc, a));
|
|
||||||
|
|
||||||
return acc.set(idx, cur.merge({
|
|
||||||
accounts: accounts,
|
|
||||||
count: accounts.count(),
|
|
||||||
me: me ? inAccounts(accounts, me) : false,
|
|
||||||
}));
|
|
||||||
}, emojiReacts)
|
|
||||||
.filter(e => e.get('count') > 0)
|
|
||||||
.reverse();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reduceEmoji = (emojiReacts: ImmutableList<EmojiReact>, favouritesCount: number, favourited: boolean, allowedEmoji = ALLOWED_EMOJI): ImmutableList<EmojiReact> => (
|
|
||||||
sortEmoji(
|
sortEmoji(
|
||||||
mergeEmojiFavourites(emojiReacts, favouritesCount, favourited),
|
mergeEmojiFavourites(emojiReacts, favouritesCount, favourited),
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
));
|
));
|
||||||
|
|
||||||
export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): EmojiReact | undefined => {
|
export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): EmojiReaction | undefined => {
|
||||||
|
if (!status.reactions) return;
|
||||||
|
|
||||||
const result = reduceEmoji(
|
const result = reduceEmoji(
|
||||||
status.pleroma.get('emoji_reactions', ImmutableList()),
|
status.reactions,
|
||||||
status.favourites_count || 0,
|
status.favourites_count || 0,
|
||||||
status.favourited,
|
status.favourited,
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
).filter(e => e.get('me') === true)
|
).filter(e => e.me === true)
|
||||||
.get(0);
|
.get(0);
|
||||||
|
|
||||||
return typeof result?.get('name') === 'string' ? result : undefined;
|
return typeof result?.name === 'string' ? result : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji: string, url?: string) => {
|
export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReaction>, emoji: string, url?: string) => {
|
||||||
const idx = emojiReacts.findIndex(e => e.get('name') === emoji);
|
const idx = emojiReacts.findIndex(e => e.name === emoji);
|
||||||
const emojiReact = emojiReacts.get(idx);
|
const emojiReact = emojiReacts.get(idx);
|
||||||
|
|
||||||
if (idx > -1 && emojiReact) {
|
if (idx > -1 && emojiReact) {
|
||||||
return emojiReacts.set(idx, emojiReact.merge({
|
return emojiReacts.set(idx, emojiReactionSchema.parse({
|
||||||
count: emojiReact.get('count') + 1,
|
...emojiReact,
|
||||||
|
count: (emojiReact.count || 0) + 1,
|
||||||
me: true,
|
me: true,
|
||||||
url,
|
url,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return emojiReacts.push(ImmutableMap({
|
return emojiReacts.push(emojiReactionSchema.parse({
|
||||||
count: 1,
|
count: 1,
|
||||||
me: true,
|
me: true,
|
||||||
name: emoji,
|
name: emoji,
|
||||||
|
@ -106,17 +75,17 @@ export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const simulateUnEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji: string) => {
|
export const simulateUnEmojiReact = (emojiReacts: ImmutableList<EmojiReaction>, emoji: string) => {
|
||||||
const idx = emojiReacts.findIndex(e =>
|
const idx = emojiReacts.findIndex(e =>
|
||||||
e.get('name') === emoji && e.get('me') === true);
|
e.name === emoji && e.me === true);
|
||||||
|
|
||||||
const emojiReact = emojiReacts.get(idx);
|
const emojiReact = emojiReacts.get(idx);
|
||||||
|
|
||||||
if (emojiReact) {
|
if (emojiReact) {
|
||||||
const newCount = emojiReact.get('count') - 1;
|
const newCount = (emojiReact.count || 1) - 1;
|
||||||
if (newCount < 1) return emojiReacts.delete(idx);
|
if (newCount < 1) return emojiReacts.delete(idx);
|
||||||
return emojiReacts.set(idx, emojiReact.merge({
|
return emojiReacts.set(idx, emojiReactionSchema.parse({
|
||||||
count: emojiReact.get('count') - 1,
|
count: (emojiReact.count || 1) - 1,
|
||||||
me: false,
|
me: false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -14,24 +14,30 @@ const overrides = custom('features');
|
||||||
/** Truthy array convenience function */
|
/** Truthy array convenience function */
|
||||||
const any = (arr: Array<any>): boolean => arr.some(Boolean);
|
const any = (arr: Array<any>): boolean => arr.some(Boolean);
|
||||||
|
|
||||||
/**
|
|
||||||
* Firefish, a fork of Misskey. Formerly known as Calckey.
|
|
||||||
* @see {@link https://joinfirefish.org/}
|
|
||||||
*/
|
|
||||||
export const FIREFISH = 'Firefish';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ditto, a Nostr server with Mastodon API.
|
* Ditto, a Nostr server with Mastodon API.
|
||||||
* @see {@link https://gitlab.com/soapbox-pub/ditto}
|
* @see {@link https://gitlab.com/soapbox-pub/ditto}
|
||||||
*/
|
*/
|
||||||
export const DITTO = 'Ditto';
|
export const DITTO = 'Ditto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Firefish, a fork of Misskey. Formerly known as Calckey.
|
||||||
|
* @see {@link https://joinfirefish.org/}
|
||||||
|
*/
|
||||||
|
export const FIREFISH = 'Firefish';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Friendica, decentralized social platform implementing multiple federation protocols.
|
* Friendica, decentralized social platform implementing multiple federation protocols.
|
||||||
* @see {@link https://friendi.ca/}
|
* @see {@link https://friendi.ca/}
|
||||||
*/
|
*/
|
||||||
export const FRIENDICA = 'Friendica';
|
export const FRIENDICA = 'Friendica';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iceshrimp, yet another Misskey fork.
|
||||||
|
* @see {@link https://iceshrimp.dev/}
|
||||||
|
*/
|
||||||
|
export const ICESHRIMP = 'Iceshrimp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mastodon, the software upon which this is all based.
|
* Mastodon, the software upon which this is all based.
|
||||||
* @see {@link https://joinmastodon.org/}
|
* @see {@link https://joinmastodon.org/}
|
||||||
|
@ -143,6 +149,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
accountLookup: any([
|
accountLookup: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
||||||
|
@ -192,6 +199,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see {@link https://docs.joinmastodon.org/methods/announcements/}
|
* @see {@link https://docs.joinmastodon.org/methods/announcements/}
|
||||||
*/
|
*/
|
||||||
announcements: any([
|
announcements: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.2.49'),
|
v.software === PLEROMA && gte(v.version, '2.2.49'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.7.0'),
|
v.software === TAKAHE && gte(v.version, '0.7.0'),
|
||||||
|
@ -230,6 +238,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
bookmarks: any([
|
bookmarks: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
|
@ -319,6 +328,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
conversations: any([
|
conversations: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
|
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
|
@ -359,6 +369,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
editProfile: any([
|
editProfile: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === MITRA,
|
v.software === MITRA,
|
||||||
v.software === PIXELFED,
|
v.software === PIXELFED,
|
||||||
|
@ -374,6 +385,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
editStatuses: any([
|
editStatuses: any([
|
||||||
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.version, '3.5.0'),
|
v.software === MASTODON && gte(v.version, '3.5.0'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
||||||
features.includes('editing'),
|
features.includes('editing'),
|
||||||
|
@ -406,6 +418,13 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'),
|
emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability to add emoji reactions to a status available in Mastodon forks.
|
||||||
|
* @see POST /v1/statuses/:id/react/:emoji
|
||||||
|
* @see POST /v1/statuses/:id/unreact/:emoji
|
||||||
|
*/
|
||||||
|
emojiReactsMastodon: instance.configuration.reactions.max_reactions > 0,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The backend allows only non-RGI ("Recommended for General Interchange") emoji reactions.
|
* The backend allows only non-RGI ("Recommended for General Interchange") emoji reactions.
|
||||||
* @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
|
* @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
|
||||||
|
@ -444,6 +463,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
exposableReactions: any([
|
exposableReactions: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
|
@ -634,6 +654,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
lists: any([
|
lists: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
]),
|
]),
|
||||||
|
@ -689,6 +710,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see PUT /api/v1/accounts/:id/mute
|
* @see PUT /api/v1/accounts/:id/mute
|
||||||
*/
|
*/
|
||||||
mutesDuration: any([
|
mutesDuration: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === PLEROMA && gte(v.version, '2.3.0'),
|
v.software === PLEROMA && gte(v.version, '2.3.0'),
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
|
||||||
v.software === TAKAHE,
|
v.software === TAKAHE,
|
||||||
|
@ -721,6 +743,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see GET /api/v1/notifications
|
* @see GET /api/v1/notifications
|
||||||
*/
|
*/
|
||||||
notificationsIncludeTypes: any([
|
notificationsIncludeTypes: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.2'),
|
v.software === TAKAHE && gte(v.version, '0.6.2'),
|
||||||
|
@ -745,6 +768,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
polls: any([
|
polls: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.version, '2.8.0'),
|
v.software === MASTODON && gte(v.version, '2.8.0'),
|
||||||
v.software === PLEROMA,
|
v.software === PLEROMA,
|
||||||
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
||||||
|
@ -785,6 +809,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
publicTimeline: any([
|
publicTimeline: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === PLEROMA,
|
v.software === PLEROMA,
|
||||||
v.software === TAKAHE,
|
v.software === TAKAHE,
|
||||||
|
@ -870,6 +895,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see POST /api/v2/search
|
* @see POST /api/v2/search
|
||||||
*/
|
*/
|
||||||
searchFromAccount: any([
|
searchFromAccount: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.version, '2.8.0'),
|
v.software === MASTODON && gte(v.version, '2.8.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '1.0.0'),
|
v.software === PLEROMA && gte(v.version, '1.0.0'),
|
||||||
]),
|
]),
|
||||||
|
@ -923,6 +949,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
suggestionsV2: any([
|
suggestionsV2: any([
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
features.includes('v2_suggestions'),
|
features.includes('v2_suggestions'),
|
||||||
|
@ -939,6 +966,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see GET /api/v1/trends/statuses
|
* @see GET /api/v1/trends/statuses
|
||||||
*/
|
*/
|
||||||
trendingStatuses: any([
|
trendingStatuses: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
||||||
]),
|
]),
|
||||||
|
@ -949,6 +977,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
trends: any([
|
trends: any([
|
||||||
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
v.software === DITTO,
|
v.software === DITTO,
|
||||||
|
|
Loading…
Reference in a new issue