pl-fe: We're not Facebook anymore
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
f2a7513d35
commit
584cde8c40
17 changed files with 203 additions and 517 deletions
|
@ -3,9 +3,8 @@ import { isLoggedIn } from 'pl-fe/utils/auth';
|
|||
import { getClient } from '../api';
|
||||
|
||||
import { importFetchedStatus } from './importer';
|
||||
import { favourite, unfavourite } from './interactions';
|
||||
|
||||
import type { EmojiReaction, Status } from 'pl-api';
|
||||
import type { Status } from 'pl-api';
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST' as const;
|
||||
|
@ -18,31 +17,6 @@ const UNEMOJI_REACT_FAIL = 'UNEMOJI_REACT_FAIL' as const;
|
|||
|
||||
const noOp = () => () => new Promise(f => f(undefined));
|
||||
|
||||
const simpleEmojiReact = (status: Pick<Status, 'id' | 'emoji_reactions' | 'favourited'>, emoji: string, custom?: string) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
const emojiReacts: Array<EmojiReaction> = status.emoji_reactions || [];
|
||||
|
||||
if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status));
|
||||
|
||||
const undo = emojiReacts.filter(e => e.me === true && e.name === emoji).length > 0;
|
||||
if (undo) return dispatch(unEmojiReact(status, emoji));
|
||||
|
||||
return Promise.all([
|
||||
...emojiReacts
|
||||
.filter((emojiReact) => emojiReact.me === true)
|
||||
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.name))),
|
||||
status.favourited && dispatch(unfavourite(status)),
|
||||
]).then(() => {
|
||||
if (emoji === '👍') {
|
||||
dispatch(favourite(status));
|
||||
} else {
|
||||
dispatch(emojiReact(status, emoji, custom));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const emojiReact = (status: Pick<Status, 'id'>, emoji: string, custom?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return dispatch(noOp());
|
||||
|
@ -119,7 +93,6 @@ export {
|
|||
UNEMOJI_REACT_REQUEST,
|
||||
UNEMOJI_REACT_SUCCESS,
|
||||
UNEMOJI_REACT_FAIL,
|
||||
simpleEmojiReact,
|
||||
emojiReact,
|
||||
unEmojiReact,
|
||||
emojiReactRequest,
|
||||
|
|
|
@ -39,7 +39,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, sty
|
|||
// @ts-ignore
|
||||
if (unicodeMapping[shortCode]) {
|
||||
// @ts-ignore
|
||||
shortCode = unicodeMapping[shortCode].shortCode;
|
||||
shortCode = unicodeMapping[shortCode].shortcode;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useHistory, useRouteMatch } from 'react-router-dom';
|
|||
|
||||
import { blockAccount } from 'pl-fe/actions/accounts';
|
||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose';
|
||||
import { emojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import { editEvent } from 'pl-fe/actions/events';
|
||||
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'pl-fe/actions/interactions';
|
||||
import { openModal } from 'pl-fe/actions/modals';
|
||||
|
@ -18,18 +19,18 @@ import { useBlockGroupMember, useGroup, useGroupRelationship, useTranslationLang
|
|||
import { useDeleteGroupStatus } from 'pl-fe/api/hooks/groups/useDeleteGroupStatus';
|
||||
import DropdownMenu from 'pl-fe/components/dropdown-menu';
|
||||
import StatusActionButton from 'pl-fe/components/status-action-button';
|
||||
import StatusReactionWrapper from 'pl-fe/components/status-reaction-wrapper';
|
||||
import { HStack } from 'pl-fe/components/ui';
|
||||
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { languages } from 'pl-fe/features/preferences';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useInstance, useOwnAccount, useSettings } from 'pl-fe/hooks';
|
||||
import { useChats } from 'pl-fe/queries/chats';
|
||||
import toast from 'pl-fe/toast';
|
||||
import copy from 'pl-fe/utils/copy';
|
||||
import { getReactForStatus, reduceEmoji } from 'pl-fe/utils/emoji-reacts';
|
||||
|
||||
import GroupPopover from './groups/popover/group-popover';
|
||||
|
||||
import type { Menu } from 'pl-fe/components/dropdown-menu';
|
||||
import type { Emoji as EmojiType } from 'pl-fe/features/emoji';
|
||||
import type { UnauthorizedModalAction } from 'pl-fe/features/ui/components/modals/unauthorized-modal';
|
||||
import type { Account, Group } from 'pl-fe/normalizers';
|
||||
import type { SelectedStatus } from 'pl-fe/selectors';
|
||||
|
@ -77,12 +78,6 @@ const messages = defineMessages({
|
|||
open: { id: 'status.open', defaultMessage: 'Show post details' },
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
quotePost: { id: 'status.quote', defaultMessage: 'Quote post' },
|
||||
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
|
||||
reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' },
|
||||
reactionLaughing: { id: 'status.reactions.laughing', defaultMessage: 'Haha' },
|
||||
reactionLike: { id: 'status.reactions.like', defaultMessage: 'Like' },
|
||||
reactionOpenMouth: { id: 'status.reactions.open_mouth', defaultMessage: 'Wow' },
|
||||
reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
|
@ -99,6 +94,7 @@ const messages = defineMessages({
|
|||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
viewReactions: { id: 'status.view_reactions', defaultMessage: 'View reactions' },
|
||||
addKnownLanguage: { id: 'status.add_known_language', defaultMessage: 'Do not auto-translate posts in {language}.' },
|
||||
translate: { id: 'status.translate', defaultMessage: 'Translate' },
|
||||
hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' },
|
||||
|
@ -201,6 +197,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handlePickEmoji = (emoji: EmojiType) => {
|
||||
dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
|
||||
};
|
||||
|
||||
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
dispatch(toggleBookmark(status));
|
||||
};
|
||||
|
@ -307,6 +307,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}));
|
||||
};
|
||||
|
||||
const handleOpenReactionsModal = (): void => {
|
||||
dispatch(openModal('REACTIONS', { statusId: status.id }));
|
||||
};
|
||||
|
||||
const handleReport: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
dispatch(initReport(ReportableEntities.STATUS, status.account, { status }));
|
||||
};
|
||||
|
@ -410,6 +414,14 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (status.emoji_reactions.length && features.exposableReactions) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.viewReactions),
|
||||
action: handleOpenReactionsModal,
|
||||
icon: require('@tabler/icons/outline/mood-happy.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (!me) {
|
||||
return menu;
|
||||
}
|
||||
|
@ -625,27 +637,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const quoteCount = status.quotes_count;
|
||||
const favouriteCount = status.favourites_count;
|
||||
|
||||
const emojiReactCount = status.emoji_reactions ? reduceEmoji(
|
||||
status.emoji_reactions,
|
||||
favouriteCount,
|
||||
status.favourited,
|
||||
).reduce((acc, cur) => acc + (cur.count || 0), 0) : undefined;
|
||||
|
||||
const meEmojiReact = getReactForStatus(status);
|
||||
const meEmojiName = meEmojiReact?.name as keyof typeof reactMessages | undefined;
|
||||
|
||||
const reactMessages = {
|
||||
'👍': messages.reactionLike,
|
||||
'❤️': messages.reactionHeart,
|
||||
'😆': messages.reactionLaughing,
|
||||
'😮': messages.reactionOpenMouth,
|
||||
'😢': messages.reactionCry,
|
||||
'😩': messages.reactionWeary,
|
||||
'': messages.favourite,
|
||||
};
|
||||
|
||||
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiName || ''] || messages.favourite);
|
||||
|
||||
const menu = _makeMenu(publicStatus);
|
||||
let reblogIcon = require('@tabler/icons/outline/repeat.svg');
|
||||
let replyTitle;
|
||||
|
@ -738,33 +729,17 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
reblogButton
|
||||
)}
|
||||
|
||||
{features.emojiReacts ? (
|
||||
<StatusReactionWrapper statusId={status.id}>
|
||||
<StatusActionButton
|
||||
title={meEmojiTitle}
|
||||
icon={require('@tabler/icons/outline/heart.svg')}
|
||||
filled
|
||||
color='accent'
|
||||
active={Boolean(meEmojiName)}
|
||||
count={emojiReactCount}
|
||||
emoji={meEmojiReact}
|
||||
text={withLabels ? meEmojiTitle : undefined}
|
||||
theme={statusActionButtonTheme}
|
||||
/>
|
||||
</StatusReactionWrapper>
|
||||
) : (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.favourite)}
|
||||
icon={features.statusDislikes ? require('@tabler/icons/outline/thumb-up.svg') : require('@tabler/icons/outline/heart.svg')}
|
||||
color='accent'
|
||||
filled
|
||||
onClick={handleFavouriteClick}
|
||||
active={status.favourited}
|
||||
count={favouriteCount}
|
||||
text={withLabels ? intl.formatMessage(status.favourited ? messages.reactionLike : messages.favourite) : undefined}
|
||||
theme={statusActionButtonTheme}
|
||||
/>
|
||||
)}
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.favourite)}
|
||||
icon={features.statusDislikes ? require('@tabler/icons/outline/thumb-up.svg') : require('@tabler/icons/outline/heart.svg')}
|
||||
color='accent'
|
||||
filled
|
||||
onClick={handleFavouriteClick}
|
||||
active={status.favourited}
|
||||
count={favouriteCount}
|
||||
text={withLabels ? intl.formatMessage(messages.favourite) : undefined}
|
||||
theme={statusActionButtonTheme}
|
||||
/>
|
||||
|
||||
{features.statusDislikes && (
|
||||
<StatusActionButton
|
||||
|
@ -780,6 +755,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{expandable && (features.emojiReacts || features.emojiReactsMastodon) && (
|
||||
<EmojiPickerDropdown onPickEmoji={handlePickEmoji} />
|
||||
)}
|
||||
|
||||
{canShare && (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.share)}
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { simpleEmojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import { openModal } from 'pl-fe/actions/modals';
|
||||
import { EmojiSelector, Portal } from 'pl-fe/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useOwnAccount } from 'pl-fe/hooks';
|
||||
import { userTouching } from 'pl-fe/is-mobile';
|
||||
import { getReactForStatus } from 'pl-fe/utils/emoji-reacts';
|
||||
|
||||
interface IStatusReactionWrapper {
|
||||
statusId: string;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
/** Provides emoji reaction functionality to the underlying button component */
|
||||
const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { account: ownAccount } = useOwnAccount();
|
||||
const status = useAppSelector(state => state.statuses.get(statusId));
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!status) return null;
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
|
||||
if (!userTouching.matches) {
|
||||
setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
|
||||
// Unless the user is touching, delay closing the emoji selector briefly
|
||||
// so the user can move the mouse diagonally to make a selection.
|
||||
if (userTouching.matches) {
|
||||
setVisible(false);
|
||||
} else {
|
||||
timeout.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReact = (emoji: string, custom?: string): void => {
|
||||
if (ownAccount) {
|
||||
dispatch(simpleEmojiReact(status, emoji, custom));
|
||||
} else {
|
||||
handleUnauthorized();
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const meEmojiReact = getReactForStatus(status)?.name || '👍';
|
||||
|
||||
if (userTouching.matches) {
|
||||
if (ownAccount) {
|
||||
if (visible) {
|
||||
handleReact(meEmojiReact);
|
||||
} else {
|
||||
setVisible(true);
|
||||
}
|
||||
} else {
|
||||
handleUnauthorized();
|
||||
}
|
||||
} else {
|
||||
handleReact(meEmojiReact);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleUnauthorized = () => {
|
||||
dispatch(openModal('UNAUTHORIZED', {
|
||||
action: 'FAVOURITE',
|
||||
ap_id: status.url,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{React.cloneElement(children, {
|
||||
onClick: handleClick,
|
||||
ref: setReferenceElement,
|
||||
})}
|
||||
|
||||
{visible && (
|
||||
<Portal>
|
||||
<EmojiSelector
|
||||
placement='top-start'
|
||||
referenceElement={referenceElement}
|
||||
onReact={handleReact}
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusReactionWrapper as default };
|
113
packages/pl-fe/src/components/status-reactions-bar.tsx
Normal file
113
packages/pl-fe/src/components/status-reactions-bar.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import clsx from 'clsx';
|
||||
import { EmojiReaction } from 'pl-api';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import unicodeMapping from 'pl-fe/features/emoji/mapping';
|
||||
import { useAppDispatch, useSettings } from 'pl-fe/hooks';
|
||||
import { sortEmoji } from 'pl-fe/utils/emoji-reacts';
|
||||
|
||||
import AnimatedNumber from './animated-number';
|
||||
import { Emoji, HStack, Icon, Text } from './ui';
|
||||
|
||||
import type { Emoji as EmojiType } from 'pl-fe/features/emoji';
|
||||
import type { SelectedStatus } from 'pl-fe/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
emojiCount: { id: 'status.reactions.label', defaultMessage: '{count} {count, plural, one {person} other {people}} reacted with {emoji}' },
|
||||
addEmoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
});
|
||||
|
||||
interface IStatusReactionsBar {
|
||||
status: Pick<SelectedStatus, 'id' | 'emoji_reactions'>;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
interface IStatusReaction {
|
||||
status: Pick<SelectedStatus, 'id'>;
|
||||
reaction: EmojiReaction;
|
||||
obfuscate?: boolean;
|
||||
}
|
||||
|
||||
const StatusReaction: React.FC<IStatusReaction> = ({ reaction, status, obfuscate }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
if (!reaction.count) return null;
|
||||
|
||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (reaction.me) {
|
||||
dispatch(unEmojiReact(status, reaction.name));
|
||||
} else {
|
||||
dispatch(emojiReact(status, reaction.name));
|
||||
}
|
||||
};
|
||||
|
||||
let shortCode = reaction.name;
|
||||
|
||||
// @ts-ignore
|
||||
if (unicodeMapping[shortCode]?.shortcode) {
|
||||
// @ts-ignore
|
||||
shortCode = unicodeMapping[shortCode].shortcode;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx('flex cursor-pointer items-center gap-2 rounded-md border border-gray-400 p-1.5 transition-colors', {
|
||||
'bg-primary-100 dark:border-primary-400 dark:bg-primary-400 hover:bg-primary-200 hover:dark:border-primary-300 hover:dark:bg-primary-300': reaction.me,
|
||||
'bg-transparent dark:border-primary-700 dark:bg-primary-700 hover:bg-primary-100 hover:dark:border-primary-600 hover:dark:bg-primary-600': !reaction.me,
|
||||
})}
|
||||
key={reaction.name}
|
||||
onClick={handleClick}
|
||||
title={intl.formatMessage(messages.emojiCount, {
|
||||
emoji: `:${shortCode}:`,
|
||||
count: reaction.count,
|
||||
})}
|
||||
>
|
||||
<Emoji className='h-4 w-4' emoji={reaction.name} src={reaction.url || undefined} />
|
||||
|
||||
<Text size='xs' weight='semibold' theme='inherit'>
|
||||
<AnimatedNumber value={reaction.count} obfuscate={obfuscate} short />
|
||||
</Text>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusReactionsBar: React.FC<IStatusReactionsBar> = ({ status, collapsed }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const { demetricator } = useSettings();
|
||||
|
||||
const handlePickEmoji = (emoji: EmojiType) => {
|
||||
dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
|
||||
};
|
||||
|
||||
if ((demetricator || status.emoji_reactions.length === 0) && collapsed) return null;
|
||||
|
||||
const sortedReactions = sortEmoji(status.emoji_reactions);
|
||||
|
||||
return (
|
||||
<HStack className='pt-2' space={2} wrap>
|
||||
{sortedReactions.map((reaction) => reaction.count ? (
|
||||
<StatusReaction key={reaction.name} status={status} reaction={reaction} obfuscate={demetricator} />
|
||||
) : null)}
|
||||
<EmojiPickerDropdown onPickEmoji={handlePickEmoji}>
|
||||
<button
|
||||
className='emoji-picker-dropdown cursor-pointer rounded-md border border-gray-400 bg-transparent p-1.5 transition-colors hover:bg-gray-50 dark:border-primary-700 dark:bg-primary-700 hover:dark:border-primary-600 hover:dark:bg-primary-600'
|
||||
title={intl.formatMessage(messages.addEmoji)}
|
||||
>
|
||||
<Icon
|
||||
className='h-4 w-4'
|
||||
src={require('@tabler/icons/outline/mood-plus.svg')}
|
||||
/>
|
||||
</button>
|
||||
</EmojiPickerDropdown>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusReactionsBar as default };
|
|
@ -21,6 +21,7 @@ import StatusActionBar from './status-action-bar';
|
|||
import StatusContent from './status-content';
|
||||
import StatusLanguagePicker from './status-language-picker';
|
||||
import StatusMedia from './status-media';
|
||||
import StatusReactionsBar from './status-reactions-bar';
|
||||
import StatusReplyMentions from './status-reply-mentions';
|
||||
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||
import StatusInfo from './statuses/status-info';
|
||||
|
@ -174,16 +175,11 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
};
|
||||
|
||||
const handleHotkeyReact = (): void => {
|
||||
_expandEmojiSelector();
|
||||
(node.current?.querySelector('.emoji-picker-dropdown') as HTMLButtonElement)?.click();
|
||||
};
|
||||
|
||||
const handleUnfilter = () => dispatch(unfilterStatus(status.filtered.length ? status.id : actualStatus.id));
|
||||
|
||||
const _expandEmojiSelector = (): void => {
|
||||
const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji');
|
||||
firstEmoji?.focus();
|
||||
};
|
||||
|
||||
const renderStatusInfo = () => {
|
||||
if (isReblog && showGroup && group) {
|
||||
return (
|
||||
|
@ -452,8 +448,15 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
)}
|
||||
</Stack>
|
||||
|
||||
<StatusReactionsBar status={actualStatus} collapsed />
|
||||
|
||||
{!hideActionBar && (
|
||||
<div className='pt-4'>
|
||||
<div
|
||||
className={clsx({
|
||||
'pt-2': actualStatus.emoji_reactions.length,
|
||||
'pt-4': !actualStatus.emoji_reactions.length,
|
||||
})}
|
||||
>
|
||||
<StatusActionBar status={actualStatus} rebloggedBy={isReblog ? status.account : undefined} fromBookmarks={fromBookmarks} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -54,7 +54,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
|
||||
const button = (
|
||||
<button className='w-fit' onClick={handleTranslate}>
|
||||
<HStack alignItems='center' space={1} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<HStack alignItems='center' space={1} className='text-primary-600 hover:underline dark:text-gray-600'>
|
||||
<Icon src={require('@tabler/icons/outline/language.svg')} className='h-4 w-4' />
|
||||
<span>
|
||||
{status.translation ? (
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
import { shift, useFloating, Placement, offset, OffsetOptions } from '@floating-ui/react';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import EmojiComponent from 'pl-fe/components/ui/emoji/emoji';
|
||||
import HStack from 'pl-fe/components/ui/hstack/hstack';
|
||||
import IconButton from 'pl-fe/components/ui/icon-button/icon-button';
|
||||
import EmojiPickerDropdown from 'pl-fe/features/emoji/components/emoji-picker-dropdown';
|
||||
import { useAppSelector, useClickOutside, useFeatures, usePlFeConfig } from 'pl-fe/hooks';
|
||||
|
||||
import type { Emoji } from 'pl-fe/features/emoji';
|
||||
|
||||
interface IEmojiButton {
|
||||
/** Unicode emoji character. */
|
||||
emoji: string;
|
||||
/** Event handler when the emoji is clicked. */
|
||||
onClick(emoji: string, custom?: string): void;
|
||||
/** Extra class name on the <button> element. */
|
||||
className?: string;
|
||||
/** Tab order of the button. */
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
/** Clickable emoji button that scales when hovered. */
|
||||
const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabIndex }): JSX.Element => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const customEmoji = emoji.startsWith(':') ? useAppSelector((state) => {
|
||||
return state.custom_emojis.find(({ shortcode }) => `:${shortcode}:` === emoji);
|
||||
}) : undefined;
|
||||
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClick(customEmoji ? emoji.replace(/:(\w+):/, '$1') : emoji, customEmoji?.url);
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
|
||||
<EmojiComponent className='h-6 w-6 duration-100 hover:scale-110' emoji={emoji} src={customEmoji?.url} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IEmojiSelector {
|
||||
onClose?(): void;
|
||||
/** Event handler when an emoji is clicked. */
|
||||
onReact(emoji: string, custom?: string): void;
|
||||
/** Element that triggers the EmojiSelector Popper */
|
||||
referenceElement: HTMLElement | null;
|
||||
placement?: Placement;
|
||||
/** Whether the selector should be visible. */
|
||||
visible?: boolean;
|
||||
offsetOptions?: OffsetOptions;
|
||||
/** Whether to allow any emoji to be chosen. */
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
/** Panel with a row of emoji buttons. */
|
||||
const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||
referenceElement,
|
||||
onClose,
|
||||
onReact,
|
||||
placement = 'top',
|
||||
visible = false,
|
||||
offsetOptions,
|
||||
all = true,
|
||||
}): JSX.Element => {
|
||||
const plFeConfig = usePlFeConfig();
|
||||
const { customEmojiReacts } = useFeatures();
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const { x, y, strategy, refs, update } = useFloating<HTMLElement>({
|
||||
placement,
|
||||
middleware: [offset(offsetOptions), shift()],
|
||||
});
|
||||
|
||||
const handleExpand: React.MouseEventHandler = () => {
|
||||
setExpanded(true);
|
||||
};
|
||||
|
||||
const handlePickEmoji = (emoji: Emoji) => {
|
||||
onReact(emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refs.setReference(referenceElement);
|
||||
}, [referenceElement]);
|
||||
|
||||
useEffect(() => () => {
|
||||
document.body.style.overflow = '';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setExpanded(false);
|
||||
}, [visible]);
|
||||
|
||||
useClickOutside(refs, () => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('z-[101] transition-opacity duration-100', {
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
})}
|
||||
ref={refs.setFloating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: 'max-content',
|
||||
}}
|
||||
>
|
||||
{expanded ? (
|
||||
<EmojiPickerDropdown
|
||||
visible={expanded}
|
||||
setVisible={setExpanded}
|
||||
update={update}
|
||||
withCustom={customEmojiReacts}
|
||||
onPickEmoji={handlePickEmoji}
|
||||
/>
|
||||
) : (
|
||||
<HStack
|
||||
className={clsx('z-[999] flex w-max max-w-[100vw] flex-wrap space-x-3 rounded-full bg-white px-3 py-2.5 shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700')}
|
||||
>
|
||||
{Array.from(plFeConfig.allowedEmoji).map((emoji) => (
|
||||
<EmojiButton
|
||||
key={emoji}
|
||||
emoji={emoji.replace(/^\\/, '')}
|
||||
onClick={onReact}
|
||||
tabIndex={visible ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{all && (
|
||||
<IconButton
|
||||
className='text-gray-600 hover:text-gray-600 dark:hover:text-white'
|
||||
src={require('@tabler/icons/outline/dots.svg')}
|
||||
onClick={handleExpand}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { EmojiSelector as default };
|
|
@ -18,7 +18,6 @@ export { default as Counter } from './counter/counter';
|
|||
export { default as Datepicker } from './datepicker/datepicker';
|
||||
export { default as Divider } from './divider/divider';
|
||||
export { default as Emoji } from './emoji/emoji';
|
||||
export { default as EmojiSelector } from './emoji-selector/emoji-selector';
|
||||
export { default as FileInput } from './file-input/file-input';
|
||||
export { default as Form } from './form/form';
|
||||
export { default as FormActions } from './form-actions/form-actions';
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useFloating, shift, flip } from '@floating-ui/react';
|
||||
import clsx from 'clsx';
|
||||
import React, { KeyboardEvent, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -13,7 +12,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const EmojiPickerDropdownContainer = (
|
||||
props: Pick<IEmojiPickerDropdown, 'onPickEmoji' | 'condensed' | 'withCustom'>,
|
||||
{ children, ...props }: Pick<IEmojiPickerDropdown, 'onPickEmoji' | 'condensed' | 'withCustom'> & { children?: JSX.Element },
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
|
@ -27,27 +26,41 @@ const EmojiPickerDropdownContainer = (
|
|||
setVisible(false);
|
||||
});
|
||||
|
||||
const handleToggle = (e: MouseEvent | KeyboardEvent) => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setVisible(!visible);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (['Enter', ' '].includes(e.key)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setVisible(!visible);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<IconButton
|
||||
className={clsx({
|
||||
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||
})}
|
||||
ref={refs.setReference}
|
||||
src={require('@tabler/icons/outline/mood-happy.svg')}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
aria-expanded={visible}
|
||||
role='button'
|
||||
onClick={handleToggle as any}
|
||||
onKeyDown={handleToggle as React.KeyboardEventHandler<HTMLButtonElement>}
|
||||
tabIndex={0}
|
||||
/>
|
||||
{children ? (
|
||||
React.cloneElement(children, {
|
||||
onClick: handleClick,
|
||||
onKeyDown: handleKeyDown,
|
||||
ref: refs.setReference,
|
||||
})
|
||||
) : (
|
||||
<IconButton
|
||||
className='emoji-picker-dropdown text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
|
||||
ref={refs.setReference}
|
||||
src={require('@tabler/icons/outline/mood-happy.svg')}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
aria-expanded={visible}
|
||||
role='button'
|
||||
onClick={handleClick as any}
|
||||
onKeyDown={handleKeyDown as React.KeyboardEventHandler<HTMLButtonElement>}
|
||||
tabIndex={0}
|
||||
/>)}
|
||||
|
||||
<Portal>
|
||||
<div
|
||||
|
|
|
@ -6,6 +6,7 @@ import Account from 'pl-fe/components/account';
|
|||
import StatusContent from 'pl-fe/components/status-content';
|
||||
import StatusLanguagePicker from 'pl-fe/components/status-language-picker';
|
||||
import StatusMedia from 'pl-fe/components/status-media';
|
||||
import StatusReactionsBar from 'pl-fe/components/status-reactions-bar';
|
||||
import StatusReplyMentions from 'pl-fe/components/status-reply-mentions';
|
||||
import SensitiveContentOverlay from 'pl-fe/components/statuses/sensitive-content-overlay';
|
||||
import StatusInfo from 'pl-fe/components/statuses/status-info';
|
||||
|
@ -133,6 +134,8 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<StatusReactionsBar status={actualStatus} />
|
||||
|
||||
<HStack justifyContent='between' alignItems='center' className='py-3' wrap>
|
||||
<StatusInteractionBar status={actualStatus} />
|
||||
|
||||
|
|
|
@ -4,14 +4,13 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import { openModal } from 'pl-fe/actions/modals';
|
||||
import AnimatedNumber from 'pl-fe/components/animated-number';
|
||||
import { HStack, Text, Emoji } from 'pl-fe/components/ui';
|
||||
import { HStack, Text } from 'pl-fe/components/ui';
|
||||
import { useAppSelector, useFeatures, useAppDispatch } from 'pl-fe/hooks';
|
||||
import { reduceEmoji } from 'pl-fe/utils/emoji-reacts';
|
||||
|
||||
import type { Status } from 'pl-fe/normalizers';
|
||||
|
||||
interface IStatusInteractionBar {
|
||||
status: Pick<Status, 'id' | 'account' | 'dislikes_count' | 'emoji_reactions' | 'favourited' | 'favourites_count' | 'reblogs_count' | 'quotes_count'>;
|
||||
status: Pick<Status, 'id' | 'account' | 'dislikes_count' | 'favourited' | 'favourites_count' | 'reblogs_count' | 'quotes_count'>;
|
||||
}
|
||||
|
||||
const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.Element | null => {
|
||||
|
@ -38,16 +37,6 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
dispatch(openModal('DISLIKES', { statusId }));
|
||||
};
|
||||
|
||||
const onOpenReactionsModal = (username: string, statusId: string): void => {
|
||||
dispatch(openModal('REACTIONS', { statusId }));
|
||||
};
|
||||
|
||||
const getNormalizedReacts = () => reduceEmoji(
|
||||
status.emoji_reactions,
|
||||
status.favourites_count,
|
||||
status.favourited,
|
||||
);
|
||||
|
||||
const handleOpenReblogsModal: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -135,49 +124,11 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
return null;
|
||||
};
|
||||
|
||||
const handleOpenReactionsModal = () => {
|
||||
if (!me) {
|
||||
return onOpenUnauthorizedModal();
|
||||
}
|
||||
|
||||
onOpenReactionsModal(account.acct, status.id);
|
||||
};
|
||||
|
||||
const getEmojiReacts = () => {
|
||||
const emojiReacts = getNormalizedReacts();
|
||||
const count = emojiReacts.reduce((acc, cur) => (
|
||||
acc + (cur.count || 0)
|
||||
), 0);
|
||||
|
||||
const handleClick = features.emojiReacts ? handleOpenReactionsModal : handleOpenFavouritesModal;
|
||||
|
||||
if (count) {
|
||||
return (
|
||||
<InteractionCounter count={count} onClick={features.exposableReactions ? handleClick : undefined}>
|
||||
<HStack space={0.5} alignItems='center'>
|
||||
{emojiReacts.slice(0, 3).map((e, i) => {
|
||||
return (
|
||||
<Emoji
|
||||
key={i}
|
||||
className='h-4.5 w-4.5 flex-none'
|
||||
emoji={e.name}
|
||||
src={e.url}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
</InteractionCounter>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack space={3}>
|
||||
{getReposts()}
|
||||
{getQuotes()}
|
||||
{(features.emojiReacts || features.emojiReactsMastodon) ? getEmojiReacts() : getFavourites()}
|
||||
{getFavourites()}
|
||||
{getDislikes()}
|
||||
</HStack>
|
||||
);
|
||||
|
@ -203,7 +154,6 @@ const InteractionCounter: React.FC<IInteractionCounter> = ({ count, children, on
|
|||
<HStack space={1} alignItems='center'>
|
||||
<Text weight='bold'>
|
||||
<AnimatedNumber value={count} short />
|
||||
{/* {shortNumberFormat(count)} */}
|
||||
</Text>
|
||||
|
||||
<Text tag='div' theme='muted'>
|
||||
|
|
|
@ -120,8 +120,7 @@ const Thread: React.FC<IThread> = ({
|
|||
|
||||
const handleHotkeyReact = () => {
|
||||
if (statusRef.current) {
|
||||
const firstEmoji: HTMLButtonElement | null = statusRef.current.querySelector('.emoji-react-selector .emoji-react-selector__emoji');
|
||||
firstEmoji?.focus();
|
||||
(node.current?.querySelector('.emoji-picker-dropdown') as HTMLButtonElement)?.click();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@ import { List as ImmutableList } from 'immutable';
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchFavourites, fetchReactions } from 'pl-fe/actions/interactions';
|
||||
import { fetchReactions } from 'pl-fe/actions/interactions';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import { Emoji, Modal, Spinner, Tabs } from 'pl-fe/components/ui';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'pl-fe/hooks';
|
||||
import { ReactionRecord } from 'pl-fe/reducers/user-lists';
|
||||
|
||||
import type { BaseModalProps } from '../modal-root';
|
||||
import type { Item } from 'pl-fe/components/ui/tabs/tabs';
|
||||
|
@ -32,16 +31,7 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
|
|||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const [reaction, setReaction] = useState(initialReaction);
|
||||
const reactions = useAppSelector<ImmutableList<ReturnType<typeof ReactionRecord>> | undefined>((state) => {
|
||||
const favourites = state.user_lists.favourited_by.get(statusId)?.items;
|
||||
const reactions = state.user_lists.reactions.get(statusId)?.items;
|
||||
return favourites && reactions && ImmutableList(favourites?.size ? [ReactionRecord({ accounts: favourites, count: favourites.size, name: '👍' })] : []).concat(reactions || []);
|
||||
});
|
||||
|
||||
const fetchData = () => {
|
||||
dispatch(fetchFavourites(statusId));
|
||||
dispatch(fetchReactions(statusId));
|
||||
};
|
||||
const reactions = useAppSelector((state) => state.user_lists.reactions.get(statusId)?.items);
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('REACTIONS');
|
||||
|
@ -83,7 +73,7 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
|
|||
}, [reactions, reaction]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
dispatch(fetchReactions(statusId));
|
||||
}, []);
|
||||
|
||||
let body;
|
||||
|
|
|
@ -1475,13 +1475,8 @@
|
|||
"status.pinned": "Pinned post",
|
||||
"status.quote": "Quote post",
|
||||
"status.quote_tombstone": "Post is unavailable.",
|
||||
"status.reactions.cry": "Sad",
|
||||
"status.reactions.empty": "No one has reacted to this post yet. When someone does, they will show up here.",
|
||||
"status.reactions.heart": "Love",
|
||||
"status.reactions.laughing": "Haha",
|
||||
"status.reactions.like": "Like",
|
||||
"status.reactions.open_mouth": "Wow",
|
||||
"status.reactions.weary": "Weary",
|
||||
"status.reactions.label": "{count} {count, plural, one {person} other {people}} reacted with {emoji}",
|
||||
"status.read_more": "Read more",
|
||||
"status.reblog": "Repost",
|
||||
"status.reblog_private": "Repost to original audience",
|
||||
|
@ -1512,6 +1507,7 @@
|
|||
"status.unbookmarked": "Bookmark removed.",
|
||||
"status.unmute_conversation": "Unmute conversation",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"status.view_reactions": "View reactions",
|
||||
"status.visibility.direct": "The post is only visible to mentioned users",
|
||||
"status.visibility.list": "The post is only visible to the members of a list",
|
||||
"status.visibility.list.named": "The post is only visible to the members of a {name} list",
|
||||
|
|
|
@ -86,14 +86,6 @@ const PlFeConfigRecord = ImmutableRecord({
|
|||
navlinks: ImmutableMap({
|
||||
homeFooter: ImmutableList<FooterItem>(),
|
||||
}),
|
||||
allowedEmoji: ImmutableList<string>([
|
||||
'👍',
|
||||
'❤️',
|
||||
'😆',
|
||||
'😮',
|
||||
'😢',
|
||||
'😩',
|
||||
]),
|
||||
verifiedIcon: '',
|
||||
displayFqn: true,
|
||||
cryptoAddresses: ImmutableList<CryptoAddress>(),
|
||||
|
|
|
@ -1,56 +1,8 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import { emojiReactionSchema, type EmojiReaction } from 'pl-api';
|
||||
|
||||
import type { Status } from 'pl-fe/normalizers';
|
||||
|
||||
// https://emojipedia.org/facebook
|
||||
// I've customized them.
|
||||
const ALLOWED_EMOJI = ImmutableList([
|
||||
'👍',
|
||||
'❤️',
|
||||
'😆',
|
||||
'😮',
|
||||
'😢',
|
||||
'😩',
|
||||
]);
|
||||
|
||||
const sortEmoji = (emojiReacts: Array<EmojiReaction>): Array<EmojiReaction> =>
|
||||
emojiReacts.toSorted(emojiReact => -(emojiReact.count || 0));
|
||||
|
||||
const mergeEmojiFavourites = (emojiReacts: Array<EmojiReaction> | null, favouritesCount: number, favourited: boolean) => {
|
||||
if (!emojiReacts) return [emojiReactionSchema.parse({ count: favouritesCount, me: favourited, name: '👍' })];
|
||||
if (!favouritesCount) return emojiReacts;
|
||||
const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.name === '👍');
|
||||
if (likeIndex > -1) {
|
||||
const likeCount = Number(emojiReacts[likeIndex].count);
|
||||
favourited = favourited || Boolean(emojiReacts[likeIndex].me || false);
|
||||
return emojiReacts.map((reaction, index) => index === likeIndex ? {
|
||||
...reaction,
|
||||
count: likeCount + favouritesCount,
|
||||
me: favourited,
|
||||
} : reaction);
|
||||
} else {
|
||||
return [...emojiReacts, emojiReactionSchema.parse({ count: favouritesCount, me: favourited, name: '👍' })];
|
||||
}
|
||||
};
|
||||
|
||||
const reduceEmoji = (emojiReacts: Array<EmojiReaction> | null, favouritesCount: number, favourited: boolean): Array<EmojiReaction> => (
|
||||
sortEmoji(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)));
|
||||
|
||||
const getReactForStatus = (
|
||||
status: Pick<Status, 'emoji_reactions' | 'favourited' | 'favourites_count'>,
|
||||
): EmojiReaction | undefined => {
|
||||
if (!status.emoji_reactions) return;
|
||||
|
||||
const result = reduceEmoji(
|
||||
status.emoji_reactions,
|
||||
status.favourites_count || 0,
|
||||
status.favourited,
|
||||
).filter(e => e.me === true)[0];
|
||||
|
||||
return typeof result?.name === 'string' ? result : undefined;
|
||||
};
|
||||
|
||||
const simulateEmojiReact = (emojiReacts: Array<EmojiReaction>, emoji: string, url?: string) => {
|
||||
const idx = emojiReacts.findIndex(e => e.name === emoji);
|
||||
const emojiReact = emojiReacts[idx];
|
||||
|
@ -92,11 +44,7 @@ const simulateUnEmojiReact = (emojiReacts: Array<EmojiReaction>, emoji: string)
|
|||
};
|
||||
|
||||
export {
|
||||
ALLOWED_EMOJI,
|
||||
sortEmoji,
|
||||
mergeEmojiFavourites,
|
||||
reduceEmoji,
|
||||
getReactForStatus,
|
||||
simulateEmojiReact,
|
||||
simulateUnEmojiReact,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue