11import { FileIcon , PaperclipIcon } from "lucide-react" ;
2- import { useState } from "react" ;
2+ import { useMemo , useState } from "react" ;
33import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb" ;
44import { getAttachmentType , getAttachmentUrl } from "@/utils/attachment" ;
55import { formatFileSize , getFileTypeLabel } from "@/utils/format" ;
@@ -11,13 +11,18 @@ interface AttachmentListProps {
1111 attachments : Attachment [ ] ;
1212}
1313
14+ // Type guards for attachment types
15+ const isImageAttachment = ( attachment : Attachment ) : boolean => getAttachmentType ( attachment ) === "image/*" ;
16+ const isVideoAttachment = ( attachment : Attachment ) : boolean => getAttachmentType ( attachment ) === "video/*" ;
17+ const isMediaAttachment = ( attachment : Attachment ) : boolean => isImageAttachment ( attachment ) || isVideoAttachment ( attachment ) ;
18+
19+ // Separate attachments into media (images/videos) and documents
1420const separateMediaAndDocs = ( attachments : Attachment [ ] ) : { media : Attachment [ ] ; docs : Attachment [ ] } => {
1521 const media : Attachment [ ] = [ ] ;
1622 const docs : Attachment [ ] = [ ] ;
1723
1824 for ( const attachment of attachments ) {
19- const attachmentType = getAttachmentType ( attachment ) ;
20- if ( attachmentType === "image/*" || attachmentType === "video/*" ) {
25+ if ( isMediaAttachment ( attachment ) ) {
2126 media . push ( attachment ) ;
2227 } else {
2328 docs . push ( attachment ) ;
@@ -55,27 +60,39 @@ const DocumentItem = ({ attachment }: { attachment: Attachment }) => {
5560 ) ;
5661} ;
5762
58- const MediaGrid = ( { attachments, onImageClick } : { attachments : Attachment [ ] ; onImageClick : ( url : string ) => void } ) => (
63+ interface MediaItemProps {
64+ attachment : Attachment ;
65+ onImageClick : ( url : string ) => void ;
66+ }
67+
68+ const MediaItem = ( { attachment, onImageClick } : MediaItemProps ) => {
69+ const isImage = isImageAttachment ( attachment ) ;
70+
71+ const handleClick = ( ) => {
72+ if ( isImage ) {
73+ onImageClick ( getAttachmentUrl ( attachment ) ) ;
74+ }
75+ } ;
76+
77+ return (
78+ < div
79+ className = "aspect-square rounded-lg overflow-hidden bg-muted/40 border border-border hover:border-accent/50 transition-all cursor-pointer group"
80+ onClick = { handleClick }
81+ >
82+ < AttachmentCard attachment = { attachment } className = "rounded-none" />
83+ </ div >
84+ ) ;
85+ } ;
86+
87+ interface MediaGridProps {
88+ attachments : Attachment [ ] ;
89+ onImageClick : ( url : string ) => void ;
90+ }
91+
92+ const MediaGrid = ( { attachments, onImageClick } : MediaGridProps ) => (
5993 < div className = "grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2" >
6094 { attachments . map ( ( attachment ) => (
61- < div
62- key = { attachment . name }
63- className = "aspect-square rounded-lg overflow-hidden bg-muted/40 border border-border hover:border-accent/50 transition-all cursor-pointer group"
64- onClick = { ( ) => onImageClick ( getAttachmentUrl ( attachment ) ) }
65- >
66- < div className = "w-full h-full relative" >
67- < AttachmentCard attachment = { attachment } className = "rounded-none" />
68- { getAttachmentType ( attachment ) === "video/*" && (
69- < div className = "absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/40 transition-colors" >
70- < div className = "w-8 h-8 rounded-full bg-white/80 flex items-center justify-center" >
71- < svg className = "w-5 h-5 text-black fill-current ml-0.5" viewBox = "0 0 24 24" >
72- < path d = "M8 5v14l11-7z" />
73- </ svg >
74- </ div >
75- </ div >
76- ) }
77- </ div >
78- </ div >
95+ < MediaItem key = { attachment . name } attachment = { attachment } onImageClick = { onImageClick } />
7996 ) ) }
8097 </ div >
8198) ;
@@ -98,18 +115,20 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
98115 mimeType : undefined ,
99116 } ) ;
100117
101- const { media : mediaItems , docs : docItems } = separateMediaAndDocs ( attachments ) ;
118+ const { media : mediaItems , docs : docItems } = useMemo ( ( ) => separateMediaAndDocs ( attachments ) , [ attachments ] ) ;
119+
120+ // Pre-compute image URLs for preview dialog to avoid filtering on every click
121+ const imageAttachments = useMemo ( ( ) => mediaItems . filter ( isImageAttachment ) , [ mediaItems ] ) ;
122+ const imageUrls = useMemo ( ( ) => imageAttachments . map ( getAttachmentUrl ) , [ imageAttachments ] ) ;
102123
103124 if ( attachments . length === 0 ) {
104125 return null ;
105126 }
106127
107128 const handleImageClick = ( imgUrl : string ) => {
108- const imageAttachments = mediaItems . filter ( ( a ) => getAttachmentType ( a ) === "image/*" ) ;
109- const imgUrls = imageAttachments . map ( ( a ) => getAttachmentUrl ( a ) ) ;
110- const index = imgUrls . findIndex ( ( url ) => url === imgUrl ) ;
129+ const index = imageUrls . findIndex ( ( url ) => url === imgUrl ) ;
111130 const mimeType = imageAttachments [ index ] ?. type ;
112- setPreviewImage ( { open : true , urls : imgUrls , index, mimeType } ) ;
131+ setPreviewImage ( { open : true , urls : imageUrls , index, mimeType } ) ;
113132 } ;
114133
115134 return (
0 commit comments