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 activity...
No activity found
{msg.body}
)} {msg.media_type && (Loading more activities...