bigbuffet-rw/packages/pl-fe/src/components/status-reactions-bar.tsx
marcin mikołajczak 1f001abe9e pl-fe: add feedback to long press
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-09-12 18:03:25 +02:00

140 lines
5.1 KiB
TypeScript

import { useLongPress } from '@uidotdev/usehooks';
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 { openModal } from 'pl-fe/actions/modals';
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
import unicodeMapping from 'pl-fe/features/emoji/mapping';
import { useAppDispatch, useLoggedIn, useSettings } from 'pl-fe/hooks';
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;
unauthenticated?: boolean;
}
const StatusReaction: React.FC<IStatusReaction> = ({ reaction, status, obfuscate, unauthenticated }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const bind = useLongPress((e) => {
if (e.type !== 'touchstart') return;
e.stopPropagation();
if ('vibrate' in navigator) navigator.vibrate(100);
dispatch(openModal('REACTIONS', { statusId: status.id, reaction: reaction.name }));
});
if (!reaction.count) return null;
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.stopPropagation();
if (unauthenticated) {
dispatch(openModal('REACTIONS', { statusId: status.id, reaction: reaction.name }));
} else if (reaction.me) {
dispatch(unEmojiReact(status, reaction.name));
} else {
dispatch(emojiReact(status, reaction.name, reaction.url));
}
};
let shortCode = reaction.name;
// @ts-ignore
if (unicodeMapping[shortCode]?.shortcode) {
// @ts-ignore
shortCode = unicodeMapping[shortCode].shortcode;
}
return (
<button
className={clsx('group flex cursor-pointer items-center gap-2 overflow-hidden rounded-md border border-gray-400 p-1.5 transition-colors', {
'bg-primary-100 dark:border-primary-400 dark:bg-primary-400 black:border-primary-600 black:bg-primary-600': reaction.me,
'bg-transparent dark:border-primary-700 dark:bg-primary-700 black:border-primary-800 black:bg-primary-800': !reaction.me,
'cursor-pointer': !unauthenticated,
'hover:bg-primary-200 hover:dark:border-primary-300 hover:dark:bg-primary-300 hover:black:bg-primary-500': reaction.me && !unauthenticated,
'hover:bg-primary-100 hover:dark:border-primary-600 hover:dark:bg-primary-600 hover:black:bg-primary-700': !reaction.me && !unauthenticated,
})}
key={reaction.name}
onClick={handleClick}
title={intl.formatMessage(messages.emojiCount, {
emoji: `:${shortCode}:`,
count: reaction.count,
})}
disabled={unauthenticated}
{...bind}
>
<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 { me } = useLoggedIn();
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 = status.emoji_reactions.toSorted((a, b) => (b.count || 0) - (a.count || 0));
return (
<HStack className='pt-2' space={2} wrap>
{sortedReactions.map((reaction) => reaction.count ? (
<StatusReaction
key={reaction.name}
status={status}
reaction={reaction}
obfuscate={demetricator}
unauthenticated={!me}
/>
) : null)}
{me && (
<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 black:border-primary-800 black:bg-primary-800 hover:black:bg-primary-700 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 };