Display emoji reactions on glitch-soc and Iceshrimp
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -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': '❤' },
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user