Merge branch 'custom-emoji-reacts' into 'develop'
Support custom emoji reacts See merge request soapbox-pub/soapbox!2360
This commit is contained in:
commit
4124b85ede
|
@ -25,7 +25,7 @@ const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
|
||||||
|
|
||||||
const noOp = () => () => new Promise(f => f(undefined));
|
const noOp = () => () => new Promise(f => f(undefined));
|
||||||
|
|
||||||
const simpleEmojiReact = (status: Status, emoji: 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<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ const simpleEmojiReact = (status: Status, emoji: string) =>
|
||||||
if (emoji === '👍') {
|
if (emoji === '👍') {
|
||||||
dispatch(favourite(status));
|
dispatch(favourite(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(emojiReact(status, emoji));
|
dispatch(emojiReact(status, emoji, custom));
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -70,11 +70,11 @@ const fetchEmojiReacts = (id: string, emoji: string) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const emojiReact = (status: Status, emoji: string) =>
|
const emojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return dispatch(noOp());
|
if (!isLoggedIn(getState)) return dispatch(noOp());
|
||||||
|
|
||||||
dispatch(emojiReactRequest(status, emoji));
|
dispatch(emojiReactRequest(status, emoji, custom));
|
||||||
|
|
||||||
return api(getState)
|
return api(getState)
|
||||||
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||||
|
@ -120,10 +120,11 @@ const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiReactRequest = (status: Status, emoji: string) => ({
|
const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({
|
||||||
type: EMOJI_REACT_REQUEST,
|
type: EMOJI_REACT_REQUEST,
|
||||||
status,
|
status,
|
||||||
emoji,
|
emoji,
|
||||||
|
custom,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,7 @@ export interface IAccount {
|
||||||
showEdit?: boolean
|
showEdit?: boolean
|
||||||
approvalStatus?: StatusApprovalStatus
|
approvalStatus?: StatusApprovalStatus
|
||||||
emoji?: string
|
emoji?: string
|
||||||
|
emojiUrl?: string
|
||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +117,7 @@ const Account = ({
|
||||||
showEdit = false,
|
showEdit = false,
|
||||||
approvalStatus,
|
approvalStatus,
|
||||||
emoji,
|
emoji,
|
||||||
|
emojiUrl,
|
||||||
note,
|
note,
|
||||||
}: IAccount) => {
|
}: IAccount) => {
|
||||||
const overflowRef = useRef<HTMLDivElement>(null);
|
const overflowRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -193,6 +195,7 @@ const Account = ({
|
||||||
<Emoji
|
<Emoji
|
||||||
className='absolute bottom-0 -right-1.5 h-5 w-5'
|
className='absolute bottom-0 -right-1.5 h-5 w-5'
|
||||||
emoji={emoji}
|
emoji={emoji}
|
||||||
|
src={emojiUrl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</LinkEl>
|
</LinkEl>
|
||||||
|
|
|
@ -538,7 +538,8 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
).reduce((acc, cur) => acc + cur.get('count'), 0);
|
).reduce((acc, cur) => acc + cur.get('count'), 0);
|
||||||
|
|
||||||
const meEmojiReact = getReactForStatus(status, allowedEmoji) as keyof typeof reactMessages | undefined;
|
const meEmojiReact = getReactForStatus(status, allowedEmoji);
|
||||||
|
const meEmojiName = meEmojiReact?.get('name') as keyof typeof reactMessages | undefined;
|
||||||
|
|
||||||
const reactMessages = {
|
const reactMessages = {
|
||||||
'👍': messages.reactionLike,
|
'👍': messages.reactionLike,
|
||||||
|
@ -550,7 +551,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
'': messages.favourite,
|
'': messages.favourite,
|
||||||
};
|
};
|
||||||
|
|
||||||
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite);
|
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiName || ''] || messages.favourite);
|
||||||
|
|
||||||
const menu = _makeMenu(publicStatus);
|
const menu = _makeMenu(publicStatus);
|
||||||
let reblogIcon = require('@tabler/icons/repeat.svg');
|
let reblogIcon = require('@tabler/icons/repeat.svg');
|
||||||
|
@ -635,7 +636,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
icon={require('@tabler/icons/heart.svg')}
|
icon={require('@tabler/icons/heart.svg')}
|
||||||
filled
|
filled
|
||||||
color='accent'
|
color='accent'
|
||||||
active={Boolean(meEmojiReact)}
|
active={Boolean(meEmojiName)}
|
||||||
count={emojiReactCount}
|
count={emojiReactCount}
|
||||||
emoji={meEmojiReact}
|
emoji={meEmojiReact}
|
||||||
text={withLabels ? meEmojiTitle : undefined}
|
text={withLabels ? meEmojiTitle : undefined}
|
||||||
|
@ -648,7 +649,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
color='accent'
|
color='accent'
|
||||||
filled
|
filled
|
||||||
onClick={handleFavouriteClick}
|
onClick={handleFavouriteClick}
|
||||||
active={Boolean(meEmojiReact)}
|
active={Boolean(meEmojiName)}
|
||||||
count={favouriteCount}
|
count={favouriteCount}
|
||||||
text={withLabels ? meEmojiTitle : undefined}
|
text={withLabels ? meEmojiTitle : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,6 +4,8 @@ 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';
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
accent: 'accent',
|
accent: 'accent',
|
||||||
success: 'success',
|
success: 'success',
|
||||||
|
@ -31,7 +33,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonEleme
|
||||||
active?: boolean
|
active?: boolean
|
||||||
color?: Color
|
color?: Color
|
||||||
filled?: boolean
|
filled?: boolean
|
||||||
emoji?: string
|
emoji?: ImmutableMap<string, any>
|
||||||
text?: React.ReactNode
|
text?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +44,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} />
|
<Emoji className='h-full w-full p-0.5' emoji={emoji.get('name')} src={emoji.get('url')} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -60,9 +60,9 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReact = (emoji: string): void => {
|
const handleReact = (emoji: string, custom?: string): void => {
|
||||||
if (ownAccount) {
|
if (ownAccount) {
|
||||||
dispatch(simpleEmojiReact(status, emoji));
|
dispatch(simpleEmojiReact(status, emoji, custom));
|
||||||
} else {
|
} else {
|
||||||
handleUnauthorized();
|
handleUnauthorized();
|
||||||
}
|
}
|
||||||
|
@ -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) || '👍';
|
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍';
|
||||||
|
|
||||||
if (isUserTouching()) {
|
if (isUserTouching()) {
|
||||||
if (ownAccount) {
|
if (ownAccount) {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { usePopper } from 'react-popper';
|
||||||
|
|
||||||
import { Emoji as EmojiComponent, HStack, IconButton } from 'soapbox/components/ui';
|
import { Emoji as EmojiComponent, HStack, IconButton } from 'soapbox/components/ui';
|
||||||
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown';
|
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
import { useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
interface IEmojiButton {
|
interface IEmojiButton {
|
||||||
/** Unicode emoji character. */
|
/** Unicode emoji character. */
|
||||||
|
@ -39,7 +39,7 @@ const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabInd
|
||||||
interface IEmojiSelector {
|
interface IEmojiSelector {
|
||||||
onClose?(): void
|
onClose?(): void
|
||||||
/** Event handler when an emoji is clicked. */
|
/** Event handler when an emoji is clicked. */
|
||||||
onReact(emoji: string): void
|
onReact(emoji: string, custom?: string): void
|
||||||
/** Element that triggers the EmojiSelector Popper */
|
/** Element that triggers the EmojiSelector Popper */
|
||||||
referenceElement: HTMLElement | null
|
referenceElement: HTMLElement | null
|
||||||
placement?: Placement
|
placement?: Placement
|
||||||
|
@ -62,6 +62,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
all = true,
|
all = true,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
const { customEmojiReacts } = useFeatures();
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
@ -102,7 +103,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePickEmoji = (emoji: Emoji) => {
|
const handlePickEmoji = (emoji: Emoji) => {
|
||||||
onReact((emoji as NativeEmoji).native);
|
onReact(emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
|
@ -148,7 +149,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
visible={expanded}
|
visible={expanded}
|
||||||
setVisible={setExpanded}
|
setVisible={setExpanded}
|
||||||
update={update}
|
update={update}
|
||||||
withCustom={false}
|
withCustom={customEmojiReacts}
|
||||||
onPickEmoji={handlePickEmoji}
|
onPickEmoji={handlePickEmoji}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
|
|
||||||
/** A single emoji image. */
|
/** A single emoji image. */
|
||||||
const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
||||||
const { emoji, alt, ...rest } = props;
|
const { emoji, alt, src, ...rest } = props;
|
||||||
const codepoints = toCodePoints(removeVS16s(emoji));
|
const codepoints = toCodePoints(removeVS16s(emoji));
|
||||||
const filename = codepoints.join('-');
|
const filename = codepoints.join('-');
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
||||||
<img
|
<img
|
||||||
draggable='false'
|
draggable='false'
|
||||||
alt={alt || emoji}
|
alt={alt || emoji}
|
||||||
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
|
src={src || joinPublicPath(`packs/emoji/${filename}.svg`)}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,7 +28,7 @@ export interface CustomEmoji {
|
||||||
export interface NativeEmoji {
|
export interface NativeEmoji {
|
||||||
id: string
|
id: string
|
||||||
colons: string
|
colons: string
|
||||||
custom?: boolean
|
custom?: false
|
||||||
unified: string
|
unified: string
|
||||||
native: string
|
native: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,6 +269,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Emoji
|
<Emoji
|
||||||
emoji={notification.emoji}
|
emoji={notification.emoji}
|
||||||
|
src={notification.emoji_url || undefined}
|
||||||
className='h-4 w-4 flex-none'
|
className='h-4 w-4 flex-none'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -154,6 +154,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
||||||
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.get('name')}
|
||||||
|
src={e.get('url')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';
|
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';
|
||||||
|
@ -17,6 +17,12 @@ const messages = defineMessages({
|
||||||
all: { id: 'reactions.all', defaultMessage: 'All' },
|
all: { id: 'reactions.all', defaultMessage: 'All' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface IAccountWithReaction {
|
||||||
|
id: string
|
||||||
|
reaction: string
|
||||||
|
reactionUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface IReactionsModal {
|
interface IReactionsModal {
|
||||||
onClose: (string: string) => void
|
onClose: (string: string) => void
|
||||||
statusId: string
|
statusId: string
|
||||||
|
@ -54,7 +60,7 @@ const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, reaction
|
||||||
reactions!.forEach(reaction => items.push(
|
reactions!.forEach(reaction => items.push(
|
||||||
{
|
{
|
||||||
text: <div className='flex items-center gap-1'>
|
text: <div className='flex items-center gap-1'>
|
||||||
<Emoji className='h-4 w-4' emoji={reaction.name} />
|
<Emoji className='h-4 w-4' emoji={reaction.name} src={reaction.url || undefined} />
|
||||||
{reaction.count}
|
{reaction.count}
|
||||||
</div>,
|
</div>,
|
||||||
action: () => setReaction(reaction.name),
|
action: () => setReaction(reaction.name),
|
||||||
|
@ -65,17 +71,25 @@ const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, reaction
|
||||||
return <Tabs items={items} activeItem={reaction || 'all'} />;
|
return <Tabs items={items} activeItem={reaction || 'all'} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const accounts = useMemo((): ImmutableList<IAccountWithReaction> | undefined => {
|
||||||
|
if (!reactions) return;
|
||||||
|
|
||||||
|
if (reaction) {
|
||||||
|
const reactionRecord = reactions.find(({ name }) => name === reaction);
|
||||||
|
|
||||||
|
if (reactionRecord) return reactionRecord.accounts.map(account => ({ id: account, reaction: reaction, reactionUrl: reactionRecord.url || undefined })).toList();
|
||||||
|
} else {
|
||||||
|
return reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url }))).flatten() as ImmutableList<IAccountWithReaction>;
|
||||||
|
}
|
||||||
|
}, [reactions, reaction]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const accounts = reactions && (reaction
|
|
||||||
? reactions.find(({ name }) => name === reaction)?.accounts.map(account => ({ id: account, reaction: reaction }))
|
|
||||||
: reactions.map(({ accounts, name }) => accounts.map(account => ({ id: account, reaction: name }))).flatten()) as ImmutableList<{ id: string, reaction: string }>;
|
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
|
|
||||||
if (!accounts) {
|
if (!accounts || !reactions) {
|
||||||
body = <Spinner />;
|
body = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
||||||
|
@ -91,7 +105,7 @@ const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, reaction
|
||||||
itemClassName='pb-3'
|
itemClassName='pb-3'
|
||||||
>
|
>
|
||||||
{accounts.map((account) =>
|
{accounts.map((account) =>
|
||||||
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} emoji={account.reaction} />,
|
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} emoji={account.reaction} emojiUrl={account.reactionUrl} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</>);
|
</>);
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const NotificationRecord = ImmutableRecord({
|
||||||
chat_message: null as ImmutableMap<string, any> | string | null, // pleroma:chat_mention
|
chat_message: null as ImmutableMap<string, any> | string | null, // pleroma:chat_mention
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
emoji: null as string | null, // pleroma:emoji_reaction
|
emoji: null as string | null, // pleroma:emoji_reaction
|
||||||
|
emoji_url: null as string | null, // pleroma:emoji_reaction
|
||||||
id: '',
|
id: '',
|
||||||
status: null as EmbeddedEntity<Status>,
|
status: null as EmbeddedEntity<Status>,
|
||||||
target: null as EmbeddedEntity<Account>, // move
|
target: null as EmbeddedEntity<Account>, // move
|
||||||
|
|
|
@ -242,7 +242,7 @@ export default function statuses(state = initialState, action: AnyAction): State
|
||||||
return state
|
return state
|
||||||
.updateIn(
|
.updateIn(
|
||||||
[action.status.get('id'), 'pleroma', 'emoji_reactions'],
|
[action.status.get('id'), 'pleroma', 'emoji_reactions'],
|
||||||
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji),
|
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom),
|
||||||
);
|
);
|
||||||
case UNEMOJI_REACT_REQUEST:
|
case UNEMOJI_REACT_REQUEST:
|
||||||
return state
|
return state
|
||||||
|
|
|
@ -82,6 +82,7 @@ export const ReactionRecord = ImmutableRecord({
|
||||||
accounts: ImmutableOrderedSet<string>(),
|
accounts: ImmutableOrderedSet<string>(),
|
||||||
count: 0,
|
count: 0,
|
||||||
name: '',
|
name: '',
|
||||||
|
url: null as string | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ReactionListRecord = ImmutableRecord({
|
const ReactionListRecord = ImmutableRecord({
|
||||||
|
|
|
@ -52,15 +52,15 @@ describe('mergeEmojiFavourites', () => {
|
||||||
|
|
||||||
describe('with existing 👍 reacts', () => {
|
describe('with existing 👍 reacts', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = fromJS([
|
||||||
{ 'count': 20, 'me': false, 'name': '👍' },
|
{ 'count': 20, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 15, 'me': false, 'name': '❤' },
|
{ 'count': 15, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
{ 'count': 7, 'me': false, 'name': '😯' },
|
{ 'count': 7, 'me': false, 'name': '😯', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
]) as ImmutableList<ImmutableMap<string, any>>;
|
||||||
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': '👍' },
|
{ 'count': 32, 'me': true, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 15, 'me': false, 'name': '❤' },
|
{ 'count': 15, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
{ 'count': 7, 'me': false, 'name': '😯' },
|
{ 'count': 7, 'me': false, 'name': '😯', 'url': undefined },
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -146,12 +146,12 @@ describe('getReactForStatus', () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
expect(getReactForStatus(status, ALLOWED_EMOJI)).toEqual('❤');
|
expect(getReactForStatus(status, ALLOWED_EMOJI)?.get('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)).toEqual('👍');
|
expect(getReactForStatus(status)?.get('name')).toEqual('👍');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined when a status has no reacts (or favourites)', () => {
|
it('returns undefined when a status has no reacts (or favourites)', () => {
|
||||||
|
@ -173,24 +173,36 @@ 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 = fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
]) as ImmutableList<ImmutableMap<string, any>>;
|
||||||
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 3, 'me': true, 'name': '❤' },
|
{ 'count': 3, 'me': true, 'name': '❤', 'url': undefined },
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
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 = fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
]) as ImmutableList<ImmutableMap<string, any>>;
|
||||||
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
{ 'count': 1, 'me': true, 'name': '😯' },
|
{ 'count': 1, 'me': true, 'name': '😯', 'url': undefined },
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a custom emoji to the list', () => {
|
||||||
|
const emojiReacts = fromJS([
|
||||||
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
|
]) as ImmutableList<ImmutableMap<string, any>>;
|
||||||
|
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': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' },
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -218,4 +230,16 @@ describe('simulateUnEmojiReact', () => {
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it ('removes custom emoji from the list', () => {
|
||||||
|
const emojiReacts = fromJS([
|
||||||
|
{ '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>>;
|
||||||
|
expect(simulateUnEmojiReact(emojiReacts, 'soapbox')).toEqual(fromJS([
|
||||||
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
|
]));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -74,19 +74,19 @@ export const reduceEmoji = (emojiReacts: ImmutableList<EmojiReact>, favouritesCo
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
));
|
));
|
||||||
|
|
||||||
export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): string | undefined => {
|
export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): EmojiReact | undefined => {
|
||||||
const result = reduceEmoji(
|
const result = reduceEmoji(
|
||||||
status.pleroma.get('emoji_reactions', ImmutableList()),
|
status.pleroma.get('emoji_reactions', ImmutableList()),
|
||||||
status.favourites_count || 0,
|
status.favourites_count || 0,
|
||||||
status.favourited,
|
status.favourited,
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
).filter(e => e.get('me') === true)
|
).filter(e => e.get('me') === true)
|
||||||
.getIn([0, 'name']);
|
.get(0);
|
||||||
|
|
||||||
return typeof result === 'string' ? result : undefined;
|
return typeof result?.get('name') === 'string' ? result : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji: string) => {
|
export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji: string, url?: string) => {
|
||||||
const idx = emojiReacts.findIndex(e => e.get('name') === emoji);
|
const idx = emojiReacts.findIndex(e => e.get('name') === emoji);
|
||||||
const emojiReact = emojiReacts.get(idx);
|
const emojiReact = emojiReacts.get(idx);
|
||||||
|
|
||||||
|
@ -94,12 +94,14 @@ export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji
|
||||||
return emojiReacts.set(idx, emojiReact.merge({
|
return emojiReacts.set(idx, emojiReact.merge({
|
||||||
count: emojiReact.get('count') + 1,
|
count: emojiReact.get('count') + 1,
|
||||||
me: true,
|
me: true,
|
||||||
|
url,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return emojiReacts.push(ImmutableMap({
|
return emojiReacts.push(ImmutableMap({
|
||||||
count: 1,
|
count: 1,
|
||||||
me: true,
|
me: true,
|
||||||
name: emoji,
|
name: emoji,
|
||||||
|
url,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -324,6 +324,11 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
v.software === TAKAHE,
|
v.software === TAKAHE,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability to add non-standard reactions to a status.
|
||||||
|
*/
|
||||||
|
customEmojiReacts: v.software === PLEROMA && gte(v.version, '2.5.50'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy DMs timeline where messages are displayed chronologically without groupings.
|
* Legacy DMs timeline where messages are displayed chronologically without groupings.
|
||||||
* @see GET /api/v1/timelines/direct
|
* @see GET /api/v1/timelines/direct
|
||||||
|
|
Loading…
Reference in New Issue