11"use client" ;
22
3- import { type FC , useCallback , useMemo } from "react" ;
4- import {
5- Image as NativeImage ,
6- Pressable ,
7- ScrollView ,
8- View ,
9- } from "react-native" ;
10- import * as ImagePicker from "expo-image-picker" ;
11- import * as DocumentPicker from "expo-document-picker" ;
12- import { Alert } from "react-native" ;
3+ import { type FC , useMemo } from "react" ;
4+ import { Image as NativeImage , ScrollView , View } from "react-native" ;
135import { Icon } from "../ui/icon" ;
146import { Text } from "../ui/text" ;
157import { cn } from "~/utils/cn" ;
@@ -18,87 +10,7 @@ import * as AttachmentPrimitive from "@creatorem/ai-react-native/primitives/atta
1810import { useAttachment } from "@creatorem/ai-react-native/primitives/attachment" ;
1911import * as ComposerPrimitive from "@creatorem/ai-react-native/primitives/composer" ;
2012import * as MessagePrimitive from "@creatorem/ai-react-native/primitives/message" ;
21- import { useComposerStore } from "@creatorem/ai-chat/primitives/composer" ;
22-
23- const uriToFile = async (
24- uri : string ,
25- name : string ,
26- type : string ,
27- ) : Promise < File > => {
28- const response = await fetch ( uri ) ;
29- const blob = await response . blob ( ) ;
30- return new File ( [ blob ] , name , { type } ) ;
31- } ;
32-
33- const isImageOnlyAccept = ( accept : string ) : boolean => {
34- if ( accept === "*" ) return false ;
35- return accept . split ( "," ) . every ( ( item ) => item . trim ( ) . startsWith ( "image" ) ) ;
36- } ;
37-
38- const useComposerAddAttachment = ( {
39- multiple = true ,
40- } : {
41- multiple ?: boolean ;
42- } = { } ) => {
43- const composerStore = useComposerStore ( ) ;
44-
45- return useCallback ( async ( ) => {
46- const { attachmentAccept, addAttachment } = composerStore . getState ( ) ;
47-
48- try {
49- if ( isImageOnlyAccept ( attachmentAccept ) ) {
50- const { status } =
51- await ImagePicker . requestMediaLibraryPermissionsAsync ( ) ;
52- if ( status !== "granted" ) {
53- Alert . alert (
54- "Permission required" ,
55- "Please grant media library access to add attachments." ,
56- ) ;
57- return ;
58- }
59-
60- const result = await ImagePicker . launchImageLibraryAsync ( {
61- mediaTypes : [ "images" ] ,
62- allowsMultipleSelection : multiple ,
63- quality : 1 ,
64- } ) ;
65-
66- if ( result . canceled || ! result . assets ?. length ) return ;
67-
68- for ( const asset of result . assets ) {
69- const file = await uriToFile (
70- asset . uri ,
71- asset . fileName || `image-${ Date . now ( ) } .jpg` ,
72- asset . mimeType || "image/jpeg" ,
73- ) ;
74- await addAttachment ( file ) ;
75- }
76- return ;
77- }
78-
79- const result = await DocumentPicker . getDocumentAsync ( {
80- multiple,
81- type :
82- attachmentAccept !== "*"
83- ? attachmentAccept . split ( "," ) . map ( ( item ) => item . trim ( ) )
84- : undefined ,
85- } ) ;
86-
87- if ( result . canceled || ! result . assets ?. length ) return ;
88-
89- for ( const asset of result . assets ) {
90- const file = await uriToFile (
91- asset . uri ,
92- asset . name ,
93- asset . mimeType || "application/octet-stream" ,
94- ) ;
95- await addAttachment ( file ) ;
96- }
97- } catch {
98- Alert . alert ( "Error" , "Failed to add attachment. Please try again." ) ;
99- }
100- } , [ composerStore , multiple ] ) ;
101- } ;
13+ import { useActionSheet } from "../ui/action-sheet" ;
10214
10315const useAttachmentImageSrc = ( ) => {
10416 const attachment = useAttachment ( ) ;
@@ -133,132 +45,116 @@ const AttachmentThumb: FC = () => {
13345 ) ;
13446} ;
13547
136- const AttachmentRemove : FC = ( ) => {
137- return (
138- < View className = "absolute top-1 right-1 z-10" >
139- < AttachmentPrimitive . Remove
140- size = "icon"
141- variant = "ghost"
142- className = "h-6 w-6 rounded-full bg-background/90 p-1"
143- aria-label = "Remove attachment"
144- >
145- < Icon name = "X" size = { 12 } />
146- </ AttachmentPrimitive . Remove >
147- </ View >
148- ) ;
149- } ;
150-
151- const AttachmentUI : FC = ( ) => {
152- const attachment = useAttachment ( ) ;
153- const isComposerAttachment = attachment . status . type !== "complete" ;
154- const tileSizeClass =
155- isComposerAttachment && attachment . type === "image"
156- ? "h-24 w-24"
157- : "h-14 w-14" ;
158-
48+ const AttachmentUI : FC < { noRemoveButton ?: boolean } > = ( { noRemoveButton } ) => {
15949 return (
16050 < AttachmentPrimitive . Root className = "relative" >
16151 < View
16252 className = { cn (
163- "overflow-hidden rounded-xl border border-border bg-secondary" ,
164- tileSizeClass ,
53+ "aspect-square flex-1 overflow-hidden rounded-xl border border-input" ,
16554 ) }
16655 >
16756 < AttachmentThumb />
16857 </ View >
16958
170- { isComposerAttachment && < AttachmentRemove /> }
171-
172- < View className = "mt-1 max-w-24" >
173- < Text className = "text-muted-foreground text-xs" numberOfLines = { 1 } >
174- < AttachmentPrimitive . Name />
175- </ Text >
176- </ View >
59+ { ! noRemoveButton && (
60+ < View className = "absolute top-1.5 right-1.5 z-20" >
61+ < AttachmentPrimitive . Remove
62+ size = "icon"
63+ variant = "ghost"
64+ className = "size-5 rounded-full bg-background p-0 text-foreground"
65+ aria-label = "Remove attachment"
66+ >
67+ < Icon name = "X" size = { 14 } strokeWidth = { 3 } />
68+ </ AttachmentPrimitive . Remove >
69+ </ View >
70+ ) }
17771 </ AttachmentPrimitive . Root >
17872 ) ;
17973} ;
18074
18175export const UserMessageAttachments : FC = ( ) => {
18276 return (
183- < View className = "col-span-full col-start-1 row-start-1 mb-2 flex w-full flex-row justify-end gap-2" >
184- < MessagePrimitive . Attachments components = { { Attachment : AttachmentUI } } />
77+ < View className = "col-span-full col-start-1 row-start-1 mb-2 flex h-32 w-full flex-row justify-end gap-2" >
78+ < MessagePrimitive . Attachments
79+ components = { { Attachment : AttachmentUI } }
80+ componentProps = { { noRemoveButton : true } }
81+ />
18582 </ View >
18683 ) ;
18784} ;
18885
18986export const ComposerAttachments : FC = ( ) => {
190- return (
87+ const attachments = ComposerPrimitive . useComposer ( ( s ) => s . attachments ) ;
88+ return attachments . length > 0 ? (
19189 < ScrollView
19290 horizontal
193- showsHorizontalScrollIndicator = { false }
19491 contentContainerStyle = { { gap : 8 , paddingHorizontal : 6 , paddingBottom : 4 } }
195- className = "mb-2 w-full"
92+ className = "h-32 w-full p-2 "
19693 >
19794 < ComposerPrimitive . Attachments
19895 components = { { Attachment : AttachmentUI } }
19996 />
20097 </ ScrollView >
201- ) ;
98+ ) : null ;
20299} ;
203100
204- // export const ComposerAddAttachment: FC<{ multiple?: boolean }> = ({
205- // multiple = true,
206- // }) => {
207- // const onAddAttachment = useComposerAddAttachment({ multiple });
208-
209- // return (
210- // <Pressable
211- // onPress={onAddAttachment}
212- // className="size-8 items-center justify-center rounded-full"
213- // accessibilityLabel="Add attachment"
214- // >
215- // <Icon name="Plus" size={18} />
216- // </Pressable>
217- // );
218- // };
219-
220- const ComposerAddAttachmentTakePhoto : FC = ( ) => {
101+ const ComposerAddAttachmentTakePhoto : FC < { onAddAttachment ?: ( ) => void } > = ( {
102+ onAddAttachment,
103+ } ) => {
221104 return (
222105 < ComposerPrimitive . AddAttachmentTakePhoto
223106 variant = "secondary"
224107 className = "flex h-20 flex-1 flex-col rounded-2xl p-2"
108+ onAddAttachment = { onAddAttachment }
225109 >
226110 < Icon name = "Camera" size = { 20 } />
227111 < Text > Take photo</ Text >
228112 </ ComposerPrimitive . AddAttachmentTakePhoto >
229113 ) ;
230114} ;
231115
232- const ComposerAddAttachmentImage : FC = ( ) => {
116+ const ComposerAddAttachmentImage : FC < { onAddAttachment ?: ( ) => void } > = ( {
117+ onAddAttachment,
118+ } ) => {
233119 return (
234120 < ComposerPrimitive . AddAttachmentImage
235121 variant = "secondary"
236122 className = "flex h-20 flex-1 flex-col rounded-2xl p-2"
123+ onAddAttachment = { onAddAttachment }
237124 >
238125 < Icon name = "Image" size = { 20 } />
239126 < Text > Image</ Text >
240127 </ ComposerPrimitive . AddAttachmentImage >
241128 ) ;
242129} ;
243130
244- const ComposerAddAttachmentFile : FC = ( ) => {
131+ const ComposerAddAttachmentFile : FC < { onAddAttachment ?: ( ) => void } > = ( {
132+ onAddAttachment,
133+ } ) => {
245134 return (
246135 < ComposerPrimitive . AddAttachmentFile
247136 variant = "secondary"
248137 className = "flex h-20 flex-1 flex-col rounded-2xl p-2"
138+ onAddAttachment = { onAddAttachment }
249139 >
250140 < Icon name = "Paperclip" size = { 20 } />
251141 < Text > Files</ Text >
252142 </ ComposerPrimitive . AddAttachmentFile >
253143 ) ;
254144} ;
255145
256- export const ComposerAddAttachment : FC = ( ) => {
146+ export const ComposerAddAttachmentSheet : FC = ( ) => {
147+ const { setOpen } = useActionSheet ( ) ;
148+
149+ const handleAddAttachment = ( ) => {
150+ setOpen ( false ) ;
151+ } ;
152+
257153 return (
258154 < View className = "flex flex-row gap-4 p-6 pt-2" >
259- < ComposerAddAttachmentTakePhoto />
260- < ComposerAddAttachmentImage />
261- < ComposerAddAttachmentFile />
155+ < ComposerAddAttachmentTakePhoto onAddAttachment = { handleAddAttachment } />
156+ < ComposerAddAttachmentImage onAddAttachment = { handleAddAttachment } />
157+ < ComposerAddAttachmentFile onAddAttachment = { handleAddAttachment } />
262158 </ View >
263159 ) ;
264160} ;
0 commit comments