Media-only view
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
.media-carousel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.95);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.carousel-close-btn {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10001;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.carousel-close-btn:hover {
|
||||
background-color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.carousel-counter {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.carousel-info {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
z-index: 10001;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.carousel-nav-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10001;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.carousel-nav-btn:hover {
|
||||
background-color: white;
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.carousel-prev-btn {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.carousel-next-btn {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.carousel-media-container {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.carousel-media {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.media-carousel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.carousel-nav-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.carousel-close-btn {
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.carousel-counter {
|
||||
top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
.carousel-info {
|
||||
bottom: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.carousel-prev-btn {
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
.carousel-next-btn {
|
||||
right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyboard navigation hint */
|
||||
@media (min-width: 769px) {
|
||||
.carousel-nav-btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -5px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import './MediaCarousel.css'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api'
|
||||
|
||||
function MediaCarousel({ mediaItems, initialIndex, onClose, transcodeVideos = new Set() }) {
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
||||
const [touchStart, setTouchStart] = useState(null)
|
||||
const [touchEnd, setTouchEnd] = useState(null)
|
||||
const videoRef = useRef(null)
|
||||
|
||||
const currentItem = mediaItems[currentIndex]
|
||||
const isVideo = currentItem?.media_type?.startsWith('video/')
|
||||
const shouldTranscode = transcodeVideos.has(currentItem?.id)
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent body scroll when carousel is open
|
||||
document.body.style.overflow = 'hidden'
|
||||
|
||||
// Hide UI elements that might appear over the carousel
|
||||
const datePickers = document.querySelectorAll('.react-datepicker-popper')
|
||||
datePickers.forEach(picker => {
|
||||
picker.style.display = 'none'
|
||||
})
|
||||
|
||||
const dateFilterContainer = document.querySelector('.date-filter-container')
|
||||
if (dateFilterContainer) {
|
||||
dateFilterContainer.style.visibility = 'hidden'
|
||||
}
|
||||
|
||||
const header = document.querySelector('header')
|
||||
if (header) {
|
||||
header.style.visibility = 'hidden'
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset'
|
||||
|
||||
// Restore visibility of hidden elements
|
||||
const datePickers = document.querySelectorAll('.react-datepicker-popper')
|
||||
datePickers.forEach(picker => {
|
||||
picker.style.display = ''
|
||||
})
|
||||
|
||||
const dateFilterContainer = document.querySelector('.date-filter-container')
|
||||
if (dateFilterContainer) {
|
||||
dateFilterContainer.style.visibility = ''
|
||||
}
|
||||
|
||||
const header = document.querySelector('header')
|
||||
if (header) {
|
||||
header.style.visibility = ''
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose()
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
handlePrevious()
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
handleNext()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [currentIndex])
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentIndex > 0) {
|
||||
setCurrentIndex(currentIndex - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentIndex < mediaItems.length - 1) {
|
||||
setCurrentIndex(currentIndex + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Touch handlers for swipe gestures
|
||||
const handleTouchStart = (e) => {
|
||||
setTouchEnd(null)
|
||||
setTouchStart(e.targetTouches[0].clientX)
|
||||
}
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
setTouchEnd(e.targetTouches[0].clientX)
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!touchStart || !touchEnd) return
|
||||
|
||||
const distance = touchStart - touchEnd
|
||||
const isLeftSwipe = distance > 50
|
||||
const isRightSwipe = distance < -50
|
||||
|
||||
if (isLeftSwipe) {
|
||||
handleNext()
|
||||
}
|
||||
if (isRightSwipe) {
|
||||
handlePrevious()
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (date) => {
|
||||
return format(new Date(date), 'MMM d, yyyy h:mm a')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="media-carousel"
|
||||
onClick={onClose}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
className="carousel-close-btn"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg style={{ width: '1.5rem', height: '1.5rem' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Counter */}
|
||||
<div className="carousel-counter">
|
||||
{currentIndex + 1} / {mediaItems.length}
|
||||
</div>
|
||||
|
||||
{/* Media info */}
|
||||
<div className="carousel-info">
|
||||
<div>{formatTime(currentItem.date)}</div>
|
||||
{currentItem.body && (
|
||||
<div className="text-muted small mt-1">{currentItem.body}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Previous button */}
|
||||
{currentIndex > 0 && (
|
||||
<button
|
||||
className="carousel-nav-btn carousel-prev-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handlePrevious()
|
||||
}}
|
||||
aria-label="Previous"
|
||||
>
|
||||
<svg style={{ width: '2rem', height: '2rem' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Next button */}
|
||||
{currentIndex < mediaItems.length - 1 && (
|
||||
<button
|
||||
className="carousel-nav-btn carousel-next-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleNext()
|
||||
}}
|
||||
aria-label="Next"
|
||||
>
|
||||
<svg style={{ width: '2rem', height: '2rem' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Media content */}
|
||||
<div
|
||||
className="carousel-media-container"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isVideo ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
controls
|
||||
autoPlay
|
||||
playsInline
|
||||
className="carousel-media"
|
||||
src={`${API_BASE}/media?id=${currentItem.id}${shouldTranscode ? '&transcode=true' : ''}`}
|
||||
key={`${currentItem.id}-${shouldTranscode}`}
|
||||
onError={(e) => {
|
||||
console.error('Video playback error:', e)
|
||||
console.error('Video src:', `${API_BASE}/media?id=${currentItem.id}${shouldTranscode ? '&transcode=true' : ''}`)
|
||||
console.error('Video error code:', e.target.error?.code)
|
||||
console.error('Video error message:', e.target.error?.message)
|
||||
}}
|
||||
onLoadStart={() => console.log('Video load started:', currentItem.id)}
|
||||
onCanPlay={() => console.log('Video can play:', currentItem.id)}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={`${API_BASE}/media?id=${currentItem.id}`}
|
||||
alt={`Media ${currentIndex + 1}`}
|
||||
className="carousel-media"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MediaCarousel
|
||||
@@ -0,0 +1,66 @@
|
||||
.media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-grid-item {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background-color: #f0f0f0;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.media-grid-item:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.media-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.media-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.video-indicator {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.media-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.media-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import MediaCarousel from './MediaCarousel'
|
||||
import './MediaGrid.css'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api'
|
||||
|
||||
function MediaGrid({ conversation, startDate, endDate }) {
|
||||
const [mediaItems, setMediaItems] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(null)
|
||||
const [failedVideos, setFailedVideos] = useState(new Set())
|
||||
const [transcodeVideos, setTranscodeVideos] = useState(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
fetchMediaItems()
|
||||
setFailedVideos(new Set()) // Reset failed videos when conversation changes
|
||||
setTranscodeVideos(new Set()) // Reset transcode videos when conversation changes
|
||||
}, [conversation, startDate, endDate])
|
||||
|
||||
const fetchMediaItems = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = {
|
||||
address: conversation.address
|
||||
}
|
||||
if (startDate) params.start = startDate.toISOString()
|
||||
if (endDate) params.end = endDate.toISOString()
|
||||
|
||||
const response = await axios.get(`${API_BASE}/media-items`, { params })
|
||||
setMediaItems(response.data || [])
|
||||
} catch (error) {
|
||||
console.error('Error fetching media items:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleThumbnailClick = (index) => {
|
||||
setSelectedIndex(index)
|
||||
}
|
||||
|
||||
const handleCloseCarousel = () => {
|
||||
setSelectedIndex(null)
|
||||
}
|
||||
|
||||
const handleVideoError = (itemId, alreadyTranscoded = false) => {
|
||||
if (!alreadyTranscoded) {
|
||||
console.log('Video failed to play, retrying with transcoding:', itemId)
|
||||
// Mark this video to be transcoded and it will reload
|
||||
setTranscodeVideos(prev => new Set([...prev, itemId]))
|
||||
return
|
||||
}
|
||||
// If already transcoded and still failing, hide it
|
||||
console.warn('Video not playable even after transcoding:', itemId)
|
||||
setFailedVideos(prev => new Set([...prev, itemId]))
|
||||
}
|
||||
|
||||
// Filter out failed videos from display
|
||||
const displayableItems = mediaItems.filter(item => !failedVideos.has(item.id))
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '400px' }}>
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading media...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (displayableItems.length === 0 && !loading) {
|
||||
return (
|
||||
<div className="text-center py-5">
|
||||
<svg
|
||||
style={{ width: '4rem', height: '4rem' }}
|
||||
className="mb-3 text-muted opacity-50"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-muted">
|
||||
{mediaItems.length === 0
|
||||
? 'No photos or videos found in this conversation'
|
||||
: 'No browser-compatible photos or videos found in this conversation'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{transcodeVideos.size > 0 && failedVideos.size === 0 && (
|
||||
<div className="alert alert-info mx-3 mt-3 mb-0" role="alert">
|
||||
<small>
|
||||
{transcodeVideos.size} video{transcodeVideos.size > 1 ? 's are' : ' is'} being transcoded for browser compatibility...
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
{failedVideos.size > 0 && (
|
||||
<div className="alert alert-warning mx-3 mt-3 mb-0" role="alert">
|
||||
<small>
|
||||
{failedVideos.size} video{failedVideos.size > 1 ? 's' : ''} could not be transcoded.
|
||||
These videos can still be viewed in the regular message view.
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
<div className="media-grid">
|
||||
{displayableItems.map((item, index) => {
|
||||
const isVideo = item.media_type?.startsWith('video/')
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="media-grid-item"
|
||||
onClick={() => handleThumbnailClick(index)}
|
||||
>
|
||||
<div className="media-thumbnail">
|
||||
{isVideo ? (
|
||||
<>
|
||||
<video
|
||||
src={`${API_BASE}/media?id=${item.id}${transcodeVideos.has(item.id) ? '&transcode=true' : ''}#t=0.1`}
|
||||
preload="metadata"
|
||||
muted
|
||||
playsInline
|
||||
key={`${item.id}-${transcodeVideos.has(item.id)}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
onError={(e) => {
|
||||
if (e.target.error?.code === 4) {
|
||||
// MEDIA_ERR_SRC_NOT_SUPPORTED - unsupported format/codec
|
||||
handleVideoError(item.id, transcodeVideos.has(item.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="video-indicator">
|
||||
<svg
|
||||
style={{ width: '2rem', height: '2rem' }}
|
||||
fill="white"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<img
|
||||
src={`${API_BASE}/media?id=${item.id}`}
|
||||
alt={`Media ${index + 1}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{selectedIndex !== null && (
|
||||
<MediaCarousel
|
||||
mediaItems={displayableItems}
|
||||
initialIndex={selectedIndex}
|
||||
onClose={handleCloseCarousel}
|
||||
transcodeVideos={transcodeVideos}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MediaGrid
|
||||
@@ -3,6 +3,7 @@ import { useLocation } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
import { format } from 'date-fns'
|
||||
import LazyMedia from './LazyMedia'
|
||||
import MediaGrid from './MediaGrid'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api'
|
||||
|
||||
@@ -12,12 +13,14 @@ function MessageThread({ conversation, startDate, endDate }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [highlightedMessageId, setHighlightedMessageId] = useState(null)
|
||||
const [isPreprintingMedia, setIsPreprintingMedia] = useState(false)
|
||||
const [showMediaOnly, setShowMediaOnly] = useState(false)
|
||||
const messageRefs = useRef({})
|
||||
const printTriggeredRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation) {
|
||||
fetchItems()
|
||||
setShowMediaOnly(false) // Reset to message view when conversation changes
|
||||
} else {
|
||||
setItems([])
|
||||
}
|
||||
@@ -464,7 +467,17 @@ function MessageThread({ conversation, startDate, endDate }) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowMediaOnly(!showMediaOnly)}
|
||||
className={`btn btn-sm ${showMediaOnly ? 'btn-primary' : 'btn-outline-primary'} d-flex align-items-center gap-1`}
|
||||
title={showMediaOnly ? "Show all messages" : "Show photos only"}
|
||||
>
|
||||
<svg style={{width: '1rem', height: '1rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="d-none d-md-inline">{showMediaOnly ? 'Show All' : 'Photos'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportPDF}
|
||||
className="btn btn-sm btn-outline-primary d-flex align-items-center gap-1"
|
||||
@@ -481,7 +494,10 @@ function MessageThread({ conversation, startDate, endDate }) {
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-fill overflow-auto p-2 p-md-4 bg-light">
|
||||
{isCallLog ? (
|
||||
{showMediaOnly && !isCallLog ? (
|
||||
// Media Grid View
|
||||
<MediaGrid conversation={conversation} startDate={startDate} endDate={endDate} />
|
||||
) : isCallLog ? (
|
||||
// Call Log View
|
||||
<div className="d-flex flex-column gap-3">
|
||||
{items.map((call) => {
|
||||
|
||||
Reference in New Issue
Block a user