@@ -10,50 +10,39 @@ import {
1010} from 'stream-chat' ;
1111
1212import {
13- AudioContainer ,
1413 CardContainer ,
1514 FileContainer ,
16- GalleryContainer ,
1715 GeolocationContainer ,
18- ImageContainer ,
16+ GiphyContainer ,
1917 MediaContainer ,
2018 UnsupportedAttachmentContainer ,
21- VoiceRecordingContainer ,
2219} from './AttachmentContainer' ;
2320import { SUPPORTED_VIDEO_FORMATS } from './utils' ;
21+ import { defaultAttachmentActionsDefaultFocus } from './AttachmentActions' ;
2422
2523import type { ReactPlayerProps } from 'react-player' ;
2624import type { SharedLocationResponse , Attachment as StreamAttachment } from 'stream-chat' ;
27- import type { AttachmentActionsProps } from './AttachmentActions' ;
25+ import type {
26+ AttachmentActionsDefaultFocusByType ,
27+ AttachmentActionsProps ,
28+ } from './AttachmentActions' ;
2829import type { AudioProps } from './Audio' ;
2930import type { VoiceRecordingProps } from './VoiceRecording' ;
30- import type { CardProps } from './Card' ;
31+ import type { CardProps } from './LinkPreview/ Card' ;
3132import type { FileAttachmentProps } from './FileAttachment' ;
3233import type { GalleryProps , ImageProps } from '../Gallery' ;
3334import type { UnsupportedAttachmentProps } from './UnsupportedAttachment' ;
3435import type { ActionHandlerReturnType } from '../Message/hooks/useActionHandler' ;
3536import type { GroupedRenderedAttachment } from './utils' ;
3637import type { GeolocationProps } from './Geolocation' ;
37-
38- const CONTAINER_MAP = {
39- audio : AudioContainer ,
40- // todo: rename to linkPreview
41- card : CardContainer ,
42- file : FileContainer ,
43- media : MediaContainer ,
44- unsupported : UnsupportedAttachmentContainer ,
45- voiceRecording : VoiceRecordingContainer ,
46- } as const ;
38+ import type { GiphyAttachmentProps } from './Giphy' ;
4739
4840export const ATTACHMENT_GROUPS_ORDER = [
49- 'card' ,
50- 'gallery' ,
51- 'image' ,
5241 'media' ,
53- 'audio' ,
54- 'voiceRecording' ,
55- 'file' ,
42+ 'giphy' ,
43+ 'card' ,
5644 'geolocation' ,
45+ 'file' ,
5746 'unsupported' ,
5847] as const ;
5948
@@ -62,6 +51,8 @@ export type AttachmentProps = {
6251 attachments : ( StreamAttachment | SharedLocationResponse ) [ ] ;
6352 /** The handler function to call when an action is performed on an attachment, examples include canceling a \/giphy command or shuffling the results. */
6453 actionHandler ?: ActionHandlerReturnType ;
54+ /** Which action should be focused on initial render, by attachment type (match by action.value) */
55+ attachmentActionsDefaultFocus ?: AttachmentActionsDefaultFocusByType ;
6556 /** Custom UI component for displaying attachment actions, defaults to and accepts same props as: [AttachmentActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/AttachmentActions.tsx) */
6657 AttachmentActions ?: React . ComponentType < AttachmentActionsProps > ;
6758 /** Custom UI component for displaying an audio type attachment, defaults to and accepts same props as: [Audio](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/Audio.tsx) */
@@ -73,6 +64,8 @@ export type AttachmentProps = {
7364 /** Custom UI component for displaying a gallery of image type attachments, defaults to and accepts same props as: [Gallery](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/Gallery.tsx) */
7465 Gallery ?: React . ComponentType < GalleryProps > ;
7566 Geolocation ?: React . ComponentType < GeolocationProps > ;
67+ /** Custom UI component for displaying a Giphy image, defaults to and accepts same props as: [Giphy](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/Giphy.tsx) */
68+ Giphy ?: React . ComponentType < GiphyAttachmentProps > ;
7669 /** Custom UI component for displaying an image type attachment, defaults to and accepts same props as: [Image](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/Image.tsx) */
7770 Image ?: React . ComponentType < ImageProps > ;
7871 /** Optional flag to signal that an attachment is a displayed as a part of a quoted message */
@@ -86,15 +79,24 @@ export type AttachmentProps = {
8679} ;
8780
8881/**
89- * A component used for rendering message attachments. By default, the component supports: AttachmentActions, Audio, Card, File, Gallery, Image, and Video
82+ * A component used for rendering message attachments.
9083 */
9184export const Attachment = ( props : AttachmentProps ) => {
92- const { attachments } = props ;
85+ const {
86+ attachmentActionsDefaultFocus = defaultAttachmentActionsDefaultFocus ,
87+ attachments,
88+ ...rest
89+ } = props ;
9390
9491 const groupedAttachments = useMemo (
95- ( ) => renderGroupedAttachments ( props ) ,
92+ ( ) =>
93+ renderGroupedAttachments ( {
94+ attachmentActionsDefaultFocus,
95+ attachments,
96+ ...rest ,
97+ } ) ,
9698 // eslint-disable-next-line react-hooks/exhaustive-deps
97- [ attachments ] ,
99+ [ attachments , attachmentActionsDefaultFocus ] ,
98100 ) ;
99101
100102 return (
@@ -111,87 +113,77 @@ const renderGroupedAttachments = ({
111113 attachments,
112114 ...rest
113115} : AttachmentProps ) : GroupedRenderedAttachment => {
114- const uploadedImages : StreamAttachment [ ] = attachments . filter ( ( attachment ) =>
115- isImageAttachment ( attachment ) ,
116+ const mediaAttachments : StreamAttachment [ ] = [ ] ;
117+ const containers = attachments . reduce < GroupedRenderedAttachment > (
118+ ( typeMap , attachment ) => {
119+ if ( isSharedLocationResponse ( attachment ) ) {
120+ typeMap . geolocation . push (
121+ < GeolocationContainer
122+ { ...rest }
123+ key = { `geolocation-${ typeMap . geolocation . length } ` }
124+ location = { attachment }
125+ /> ,
126+ ) ;
127+ } else if ( attachment . type === 'giphy' ) {
128+ typeMap . card . push (
129+ < GiphyContainer
130+ key = { `giphy-${ typeMap . giphy . length } ` }
131+ { ...rest }
132+ attachment = { attachment }
133+ /> ,
134+ ) ;
135+ } else if ( isScrapedContent ( attachment ) ) {
136+ typeMap . card . push (
137+ < CardContainer
138+ key = { `card-${ typeMap . card . length } ` }
139+ { ...rest }
140+ attachment = { attachment }
141+ /> ,
142+ ) ;
143+ } else if (
144+ isImageAttachment ( attachment ) ||
145+ isVideoAttachment ( attachment , SUPPORTED_VIDEO_FORMATS )
146+ ) {
147+ mediaAttachments . push ( attachment ) ;
148+ } else if (
149+ isAudioAttachment ( attachment ) ||
150+ isVoiceRecordingAttachment ( attachment ) ||
151+ isFileAttachment ( attachment , SUPPORTED_VIDEO_FORMATS )
152+ ) {
153+ typeMap . file . push (
154+ < FileContainer
155+ key = { `file-${ typeMap . file . length } ` }
156+ { ...rest }
157+ attachment = { attachment }
158+ /> ,
159+ ) ;
160+ } else {
161+ typeMap . unsupported . push (
162+ < UnsupportedAttachmentContainer
163+ key = { `unsupported-${ typeMap . unsupported . length } ` }
164+ { ...rest }
165+ attachment = { attachment }
166+ /> ,
167+ ) ;
168+ }
169+
170+ return typeMap ;
171+ } ,
172+ {
173+ card : [ ] ,
174+ file : [ ] ,
175+ geolocation : [ ] ,
176+ giphy : [ ] ,
177+ media : [ ] ,
178+ unsupported : [ ] ,
179+ } ,
116180 ) ;
117181
118- const containers = attachments
119- . filter ( ( attachment ) => ! isImageAttachment ( attachment ) )
120- . reduce < GroupedRenderedAttachment > (
121- ( typeMap , attachment ) => {
122- if ( isSharedLocationResponse ( attachment ) ) {
123- typeMap . geolocation . push (
124- < GeolocationContainer
125- { ...rest }
126- key = 'geolocation-container'
127- location = { attachment }
128- /> ,
129- ) ;
130- } else {
131- const attachmentType = getAttachmentType ( attachment ) ;
132-
133- const Container = CONTAINER_MAP [ attachmentType ] ;
134- typeMap [ attachmentType ] . push (
135- < Container
136- key = { `${ attachmentType } -${ typeMap [ attachmentType ] . length } ` }
137- { ...rest }
138- attachment = { attachment }
139- /> ,
140- ) ;
141- }
142-
143- return typeMap ;
144- } ,
145- {
146- audio : [ ] ,
147- card : [ ] ,
148- file : [ ] ,
149- media : [ ] ,
150- unsupported : [ ] ,
151- // not used in reduce
152- // eslint-disable-next-line sort-keys
153- image : [ ] ,
154- // eslint-disable-next-line sort-keys
155- gallery : [ ] ,
156- geolocation : [ ] ,
157- voiceRecording : [ ] ,
158- } ,
182+ if ( mediaAttachments . length ) {
183+ containers . media . push (
184+ < MediaContainer key = 'media-container' { ...rest } attachments = { mediaAttachments } /> ,
159185 ) ;
160-
161- if ( uploadedImages . length > 1 ) {
162- containers [ 'gallery' ] = [
163- < GalleryContainer
164- key = 'gallery-container'
165- { ...rest }
166- attachment = { {
167- images : uploadedImages ,
168- type : 'gallery' ,
169- } }
170- /> ,
171- ] ;
172- } else if ( uploadedImages . length === 1 ) {
173- containers [ 'image' ] = [
174- < ImageContainer key = 'image-container' { ...rest } attachment = { uploadedImages [ 0 ] } /> ,
175- ] ;
176186 }
177187
178188 return containers ;
179189} ;
180-
181- export const getAttachmentType = (
182- attachment : AttachmentProps [ 'attachments' ] [ number ] ,
183- ) : keyof typeof CONTAINER_MAP => {
184- if ( isScrapedContent ( attachment ) ) {
185- return 'card' ;
186- } else if ( isVideoAttachment ( attachment , SUPPORTED_VIDEO_FORMATS ) ) {
187- return 'media' ;
188- } else if ( isAudioAttachment ( attachment ) ) {
189- return 'audio' ;
190- } else if ( isVoiceRecordingAttachment ( attachment ) ) {
191- return 'voiceRecording' ;
192- } else if ( isFileAttachment ( attachment , SUPPORTED_VIDEO_FORMATS ) ) {
193- return 'file' ;
194- }
195-
196- return 'unsupported' ;
197- } ;
0 commit comments