nicolium: announcement reactions bar UI consistency

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-06 16:03:34 +01:00
parent 279f1cb483
commit 97ffcd88e2
7 changed files with 141 additions and 113 deletions

View File

@ -85,7 +85,7 @@ const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate, short, ma
});
if (reduceMotion) {
return <>{formattedValue}</>;
return formattedValue;
}
return (

View File

@ -1,6 +1,7 @@
import { animated, type AnimatedProps } from '@react-spring/web';
import clsx from 'clsx';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import AnimatedNumber from '@/components/animated-number';
import unicodeMapping from '@/features/emoji/mapping';
@ -10,6 +11,13 @@ import Emoji from './emoji';
import type { AnnouncementReaction, CustomEmoji } from 'pl-api';
const messages = defineMessages({
emojiCount: {
id: 'status.reactions.label',
defaultMessage: '{count} {count, plural, one {person} other {people}} reacted with {emoji}',
},
});
interface IReaction {
announcementId: string;
reaction: AnnouncementReaction;
@ -18,6 +26,7 @@ interface IReaction {
}
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, style }) => {
const intl = useIntl();
const [hovered, setHovered] = useState(false);
const { addReaction, removeReaction } = useAnnouncements();
@ -46,25 +55,23 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, sty
return (
<animated.button
className={clsx(
'flex shrink-0 items-center gap-1.5 rounded-sm bg-gray-100 px-1.5 py-1 transition-colors dark:bg-primary-900',
{
'bg-gray-200 dark:bg-primary-800': hovered,
'bg-primary-200 dark:bg-primary-500': reaction.me,
},
)}
className={clsx('⁂-status-reactions-bar__button', {
'⁂-status-reactions-bar__button--active': reaction.me,
})}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
title={`:${shortCode}:`}
title={intl.formatMessage(messages.emojiCount, {
emoji: `:${shortCode}:`,
count: reaction.count,
})}
style={style}
>
<span className='block size-4'>
<Emoji hovered={hovered} emoji={reaction.name} emojiMap={emojiMap} />
</span>
<span className='block min-w-[9px] text-center text-xs font-medium text-primary-600 dark:text-white'>
<Emoji hovered={hovered} emoji={reaction.name} emojiMap={emojiMap} />
<p>
<AnimatedNumber value={reaction.count} />
</span>
</p>
</animated.button>
);
};

View File

@ -1,15 +1,22 @@
import { useTransition } from '@react-spring/web';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import EmojiPickerDropdown from '@/features/emoji/containers/emoji-picker-dropdown-container';
import { useAnnouncements } from '@/queries/announcements/use-announcements';
import { useSettings } from '@/stores/settings';
import Icon from '../ui/icon';
import Reaction from './reaction';
import type { Emoji, NativeEmoji } from '@/features/emoji';
import type { AnnouncementReaction, CustomEmoji } from 'pl-api';
const messages = defineMessages({
addEmoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
});
interface IReactionsBar {
announcementId: string;
reactions: Array<AnnouncementReaction>;
@ -17,6 +24,7 @@ interface IReactionsBar {
}
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emojiMap }) => {
const intl = useIntl();
const { reduceMotion } = useSettings();
const { addReaction } = useAnnouncements();
@ -41,7 +49,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emoj
});
return (
<div className='flex flex-wrap items-center gap-1'>
<div className='⁂-status-reactions-bar'>
{transitions(({ scale }, reaction) => (
<Reaction
key={reaction.name}
@ -52,7 +60,16 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emoj
/>
))}
{visibleReactions.length < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
{visibleReactions.length < 8 && (
<EmojiPickerDropdown onPickEmoji={handleEmojiPick}>
<button
className='⁂-status-reactions-bar__picker-button emoji-picker-dropdown'
title={intl.formatMessage(messages.addEmoji)}
>
<Icon src={require('@phosphor-icons/core/regular/smiley-sticker.svg')} aria-hidden />
</button>
</EmojiPickerDropdown>
)}
</div>
);
};

View File

