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:
@@ -29,6 +29,7 @@ import React, {
|
||||
MouseEventHandler,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
@@ -58,7 +59,8 @@ import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
import * as css from './styles.css';
|
||||
import { SendingSpinClass } from '../../../styles/Animations.css';
|
||||
import { MsgAppearClass, SendingSpinClass } from '../../../styles/Animations.css';
|
||||
import { MentionHighlightPulse } from '../../../components/message/layout/layout.css';
|
||||
import { EventReaders } from '../../../components/event-readers';
|
||||
import { ReadReceiptAvatars } from '../../../components/read-receipt-avatars';
|
||||
import { useReadPositions } from '../ReadPositionsContext';
|
||||
@@ -787,6 +789,19 @@ export const Message = React.memo(
|
||||
: (readPositions.get(mEvent.getId() ?? '') ?? []);
|
||||
const isMine = mEvent.getSender() === mx.getUserId();
|
||||
const lotusTerminal = lotusTerminalProp;
|
||||
// Track whether this message should play the appear animation (own messages only)
|
||||
const isNewRef = useRef(true);
|
||||
const [playAppear, setPlayAppear] = useState(isMine && isNewRef.current);
|
||||
// Mention pulse: play once for new incoming @mention messages from others
|
||||
const myUserId = mx.getUserId() ?? '';
|
||||
const mentionContent = mEvent.getContent<{
|
||||
'm.mentions'?: { user_ids?: string[]; room?: boolean };
|
||||
}>();
|
||||
const isMentioned =
|
||||
!isMine &&
|
||||
(mentionContent['m.mentions']?.user_ids?.includes(myUserId) === true ||
|
||||
mentionContent['m.mentions']?.room === true);
|
||||
const [playMentionPulse, setPlayMentionPulse] = useState(isMentioned && isNewRef.current);
|
||||
const [hover, setHover] = useState(false);
|
||||
const { hoverProps } = useHover({ onHoverChange: setHover });
|
||||
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
|
||||
@@ -956,12 +971,24 @@ export const Message = React.memo(
|
||||
<MessageBase
|
||||
className={classNames(css.MessageBase, className, {
|
||||
[css.MessageBaseBubbleCollapsed]: messageLayout === MessageLayout.Bubble && collapse,
|
||||
[MsgAppearClass]: playAppear,
|
||||
[MentionHighlightPulse]: playMentionPulse,
|
||||
})}
|
||||
tabIndex={0}
|
||||
space={messageSpacing}
|
||||
collapse={collapse}
|
||||
highlight={highlight}
|
||||
selected={!!menuAnchor || !!emojiBoardAnchor}
|
||||
onAnimationEnd={() => {
|
||||
if (playAppear) {
|
||||
isNewRef.current = false;
|
||||
setPlayAppear(false);
|
||||
}
|
||||
if (playMentionPulse) {
|
||||
isNewRef.current = false;
|
||||
setPlayMentionPulse(false);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
{...hoverProps}
|
||||
{...focusWithinProps}
|
||||
|
||||
Reference in New Issue
Block a user