import { useState, useEffect, useRef, useCallback } from 'react' import axios from 'axios' import LazyMedia from './LazyMedia' const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8085/api' const PAGE_SIZE = 50 function Activity({ startDate, endDate }) { const [activities, setActivities] = useState([]) const [loading, setLoading] = useState(true) const [loadingMore, setLoadingMore] = useState(false) const [hasMore, setHasMore] = useState(true) const [offset, setOffset] = useState(0) const observerTarget = useRef(null) const scrollContainerRef = useRef(null) // Reset when date range changes useEffect(() => { setActivities([]) setOffset(0) setHasMore(true) fetchActivity(0, false) }, [startDate, endDate]) const fetchActivity = async (currentOffset, append = false) => { if (append) { setLoadingMore(true) } else { setLoading(true) } try { const params = { limit: PAGE_SIZE, offset: currentOffset } if (startDate) params.start = startDate.toISOString() if (endDate) params.end = endDate.toISOString() const response = await axios.get(`${API_BASE}/activity`, { params }) const newActivities = response.data || [] // If we got fewer items than the page size, we've reached the end if (newActivities.length < PAGE_SIZE) { setHasMore(false) } if (append) { setActivities(prev => [...prev, ...newActivities]) } else { setActivities(newActivities) } } catch (error) { console.error('Error fetching activity:', error) } finally { setLoading(false) setLoadingMore(false) } } const loadMore = useCallback(() => { console.log('loadMore called:', { loadingMore, hasMore, offset }) if (!loadingMore && hasMore) { const newOffset = offset + PAGE_SIZE setOffset(newOffset) fetchActivity(newOffset, true) } }, [offset, loadingMore, hasMore]) // Set up intersection observer for infinite scroll useEffect(() => { // Make sure both refs are available if (!scrollContainerRef.current || !observerTarget.current) { console.log('Refs not ready:', { scroll: !!scrollContainerRef.current, target: !!observerTarget.current }) return } console.log('Setting up IntersectionObserver', { hasMore, loadingMore, activitiesCount: activities.length }) const observer = new IntersectionObserver( (entries) => { console.log('Observer callback fired', { isIntersecting: entries[0].isIntersecting, hasMore, loadingMore }) if (entries[0].isIntersecting && hasMore && !loadingMore) { console.log('Intersection detected, loading more...') loadMore() } }, { root: scrollContainerRef.current, rootMargin: '100px', threshold: 0.1 } ) observer.observe(observerTarget.current) return () => { observer.disconnect() } }, [loadMore, hasMore, loadingMore, activities]) const formatCallType = (type) => { switch (type) { case 1: return { label: 'Incoming call', icon: '📞', color: 'success' } case 2: return { label: 'Outgoing call', icon: '📱', color: 'primary' } case 3: return { label: 'Missed call', icon: '📵', color: 'danger' } case 4: return { label: 'Voicemail', icon: '🎙️', color: 'info' } case 5: return { label: 'Rejected call', icon: '🚫', color: 'warning' } case 6: return { label: 'Refused call', icon: '❌', color: 'danger' } default: return { label: 'Call', icon: '📞', color: 'secondary' } } } const formatDuration = (seconds) => { if (seconds < 60) return `${seconds}s` const minutes = Math.floor(seconds / 60) const secs = seconds % 60 return `${minutes}m ${secs}s` } const formatPhoneNumber = (phoneNumber) => { if (!phoneNumber) return '' // Handle comma-separated numbers (group conversations) if (phoneNumber.includes(',')) { const numbers = phoneNumber.split(',').map(n => n.trim()) return numbers.map(n => formatSinglePhoneNumber(n)).join(', ') } return formatSinglePhoneNumber(phoneNumber) } const formatSinglePhoneNumber = (phoneNumber) => { if (!phoneNumber) return '' // Remove any non-numeric characters except leading + let cleaned = phoneNumber.replace(/[^\d+]/g, '') // Handle +1 prefix (US numbers) if (cleaned.startsWith('+1') && cleaned.length === 12) { // Format as +1 (XXX) XXX-XXXX return `+1 (${cleaned.slice(2, 5)}) ${cleaned.slice(5, 8)}-${cleaned.slice(8)}` } // Handle numbers with + country code if (cleaned.startsWith('+')) { return cleaned // Return international numbers as-is } // Handle 11-digit numbers starting with 1 (US numbers without +) if (cleaned.length === 11 && cleaned.startsWith('1')) { return `+1 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}` } // Handle 10-digit US numbers if (cleaned.length === 10) { return `+1 (${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}` } // Return as-is if format doesn't match return phoneNumber } const formatDate = (dateStr) => { const date = new Date(dateStr) const now = new Date() const diffMs = now - date const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) const timeStr = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }) if (diffDays === 0) return `Today at ${timeStr}` if (diffDays === 1) return `Yesterday at ${timeStr}` if (diffDays < 7) return date.toLocaleDateString('en-US', { weekday: 'long', hour: 'numeric', minute: '2-digit', hour12: true }) return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, hour: 'numeric', minute: '2-digit', hour12: true }) } const getMessageTypeLabel = (type) => { switch (type) { case 1: return { label: 'Received', color: 'primary' } case 2: return { label: 'Sent', color: 'success' } case 3: return { label: 'Draft', color: 'secondary' } case 4: return { label: 'Outbox', color: 'warning' } case 5: return { label: 'Failed', color: 'danger' } case 6: return { label: 'Queued', color: 'info' } default: return { label: 'Message', color: 'secondary' } } } const shouldDisplaySubject = (subject) => { if (!subject) return false // Filter out protocol buffer/RCS subjects if (subject.startsWith('proto:')) return false return true } // Get sender display name for a message in group conversations const getSenderDisplayName = (message) => { // For received messages, use the sender field if available let senderPhone = message.sender // If sender is empty, try to extract from addresses array if (!senderPhone && message.addresses && message.addresses.length > 0) { // Use the first address as the sender senderPhone = message.addresses[0] } // If sender contains comma-separated numbers (shouldn't happen, but handle it), // extract only the first one if (senderPhone && senderPhone.includes(',')) { senderPhone = senderPhone.split(',')[0].trim() } if (!senderPhone) return 'Unknown' // Format as a single phone number (not as a group) return formatSinglePhoneNumber(senderPhone) } if (loading) { return (
Loading...