@ -90,7 +90,6 @@ const StatusReaction: React.FC<IStatusReaction> = ({
className={clsx('⁂-status-reactions-bar__button', {
'⁂-status-reactions-bar__button--active': reaction.me,
})}
key={reaction.name}
onClick={handleClick}
title={intl.formatMessage(messages.emojiCount, {
emoji: `:${shortCode}:`,

View File

@ -98,112 +98,110 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
if (!account) return null;
return (
<div className='border-box'>
<div ref={node} className='detailed-actualStatus' tabIndex={-1}>
{renderStatusInfo()}
<div ref={node} className='⁂-detailed-status' tabIndex={-1}>
{renderStatusInfo()}
{actualStatus.rss_feed ? (
<RssFeedInfo feed={actualStatus.rss_feed} timestamp={actualStatus.created_at} />
) : (
<div className='mb-4'>
<Account
key={account.id}
account={account}
avatarSize={42}
hideActions
approvalStatus={actualStatus.approval_status}
/>
</div>
)}
{actualStatus.rss_feed ? (
<RssFeedInfo feed={actualStatus.rss_feed} timestamp={actualStatus.created_at} />
) : (
<div className='mb-4'>
<Account
key={account.id}
account={account}
avatarSize={42}
hideActions
approvalStatus={actualStatus.approval_status}
/>
</div>
)}
<StatusReplyMentions status={actualStatus} />
<StatusReplyMentions status={actualStatus} />
<Stack className='relative z-0'>
<Stack space={4}>
<StatusContent status={actualStatus} textSize='lg' translatable withMedia={withMedia} />
</Stack>
<Stack className='relative z-0'>
<Stack space={4}>
<StatusContent status={actualStatus} textSize='lg' translatable withMedia={withMedia} />
</Stack>
</Stack>
{!status.rss_feed && (
<>
<StatusReactionsBar status={actualStatus} />
{!status.rss_feed && (
<>
<StatusReactionsBar status={actualStatus} />
<HStack space={2} justifyContent='between' alignItems='center' className='py-3' wrap>
<StatusInteractionBar status={actualStatus} />
<HStack space={2} justifyContent='between' alignItems='center' className='py-3' wrap>
<StatusInteractionBar status={actualStatus} />
<HStack space={1} alignItems='center'>
<span>
<Text tag='span' theme='muted' size='sm'>
<HStack space={1} alignItems='center' wrap>
<a
href={actualStatus.url}
target='_blank'
rel='noopener noreferrer'
className='hover:underline'
>
<FormattedDate
value={new Date(actualStatus.created_at)}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
</a>
<HStack space={1} alignItems='center'>
<span>
<Text tag='span' theme='muted' size='sm'>
<HStack space={1} alignItems='center' wrap>
<a
href={actualStatus.url}
target='_blank'
rel='noopener noreferrer'
className='hover:underline'
>
<FormattedDate
value={new Date(actualStatus.created_at)}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
</a>
{actualStatus.application && (
<>
<span className='⁂-separator' />
<a
href={actualStatus.application.website ?? '#'}
target='_blank'
rel='noopener noreferrer'
className='hover:underline'
title={intl.formatMessage(messages.applicationName, {
name: actualStatus.application.name,
})}
>
{actualStatus.application.name}
</a>
</>
)}
{actualStatus.application && (
<>
<span className='⁂-separator' />
<a
href={actualStatus.application.website ?? '#'}
target='_blank'
rel='noopener noreferrer'
className='hover:underline'
title={intl.formatMessage(messages.applicationName, {
name: actualStatus.application.name,
})}
>
{actualStatus.application.name}
</a>
</>
)}
{actualStatus.edited_at && (
<>
<span className='⁂-separator' />
<button
className='inline hover:underline'
onClick={handleOpenCompareHistoryModal}
>
<FormattedMessage
id='status.edited'
defaultMessage='Edited {date}'
values={{
date: intl.formatDate(new Date(actualStatus.edited_at), {
hour12: true,
month: 'short',
day: '2-digit',
hour: 'numeric',
minute: '2-digit',
}),
}}
/>
</button>
</>
)}
</HStack>
</Text>
</span>
{actualStatus.edited_at && (
<>
<span className='⁂-separator' />
<button
className='inline hover:underline'
onClick={handleOpenCompareHistoryModal}
>
<FormattedMessage
id='status.edited'
defaultMessage='Edited {date}'
values={{
date: intl.formatDate(new Date(actualStatus.edited_at), {
hour12: true,
month: 'short',
day: '2-digit',
hour: 'numeric',
minute: '2-digit',
}),
}}
/>
</button>
</>
)}
</HStack>
</Text>
</span>
<StatusTypeIcon visibility={actualStatus.visibility} />
<StatusTypeIcon visibility={actualStatus.visibility} />
<StatusLanguagePicker status={actualStatus} showLabel />
</HStack>
<StatusLanguagePicker status={actualStatus} showLabel />
</HStack>
</>
)}
</div>
</HStack>
</>
)}
</div>
);
};

View File

@ -303,7 +303,7 @@ const Thread = ({
});
setTimeout(() => {
(node.current?.querySelector('.detailed-actualStatus') as HTMLDivElement)?.focus();
(node.current?.querySelector('.⁂-detailed-status') as HTMLDivElement)?.focus();
}, 100);
}, 0);
}, [status.id, statusIndex]);

View File

@ -20,8 +20,15 @@
@apply mb-1 block text-sm text-gray-700 dark:text-gray-600;
}
.-status,
.-detailed-status {
.-status-reactions-bar {
padding-top: 0.5rem;
}
}
.-status-reactions-bar {
@apply flex gap-2 flex-wrap pt-2;
@apply flex gap-2 flex-wrap;
&__button {
@apply flex cursor-pointer items-center gap-2 overflow-hidden rounded-md border border-gray-400 px-1.5 py-1 transition-all bg-transparent dark:border-primary-700 dark:bg-primary-700 black:border-primary-800 black:bg-primary-800;