fix(message): give the action rail custom stroke icons, drop its inline reactions, and lift it above the bubble

This commit is contained in:
heaven 2026-06-03 13:40:05 +03:00
parent 78f9b84850
commit ebc7ec87f0
2 changed files with 72 additions and 53 deletions

View file

@ -116,45 +116,69 @@ export const MessageQuickReactions = as<'div', MessageQuickReactionsProps>(
}
);
// Default reactions shown in the hover rail before a user has built up any
// recent-emoji history, so the "быстрый рельс" always offers one-tap reactions.
const RAIL_DEFAULT_REACTIONS: { unicode: string; shortcode: string }[] = [
{ unicode: '👍', shortcode: 'thumbsup' },
{ unicode: '❤️', shortcode: 'heart' },
{ unicode: '😂', shortcode: 'joy' },
];
type RailQuickReactionsProps = {
onReaction: (key: string, shortcode?: string) => void;
};
// Inline quick-reactions surfaced directly in the hover action rail — react in
// one tap without opening the emoji board. Mounts ONLY inside the
// conditionally-rendered rail (one row at a time), so there is no per-row cost.
function RailQuickReactions({ onReaction }: RailQuickReactionsProps) {
const mx = useMatrixClient();
const recent = useRecentEmoji(mx, 3);
const items =
recent.length > 0
? recent.slice(0, 3).map((e) => ({ unicode: e.unicode, shortcode: e.shortcode }))
: RAIL_DEFAULT_REACTIONS;
return (
// Hover action-rail icons — thin stroke-outline style matching the composer's
// StreamComposerIcons (Dawn canon), replacing folds' stock filled glyphs so the
// rail reads distinct from upstream Cinny. folds Icon wraps these in
// `<svg viewBox="0 0 24 24" fill="none">`, so each returns just the shapes.
const RailIcons = {
Smile: () => (
<>
{items.map((emoji) => (
<IconButton
key={emoji.unicode}
size="300"
variant="SurfaceVariant"
radii="300"
title={emoji.shortcode}
aria-label={emoji.shortcode}
onClick={() => onReaction(emoji.unicode, emoji.shortcode)}
>
<Text size="T400">{emoji.unicode}</Text>
</IconButton>
))}
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6" />
<path
d="M8 14s1.5 2 4 2 4-2 4-2M9 10h.01M15 10h.01"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</>
);
}
),
Reply: () => (
<>
<path
d="M9 17L4 12l5-5"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20 18v-2a4 4 0 0 0-4-4H4"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
),
Thread: () => (
<>
<path
d="M5 4h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H8l-4 4V5a1 1 0 0 1 1-1z"
stroke="currentColor"
strokeWidth="1.7"
strokeLinejoin="round"
/>
<path d="M12 7.5v4M10 9.5h4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
</>
),
Edit: () => (
<path
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
/>
),
More: () => (
<path
d="M5 12h.01M12 12h.01M19 12h.01"
stroke="currentColor"
strokeWidth="2.6"
strokeLinecap="round"
/>
),
};
export const MessageAllReactionItem = as<
'button',
@ -954,14 +978,6 @@ const MessageInner = as<'div', MessageProps>(
<div className={css.MessageOptionsBase}>
<Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
<Box gap="100">
{canSendReaction && (
<RailQuickReactions
onReaction={(key, shortcode) => {
const evtId = mEvent.getId();
if (evtId) onReactionToggle(evtId, key, shortcode);
}}
/>
)}
{canSendReaction && (
<PopOut
position="Bottom"
@ -996,7 +1012,7 @@ const MessageInner = as<'div', MessageProps>(
radii="300"
aria-pressed={!!emojiBoardAnchor}
>
<Icon src={Icons.SmilePlus} size="100" />
<Icon src={RailIcons.Smile} size="100" />
</IconButton>
</PopOut>
)}
@ -1008,7 +1024,7 @@ const MessageInner = as<'div', MessageProps>(
size="300"
radii="300"
>
<Icon src={Icons.ReplyArrow} size="100" />
<Icon src={RailIcons.Reply} size="100" />
</IconButton>
)}
{!isThreadReply && !hideThreadReplyAffordance && (
@ -1019,7 +1035,7 @@ const MessageInner = as<'div', MessageProps>(
size="300"
radii="300"
>
<Icon src={Icons.ThreadPlus} size="100" />
<Icon src={RailIcons.Thread} size="100" />
</IconButton>
)}
{canEditEvent(mx, mEvent) && onEditId && (
@ -1029,7 +1045,7 @@ const MessageInner = as<'div', MessageProps>(
size="300"
radii="300"
>
<Icon src={Icons.Pencil} size="100" />
<Icon src={RailIcons.Edit} size="100" />
</IconButton>
)}
<PopOut
@ -1198,7 +1214,7 @@ const MessageInner = as<'div', MessageProps>(
onClick={handleOpenMenu}
aria-pressed={!!menuAnchor}
>
<Icon src={Icons.VerticalDots} size="100" />
<Icon src={RailIcons.More} size="100" />
</IconButton>
</PopOut>
</Box>
@ -1540,7 +1556,7 @@ export const Event = as<'div', EventProps>(
onClick={handleOpenMenu}
aria-pressed={!!menuAnchor}
>
<Icon src={Icons.VerticalDots} size="100" />
<Icon src={RailIcons.More} size="100" />
</IconButton>
</PopOut>
</Box>

View file

@ -11,10 +11,13 @@ export const MessageOptionsBase = style([
position: 'absolute',
// Hug the message's top-right corner with a small overlap instead of the old
// -30px overhang that detached the rail from the row and clipped at the
// timeline top. (First iteration — exact offset is easy to nudge.)
// timeline top. (Exact offset is easy to nudge.)
top: toRem(-14),
right: config.space.S200,
zIndex: 1,
// Above the message content/bubble (which sits at zIndex 2 in layout.css.ts)
// so the rail never renders behind the bubble — was the "за баблом" bug on
// native where the rail overlaps the bubble's top-right.
zIndex: 5,
},
]);
export const MessageOptionsBar = style([