Loading activity...

) } if (activities.length === 0) { return (

No activity found

) } return (

Activity Timeline {activities.length} items

{activities.map((activity, index) => { if (activity.type === 'message' && activity.message) { const msg = activity.message const msgType = getMessageTypeLabel(msg.type) // For MMS with multiple recipients, use the addresses array; otherwise use the single address let displayAddress if (msg.addresses && msg.addresses.length > 0) { // Format each address and join with commas displayAddress = msg.addresses.map(addr => formatPhoneNumber(addr)).join(', ') } else { // Fall back to the single address field displayAddress = formatPhoneNumber(activity.address) } const displayName = activity.contact_name || displayAddress // Check if this is a group conversation const isGroupConversation = msg.addresses && msg.addresses.length > 1 const isSent = msg.type === 2 const showSenderLabel = isGroupConversation && !isSent // Debug logging for ALL messages to understand what we're receiving console.log('Message received:', { id: msg.id, addresses: msg.addresses, addressesType: typeof msg.addresses, addressesLength: msg.addresses?.length, sender: msg.sender, address: msg.address, type: msg.type, isSent, isGroupConversation, showSenderLabel, body: msg.body?.substring(0, 30) }) return (
{displayName}
{displayAddress}
{msgType.label}
{formatDate(activity.date)}
{/* Sender label for received messages in group conversations */} {showSenderLabel && (
From: {getSenderDisplayName(msg)}
)} {shouldDisplaySubject(msg.subject) && (
Subject: {msg.subject}
)} {msg.body && (

{msg.body}

)} {msg.media_type && ( )}
) } else if (activity.type === 'call' && activity.call) { const call = activity.call const callType = formatCallType(call.type) const formattedAddress = formatPhoneNumber(activity.address) const displayName = activity.contact_name || formattedAddress return (
{callType.icon}
{displayName}
{formattedAddress}
{callType.label}
{formatDate(activity.date)}
{call.duration > 0 && (
Duration: {formatDuration(call.duration)}
)}
) } return null })} {/* Infinite scroll trigger */}
{/* Loading more indicator */} {loadingMore && (
Loading more...

Loading more activities...

)} {/* End of results indicator */} {!hasMore && activities.length > 0 && (
No more activities to load
)}
) } export default Activity