aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
293 lines (290 loc) • 14.2 kB
JavaScript
'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