UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

293 lines (290 loc) 14.2 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { Clock, Image, Video, File, Download, Heart, Reply, MoreHorizontal, AlertCircle, Check, CheckCheck } from 'lucide-react'; import { useState, useRef, useEffect, useCallback } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { GlassButton } from '../button/GlassButton.js'; import '../button/GlassFab.js'; import '../button/GlassMagneticButton.js'; import { CardContent } from '../card/index.js'; import { GlassCard } from '../card/GlassCard.js'; /** * GlassMessageList component * A scrollable list of chat messages with reactions, replies, and attachments */ const GlassMessageList = ({ messages, currentUserId, enableReactions = true, enableReplies = true, showMessageStatus = true, showTimestamps = true, showAvatars = true, enableSearch = false, virtualScroll = false, onMessageClick, onMessageReaction, onMessageReply, onAttachmentDownload, className, ...props }) => { const prefersReducedMotion = useReducedMotion(); const [selectedMessage, setSelectedMessage] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [showSearch, setShowSearch] = useState(false); const messagesEndRef = useRef(null); // Auto-scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Handle message click const handleMessageClick = useCallback(message => { setSelectedMessage(selectedMessage === message.id ? null : message.id); onMessageClick?.(message); }, [selectedMessage, onMessageClick]); // Handle reaction const handleReaction = useCallback((messageId, emoji) => { onMessageReaction?.(messageId, emoji); }, [onMessageReaction]); // Handle reply const handleReply = useCallback(messageId => { onMessageReply?.(messageId); }, [onMessageReply]); // Handle attachment download const handleAttachmentDownload = useCallback(attachment => { onAttachmentDownload?.(attachment); }, [onAttachmentDownload]); // Format timestamp const formatTimestamp = useCallback(date => { const now = new Date(); const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); if (minutes < 1) return "now"; if (minutes < 60) return `${minutes}m`; if (hours < 24) return `${hours}h`; return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }, []); // Filter messages based on search const filteredMessages = searchQuery ? messages.filter(message => message.content.toLowerCase().includes(searchQuery.toLowerCase()) || message.sender.name.toLowerCase().includes(searchQuery.toLowerCase())) : messages; // Group messages by date const groupedMessages = filteredMessages.reduce((groups, message) => { const date = message.timestamp.toDateString(); if (!groups[date]) { groups[date] = []; } groups[date].push(message); return groups; }, {}); return jsx(MotionFramer, { "data-glass-component": true, preset: "fadeIn", className: "glass-w-full glass-h-full", children: jsxs(GlassCard, { className: cn("flex flex-col h-full overflow-hidden", className), ...props, children: [enableSearch && showSearch && jsx("div", { className: "glass-p-4 glass-border-b glass-border-white/10", children: jsx("input", { type: "text", placeholder: "Search messages...", value: searchQuery, onChange: e => setSearchQuery(e.target.value), className: 'glass-w-full bg-glass-fill ring-1 ring-white/10 glass-radius-lg glass-px-4 glass-py-2 text-primary placeholder-white/50 focus:outline-none focus:ring-white/30 glass-focus glass-touch-target glass-contrast-guard' }) }), jsxs(CardContent, { className: 'glass-flex-1 overflow-y-auto glass-p-4', spacing: "lg", children: [Object.entries(groupedMessages).map(([date, dateMessages]) => jsxs("div", { children: [jsx("div", { className: "glass-flex glass-items-center glass-justify-center glass-my-6", children: jsx("div", { className: "glass-px-3 glass-py-1 glass-surface-subtle/10 glass-radius-full", children: jsx("span", { className: 'text-primary/60 glass-text-xs', children: new Date(date).toLocaleDateString() }) }) }), jsx("div", { className: "glass-auto-gap glass-auto-gap-md", children: dateMessages.map((message, index) => { const isCurrentUser = message.sender.id === currentUserId; const isSelected = selectedMessage === message.id; return jsx("div", { className: cn("group relative cursor-pointer glass-focus glass-touch-target", !prefersReducedMotion && "transition-all duration-200 animate-slide-in-up", isSelected && "ring-2 ring-primary glass-radius-lg"), style: { animationDelay: `${Math.min(index, 20) * 20}ms`, animationFillMode: "both" }, onClick: () => handleMessageClick(message), children: jsxs("div", { className: cn("flex glass-gap-3 glass-p-3 glass-radius-lg transition-all duration-200", isSelected ? "bg-primary/20" : "hover:bg-white/5"), children: [showAvatars && jsx("div", { className: "glass-flex-shrink-0", children: jsx("div", { className: 'w-10 h-10 glass-radius-full glass-surface-subtle/20 glass-flex glass-items-center glass-justify-center', children: message.sender.avatar ? jsx("img", { src: message.sender.avatar, alt: message.sender.name, className: 'glass-w-full glass-h-full glass-radius-full object-cover' }) : jsx("span", { className: 'text-primary/80 glass-text-sm font-medium', children: message.sender.name.charAt(0).toUpperCase() }) }) }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsxs("div", { className: 'glass-flex glass-items-center glass-gap-2 mb-1', children: [jsx("span", { className: 'text-primary font-medium glass-text-sm', children: message.sender.name }), message.sender.status && jsx("div", { className: cn("w-2 h-2 glass-radius-full", message.sender.status === "online" ? "bg-green-400" : message.sender.status === "away" ? "bg-yellow-400" : message.sender.status === "busy" ? "bg-red-400" : "bg-gray-400") }), showTimestamps && jsxs("span", { className: 'text-primary/60 glass-text-xs glass-flex glass-items-center glass-gap-1', children: [jsx(Clock, { className: 'w-3 h-3' }), formatTimestamp(message.timestamp)] }), message.edited && jsx("span", { className: 'text-primary/50 glass-text-xs', children: "(edited)" })] }), jsx("div", { className: 'text-primary/90 glass-text-sm leading-relaxed', children: message.content }), message.attachments && message.attachments.length > 0 && jsx("div", { className: 'mt-3 glass-auto-gap glass-auto-gap-sm', children: message.attachments.map((attachment, attIndex) => jsxs("div", { className: 'glass-flex glass-items-center glass-gap-3 glass-p-3 glass-surface-dark/20 glass-radius-lg hover:glass-surface-dark/30 transition-colors cursor-pointer glass-border glass-border-white/10 hover:border-white/20 glass-focus glass-touch-target glass-contrast-guard', onClick: e => { e.stopPropagation(); handleAttachmentDownload({ url: attachment.url, name: attachment.name }); }, children: [jsxs("div", { className: "glass-flex-shrink-0", children: [attachment.type === "image" && jsx(Image, { className: 'w-5 h-5 text-primary' }), attachment.type === "video" && jsx(Video, { className: 'w-5 h-5 text-primary' }), attachment.type === "file" && jsx(File, { className: 'w-5 h-5 text-primary' })] }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsx("p", { className: 'text-primary/90 glass-text-sm truncate', children: attachment.name }), attachment.size && jsxs("p", { className: 'text-primary/60 glass-text-xs', children: [(attachment.size / 1024 / 1024).toFixed(1), " ", "MB"] })] }), jsx(GlassButton, { variant: "ghost", size: "sm", className: "glass-p-1", children: jsx(Download, { className: 'w-4 h-4' }) })] }, attIndex)) }), message.reactions && message.reactions.length > 0 && jsx("div", { className: "glass-flex glass-gap-1 glass-mt-2", children: message.reactions.map((reaction, reactionIndex) => jsxs(GlassButton, { variant: "ghost", size: "sm", onClick: e => { e.stopPropagation(); handleReaction(message.id, reaction.emoji); }, className: 'h-6 glass-px-2 glass-text-xs glass-surface-subtle/10 glass-focus glass-touch-target', children: [reaction.emoji, " ", reaction.count] }, reactionIndex)) })] }), jsx("div", { className: 'glass-flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity', children: jsxs("div", { className: "glass-flex glass-flex-col glass-gap-1", children: [enableReactions && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => { e.stopPropagation(); handleReaction(message.id, "👍"); }, className: "glass-p-1 glass-focus glass-touch-target", children: jsx(Heart, { className: 'w-3 h-3' }) }), enableReplies && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => { e.stopPropagation(); handleReply(message.id); }, className: "glass-p-1 glass-focus glass-touch-target", children: jsx(Reply, { className: 'w-3 h-3' }) }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => e.stopPropagation(), className: "glass-p-1 glass-focus glass-touch-target", children: jsx(MoreHorizontal, { className: 'w-3 h-3' }) })] }) }), showMessageStatus && isCurrentUser && jsx("div", { className: "glass-flex-shrink-0 glass-ml-2", children: message.type === "system" ? jsx(AlertCircle, { className: 'w-4 h-4 text-primary' }) : jsxs("div", { className: "glass-flex", children: [jsx(Check, { className: 'w-3 h-3 text-primary/60' }), jsx(CheckCheck, { className: 'w-3 h-3 text-primary -glass-ml-1' })] }) })] }) }, message.id); }) })] }, date)), jsx("div", { ref: messagesEndRef })] }), enableSearch && jsx("div", { className: "glass-p-4 glass-border-t glass-border-white/10", children: jsx(GlassButton, { variant: "ghost", size: "sm", onClick: () => setShowSearch(!showSearch), className: "glass-w-full glass-focus glass-touch-target", children: showSearch ? "Hide Search" : "Search Messages" }) })] }) }); }; export { GlassMessageList, GlassMessageList as default }; //# sourceMappingURL=GlassMessageList.js.map