feat: GIF previews, room context menu, policy lists, mention pulse, collapsible messages, send animation, D&D fix

P3-5: Giphy/Tenor URL preview cards — full-width thumbnail from og:image
mxc URL, GIF badge overlay, site badge + title footer; GifCard shared by
both; BadgeGiphy (teal) and BadgeTenor (blue) CSS classes

P3-9: Policy list viewer — read-only panel in Room Settings + Space
Settings (admin/50+ PL only); enter room ID or alias; tabs for Users /
Rooms / Servers; glob pattern warning color; Ban badge; entity + reason

P5-8: Mention highlight pulse — 0.6s scale+glow keyframe on incoming
@mention messages; prefers-reduced-motion aware; only fires on new
incoming messages (isNewRef), not on history load; onAnimationEnd cleanup

P5-19: Collapsible long messages — ResizeObserver clamps text bodies
>320px with gradient fade + "Read more ↓" / "Show less ↑" button; resets
on eventId change; skips images/video/audio/file; smooth CSS transition

P5-23: Message send animation — own messages fade+scale in (0.97→1,
0.4→1 opacity, 150ms ease-out); prefers-reduced-motion aware; one-shot
via isNewRef + onAnimationEnd clear

P5-26: Room context menu — Copy Link (matrix.to URL, 1.5s Copied!
feedback), Mute with duration (15m/1h/8h/24h/indefinite, localStorage
timer key io.lotus.mute_timers), Mark as read; Icons.Link + Icons.BellMute

BUG D&D: dragCounter ref replaces fragile dragState machine — enter
increments, leave decrements (hides at 0), drop resets to 0; fixes
spurious dragleave from child element boundary crossings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 15:51:18 -04:00
co-authored by Claude Sonnet 4.6
parent fbdd0e7083
commit 657ca3a5ca
16 changed files with 979 additions and 88 deletions
@@ -675,3 +675,68 @@ export const PortraitSideLayout = style([
alignItems: 'flex-start',
},
]);
// ---------------------------------------------------------------------------
// GIF card (Giphy / Tenor)
// ---------------------------------------------------------------------------
export const GifThumbnailWrapper = style([
DefaultReset,
{
position: 'relative',
width: '100%',
maxHeight: toRem(200),
overflow: 'hidden',
flexShrink: 0,
backgroundColor: color.Surface.Container,
cursor: 'pointer',
':hover': {
filter: 'brightness(0.9)',
},
},
]);
export const GifThumbnailImg = style([
DefaultReset,
{
width: '100%',
maxHeight: toRem(200),
objectFit: 'cover',
objectPosition: 'center',
display: 'block',
},
]);
export const GifBadge = style([
DefaultReset,
{
position: 'absolute',
top: config.space.S100,
right: config.space.S100,
display: 'inline-flex',
alignItems: 'center',
paddingLeft: config.space.S100,
paddingRight: config.space.S100,
paddingTop: '2px',
paddingBottom: '2px',
borderRadius: config.radii.R300,
fontSize: toRem(11),
fontWeight: 700,
lineHeight: '1.4',
letterSpacing: '0.05em',
backgroundColor: 'rgba(0, 0, 0, 0.65)',
color: '#ffffff',
pointerEvents: 'none',
},
]);
export const BadgeGiphy = style({
backgroundColor: '#00c0c0',
color: '#ffffff',
});
export const BadgeTenor = style({
backgroundColor: '#0078d4',
color: '#ffffff',
});