Display emoji reactions on glitch-soc and Iceshrimp

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-10-29 23:00:48 +01:00
parent 3000ed6f9d
commit 1d3424e648
12 changed files with 103 additions and 129 deletions

View file

@ -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';
@ -8,7 +8,7 @@ import { importFetchedAccounts, importFetchedStatus } from './importer';
import { favourite, unfavourite } from './interactions';
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_SUCCESS = 'EMOJI_REACT_SUCCESS';
@ -26,17 +26,17 @@ const noOp = () => () => new Promise(f => f(undefined));
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
(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));
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));
return Promise.all([
...emojiReacts
.filter((emojiReact) => emojiReact.get('me') === true)
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))).toArray(),
.filter((emojiReact) => emojiReact.me === true)
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.name))).toArray(),
status.favourited && dispatch(unfavourite(status)),
]).then(() => {
if (emoji === '👍') {

View file

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory, useRouteMatch } from 'react-router-dom';
@ -626,15 +625,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const reblogCount = status.reblogs_count;
const favouriteCount = status.favourites_count;
const emojiReactCount = reduceEmoji(
(status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList<any>,
const emojiReactCount = status.reactions ? reduceEmoji(
status.reactions,
favouriteCount,
status.favourited,
allowedEmoji,
).reduce((acc, cur) => acc + cur.get('count'), 0);
).reduce((acc, cur) => acc + (cur.count || 0), 0) : undefined;
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 = {
'👍': messages.reactionLike,

View file

@ -4,7 +4,7 @@ import React from 'react';
import { Text, Icon, Emoji } from 'soapbox/components/ui';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import type { Map as ImmutableMap } from 'immutable';
import type { EmojiReaction } from 'soapbox/schemas';
const COLORS = {
accent: 'accent',
@ -33,7 +33,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonEleme
active?: boolean;
color?: Color;
filled?: boolean;
emoji?: ImmutableMap<string, any>;
emoji?: EmojiReaction;
text?: React.ReactNode;
theme?: 'default' | 'inverse';
}
@ -45,7 +45,7 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
if (emoji) {
return (
<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>
);
} else {

View file

@ -71,7 +71,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
};
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 (ownAccount) {

View file

@ -1,6 +1,4 @@
import clsx from 'clsx';
import { List as ImmutableList } from 'immutable';
import React from 'react';
import clsx from 'clsx';import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
@ -59,7 +57,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
const getNormalizedReacts = () => {
return reduceEmoji(
ImmutableList(status.pleroma.get('emoji_reactions') as any),
status.reactions,
status.favourites_count,
status.favourited,
allowedEmoji,
@ -164,20 +162,22 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
const getEmojiReacts = () => {
const emojiReacts = getNormalizedReacts();
const count = emojiReacts.reduce((acc, cur) => (
acc + cur.get('count')
acc + (cur.count || 0)
), 0);
const handleClick = features.emojiReacts ? handleOpenReactionsModal : handleOpenFavouritesModal;
if (count) {
return (
<InteractionCounter count={count} onClick={features.exposableReactions ? handleOpenReactionsModal : undefined}>
<InteractionCounter count={count} onClick={features.exposableReactions ? handleClick : undefined}>
<HStack space={0.5} alignItems='center'>
{emojiReacts.take(3).map((e, i) => {
return (
<Emoji
key={i}
className='h-4.5 w-4.5 flex-none'
emoji={e.get('name')}
src={e.get('url')}
emoji={e.name}
src={e.url}
/>
);
})}
@ -193,7 +193,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
<HStack space={3}>
{getReposts()}
{getQuotes()}
{features.emojiReacts ? getEmojiReacts() : getFavourites()}
{(features.emojiReacts || features.emojiReactsMastodon) ? getEmojiReacts() : getFavourites()}
{getDislikes()}
</HStack>
);

View file

@ -13,10 +13,10 @@ import {
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
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 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 StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self' | 'group';
@ -69,6 +69,7 @@ export const StatusRecord = ImmutableRecord({
poll: null as EmbeddedEntity<Poll>,
quote: null as EmbeddedEntity<any>,
quotes_count: 0,
reactions: null as ImmutableList<EmojiReaction> | null,
reblog: null as EmbeddedEntity<any>,
reblogged: false,
reblogs_count: 0,
@ -104,8 +105,8 @@ const normalizeMentions = (status: ImmutableMap<string, any>) => {
});
};
// Normalize emojis
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
// Normalize emoji reactions
const normalizeReactions = (entity: ImmutableMap<string, any>) => {
return entity.update('emojis', ImmutableList(), emojis => {
return emojis.map(normalizeEmoji);
});
@ -218,6 +219,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. */
const fixContent = (status: ImmutableMap<string, any>) => {
if (status.get('content') === '<p></p>') {
@ -275,6 +284,7 @@ export const normalizeStatus = (status: Record<string, any>) => {
fixQuote(status);
fixSensitivity(status);
normalizeEvent(status);
normalizeReactions(status);
fixContent(status);
normalizeFilterResults(status);
normalizeDislikes(status);

View file

@ -274,13 +274,13 @@ export default function statuses(state = initialState, action: AnyAction): State
case EMOJI_REACT_REQUEST:
return state
.updateIn(
[action.status.id, 'pleroma', 'emoji_reactions'],
[action.status.id, 'reactions'],
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom),
);
case UNEMOJI_REACT_REQUEST:
return state
.updateIn(
[action.status.id, 'pleroma', 'emoji_reactions'],
[action.status.id, 'reactions'],
emojiReacts => simulateUnEmojiReact(emojiReacts as any, action.emoji),
);
case FAVOURITE_FAIL:

View file

@ -49,6 +49,9 @@ const configurationSchema = coerceObject({
max_options: z.number().optional().catch(undefined),
min_expiration: z.number().optional().catch(undefined),
}),
reactions: coerceObject({
max_reactions: z.number().catch(0),
}),
statuses: coerceObject({
max_characters: z.number().optional().catch(undefined),
max_media_attachments: z.number().optional().catch(undefined),

View file

@ -19,7 +19,6 @@ import { contentSchema, dateSchema, filteredArray, makeCustomEmojiMap } from './
import type { Resolve } from 'soapbox/utils/types';
const statusPleromaSchema = z.object({
emoji_reactions: filteredArray(emojiReactionSchema),
event: eventSchema.nullish().catch(undefined),
quote: z.literal(null).catch(null),
quote_visible: z.boolean().catch(true),
@ -51,6 +50,7 @@ const baseStatusSchema = z.object({
muted: z.coerce.boolean(),
pinned: z.coerce.boolean(),
pleroma: statusPleromaSchema.optional().catch(undefined),
reactions: filteredArray(emojiReactionSchema),
poll: pollSchema.nullable().catch(null),
quote: z.literal(null).catch(null),
quotes_count: z.number().catch(0),
@ -131,16 +131,18 @@ const statusSchema = baseStatusSchema.extend({
reblog: embeddedStatusSchema,
pleroma: statusPleromaSchema.extend({
quote: embeddedStatusSchema,
emoji_reactions: filteredArray(emojiReactionSchema),
}).optional().catch(undefined),
}).transform(({ pleroma, ...status }) => {
return {
...status,
event: pleroma?.event,
quote: pleroma?.quote || status.quote || null,
reactions: pleroma?.emoji_reactions || status.reactions || null,
// There's apparently no better way to do this...
// Just trying to remove the `event` and `quote` keys from the object.
pleroma: pleroma ? (() => {
const { event, quote, ...rest } = pleroma;
const { event, quote, emoji_reactions, ...rest } = pleroma;
return rest;
})() : undefined,
};

View file

@ -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 { emojiReactionSchema } from 'soapbox/schemas';
import {
sortEmoji,
mergeEmojiFavourites,
oneEmojiPerAccount,
reduceEmoji,
getReactForStatus,
simulateEmojiReact,
@ -23,7 +23,7 @@ const ALLOWED_EMOJI = ImmutableList([
describe('sortEmoji', () => {
describe('with an unsorted list of emoji', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ 'count': 7, 'me': true, 'name': '😃' },
{ 'count': 7, 'me': true, 'name': '😯' },
{ 'count': 3, 'me': true, 'name': '😢' },
@ -31,7 +31,7 @@ describe('sortEmoji', () => {
{ 'count': 20, 'me': true, 'name': '👍' },
{ 'count': 7, 'me': true, 'name': '😂' },
{ 'count': 15, 'me': true, 'name': '❤' },
]) as ImmutableList<ImmutableMap<string, any>>;
].map((react) => emojiReactionSchema.parse(react)));
it('sorts the emoji by count', () => {
expect(sortEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([
{ 'count': 20, 'me': true, 'name': '👍' },
@ -51,11 +51,11 @@ describe('mergeEmojiFavourites', () => {
const favourited = true;
describe('with existing 👍 reacts', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ 'count': 20, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 15, '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', () => {
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
{ 'count': 32, 'me': true, 'name': '👍', 'url': undefined },
@ -66,10 +66,10 @@ describe('mergeEmojiFavourites', () => {
});
describe('without existing 👍 reacts', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ 'count': 15, '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', () => {
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
{ 'count': 15, 'me': false, 'name': '❤' },
@ -88,7 +88,7 @@ describe('mergeEmojiFavourites', () => {
describe('reduceEmoji', () => {
describe('with a clusterfuck of emoji', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ 'count': 1, 'me': false, 'name': '😡' },
{ 'count': 1, 'me': true, 'name': '🔪' },
{ 'count': 7, 'me': true, 'name': '😯' },
@ -99,7 +99,7 @@ describe('reduceEmoji', () => {
{ 'count': 15, 'me': true, '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', () => {
expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([
{ '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', () => {
it('returns a single owned react (including favourite) for the status', () => {
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', () => {
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)', () => {
@ -172,10 +156,10 @@ describe('getReactForStatus', () => {
describe('simulateEmojiReact', () => {
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 },
]) as ImmutableList<ImmutableMap<string, any>>;
].map((react) => emojiReactionSchema.parse(react)));
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
{ 'count': 2, 'me': false, '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', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ '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([
{ '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', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ '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([
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
@ -209,10 +193,10 @@ describe('simulateEmojiReact', () => {
describe('simulateUnEmojiReact', () => {
it('removes the emoji from the list', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 3, 'me': true, 'name': '❤' },
]) as ImmutableList<ImmutableMap<string, any>>;
].map((react) => emojiReactionSchema.parse(react)));
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
{ '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', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
{ 'count': 1, 'me': true, 'name': '😯' },
]) as ImmutableList<ImmutableMap<string, any>>;
].map((react) => emojiReactionSchema.parse(react)));
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
@ -232,11 +216,11 @@ describe('simulateUnEmojiReact', () => {
});
it ('removes custom emoji from the list', () => {
const emojiReacts = fromJS([
const emojiReacts = ImmutableList([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
{ '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([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },

View file

@ -1,9 +1,6 @@
import {
Map as ImmutableMap,
List as ImmutableList,
} from 'immutable';
import { List as ImmutableList } from 'immutable';
import type { Me } from 'soapbox/types/soapbox';
import { EmojiReaction, emojiReactionSchema } from 'soapbox/schemas';
// https://emojipedia.org/facebook
// I've customized them.
@ -16,18 +13,16 @@ export const ALLOWED_EMOJI = ImmutableList([
'😩',
]);
type Account = ImmutableMap<string, any>;
type EmojiReact = ImmutableMap<string, any>;
export const sortEmoji = (emojiReacts: ImmutableList<EmojiReact>, allowedEmoji: ImmutableList<string>): ImmutableList<EmojiReact> => (
export const sortEmoji = (emojiReacts: ImmutableList<EmojiReaction>, allowedEmoji: ImmutableList<string>): ImmutableList<EmojiReaction> => (
emojiReacts
.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;
const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.get('name') === '👍');
const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.name === '👍');
if (likeIndex > -1) {
const likeCount = Number(emojiReacts.getIn([likeIndex, 'count']));
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, 'me'], favourited);
} 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 => (
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> => (
export const reduceEmoji = (emojiReacts: ImmutableList<EmojiReaction> | null, favouritesCount: number, favourited: boolean, allowedEmoji = ALLOWED_EMOJI): ImmutableList<EmojiReaction> => (
sortEmoji(
mergeEmojiFavourites(emojiReacts, favouritesCount, favourited),
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(
status.pleroma.get('emoji_reactions', ImmutableList()),
status.reactions,
status.favourites_count || 0,
status.favourited,
allowedEmoji,
).filter(e => e.get('me') === true)
).filter(e => e.me === true)
.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) => {
const idx = emojiReacts.findIndex(e => e.get('name') === emoji);
export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReaction>, emoji: string, url?: string) => {
const idx = emojiReacts.findIndex(e => e.name === emoji);
const emojiReact = emojiReacts.get(idx);
if (idx > -1 && emojiReact) {
return emojiReacts.set(idx, emojiReact.merge({
count: emojiReact.get('count') + 1,
return emojiReacts.set(idx, emojiReactionSchema.parse({
...emojiReact,
count: (emojiReact.count || 0) + 1,
me: true,
url,
}));
} else {
return emojiReacts.push(ImmutableMap({
return emojiReacts.push(emojiReactionSchema.parse({
count: 1,
me: true,
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 =>
e.get('name') === emoji && e.get('me') === true);
e.name === emoji && e.me === true);
const emojiReact = emojiReacts.get(idx);
if (emojiReact) {
const newCount = emojiReact.get('count') - 1;
const newCount = (emojiReact.count || 1) - 1;
if (newCount < 1) return emojiReacts.delete(idx);
return emojiReacts.set(idx, emojiReact.merge({
count: emojiReact.get('count') - 1,
return emojiReacts.set(idx, emojiReactionSchema.parse({
count: (emojiReact.count || 1) - 1,
me: false,
}));
} else {

View file

@ -418,6 +418,13 @@ const getInstanceFeatures = (instance: Instance) => {
*/
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.
* @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji