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:
@@ -14,14 +14,14 @@ export const useFileDropZone = (
|
||||
zoneRef: RefObject<HTMLElement>,
|
||||
onDrop: (file: File[]) => void,
|
||||
): boolean => {
|
||||
const dragStateRef = useRef<'start' | 'leave' | 'over' | undefined>(undefined);
|
||||
const dragCounterRef = useRef(0);
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const target = zoneRef.current;
|
||||
const handleDrop = (evt: DragEvent) => {
|
||||
evt.preventDefault();
|
||||
dragStateRef.current = undefined;
|
||||
dragCounterRef.current = 0;
|
||||
setActive(false);
|
||||
if (!evt.dataTransfer) return;
|
||||
const files = getDataTransferFiles(evt.dataTransfer);
|
||||
@@ -38,18 +38,19 @@ export const useFileDropZone = (
|
||||
const target = zoneRef.current;
|
||||
const handleDragEnter = (evt: DragEvent) => {
|
||||
if (evt.dataTransfer?.types.includes('Files')) {
|
||||
dragStateRef.current = 'start';
|
||||
dragCounterRef.current += 1;
|
||||
setActive(true);
|
||||
}
|
||||
};
|
||||
const handleDragLeave = () => {
|
||||
if (dragStateRef.current !== 'over') return;
|
||||
dragStateRef.current = 'leave';
|
||||
setActive(false);
|
||||
dragCounterRef.current -= 1;
|
||||
if (dragCounterRef.current <= 0) {
|
||||
dragCounterRef.current = 0;
|
||||
setActive(false);
|
||||
}
|
||||
};
|
||||
const handleDragOver = (evt: DragEvent) => {
|
||||
evt.preventDefault();
|
||||
dragStateRef.current = 'over';
|
||||
};
|
||||
|
||||
target?.addEventListener('dragenter', handleDragEnter);
|
||||
|
||||
Reference in New Issue
Block a user