Skip to content

Commit

Permalink
fix type issues, fix attachment display
Browse files Browse the repository at this point in the history
  • Loading branch information
natew committed Dec 29, 2024
1 parent c64da0e commit d9cf738
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 226 deletions.
62 changes: 47 additions & 15 deletions apps/chat/src/interface/messages/MessageInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RefMDEditor } from '@uiw/react-md-editor'
import { startTransition, useEffect, useRef, useState } from 'react'
import { Input, YStack, XStack, Image, Progress } from 'tamagui'
import { Input, YStack, XStack, Progress, Button } from 'tamagui'
import { Image } from '@tamagui/image-next'
import { useAuth } from '~/better-auth/authClient'
import { Editor, type EditorRef } from '~/editor/Editor'
import { randomID } from '~/helpers/randomID'
Expand All @@ -10,6 +11,7 @@ import { zero } from '~/zero/zero'
import { messagesListEmitter } from './MessagesList'
import { attachmentEmitter } from '../upload/DragDropFile'
import type { FileUpload } from '../upload/uploadImage'
import { X } from '@tamagui/lucide-icons'

let mainInputRef: EditorRef | null = null

Expand Down Expand Up @@ -45,7 +47,7 @@ export const MessageInput = ({ inThread }: { inThread?: boolean }) => {
}, [channel, inThread])

return (
<YStack btw={1} bc="$color4" p="$2">
<YStack btw={1} bc="$color4" p="$2" gap="$2">
<Editor
ref={inputRef}
onKeyDown={(e) => {
Expand Down Expand Up @@ -127,30 +129,60 @@ const MessageInputAttachments = () => {
const [attachments, setAttachments] = useState<FileUpload[]>([])

attachmentEmitter.use((value) => {
startTransition(() => {
console.warn('got attachment', value)
setAttachments(value)
})
console.warn('got attachment', value)
setAttachments(value)
})

return (
<XStack gap="$2">
{attachments.map((attachment) => {
const url = attachment.url || attachment.preview
const size = 50

return (
<YStack key={attachment.name} gap="$1">
<YStack key={attachment.name} gap="$1" w={size} h={size}>
{url && (
<Image
source={{ uri: attachment.url }}
width={100}
height={100}
objectFit="contain"
/>
<YStack pos="relative">
<Button
circular
icon={X}
size="$1"
pos="absolute"
t={-2}
r={-2}
zi={10}
onPress={() => {
setAttachments((prev) => {
return prev.filter((_) => _.name !== attachment.name)
})
}}
/>
<Image
src={url}
br="$6"
ov="hidden"
bw={1}
bc="$color3"
width={size}
height={size}
objectFit="contain"
/>
</YStack>
)}
{attachment.progress !== 100 && (
<Progress mt="$2" value={attachment.progress} bg="$color2">
<Progress.Indicator bc="$color7" animation="bouncy" />
<Progress
pos="absolute"
b={0}
l={0}
r={0}
w={size}
miw={size}
h={5}
zi={100}
value={attachment.progress}
bg="$color2"
>
<Progress.Indicator h={5} bc="$color7" animation="bouncy" />
</Progress>
)}
</YStack>
Expand Down
213 changes: 210 additions & 3 deletions apps/chat/src/interface/upload/DragDropFile.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { exists, readFile } from '@tauri-apps/plugin-fs'
import { createEmitter } from '@vxrn/emitter'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { isWeb } from 'tamagui'
import { isImageFile, useDragDrop, type DragDropEvent } from '~/tauri/useDragDrop'
import { useUploadImages, type FileUpload } from './uploadImage'
import { isTauri } from '~/tauri/constants'

export const attachmentEmitter = createEmitter<FileUpload[]>()

Expand All @@ -18,9 +20,13 @@ export const DragDropFile = (props: { children: any }) => {
onChange: (uploads) => {
attachmentEmitter.emit(
uploads.map((upload) => {
if (upload.status === 'complete') {
return upload
}
const preview = uploading.find((u) => u.file.name === upload.name)?.preview
return {
...upload,
preview: uploading.find((u) => u.file.name === upload.name)?.preview,
preview,
}
})
)
Expand Down Expand Up @@ -72,3 +78,204 @@ export const DragDropFile = (props: { children: any }) => {
</>
)
}

export type DropFile = {
name: string
contents: Uint8Array<ArrayBufferLike>
preview?: string
}

export type DropEvent = {
type: 'drop'
files: DropFile[]
}

export type DragDropEvent =
| {
type: 'drag'
x: number
y: number
}
| DropEvent
| {
type: 'cancel'
}

export const useDragDrop = (callback: (e: DragDropEvent) => void) => {
const [node, setNode] = useState<HTMLDivElement | null>(null)

if (isTauri) {
useEffect(() => {
let unlisten: Function | null = null

getCurrentWebview()
.onDragDropEvent(async (event) => {
if (event.payload.type === 'over') {
callback({
...event.payload.position,
type: 'drag',
})
} else if (event.payload.type === 'drop') {
const paths = event.payload.paths
const files = (
await Promise.all(
paths.flatMap(async (name) => {
if (await exists(name)) {
const contents = await readFile(name)
return [
{
name,
contents,
preview: await fileLikeToDataURI({ name, contents }),
} satisfies DropFile,
]
}

return []
})
)
).flat()

callback({
type: 'drop',
files,
})
} else {
callback({
type: 'cancel',
})
}
})
.then((disposer) => {
unlisten = disposer
})

return () => {
unlisten?.()
}
}, [])
} else {
useEffect(() => {
if (!node) return
const controller = new AbortController()
const { signal } = controller

node.addEventListener(
'dragover',
(e) => {
e.preventDefault()
e.stopPropagation()
e.dataTransfer!.dropEffect = 'copy'
callback({
type: 'drag',
x: e.clientX,
y: e.clientY,
})
},
{ signal }
)

node.addEventListener(
'dragleave',
(e) => {
e.preventDefault()
e.stopPropagation()
callback({
type: 'cancel',
})
},
{ signal }
)

node.addEventListener(
'drop',
async (e) => {
e.preventDefault()
e.stopPropagation()

const files: DropFile[] = []

for (const item of Array.from(e.dataTransfer!.files)) {
try {
files.push({
name: item.name,
contents: new Uint8Array(await item.arrayBuffer()),
preview: await fileLikeToDataURI(item),
})
} catch (err) {
console.error('Failed to read dropped file:', err)
}
}

callback({
type: 'drop',
files,
})
},
{ signal }
)

return () => controller.abort()
}, [node, callback])
}

return {
createElement: (children: any) => {
return (
<div
id="drag-drop-root"
ref={setNode}
style={{
minWidth: '100vw',
minHeight: '100vh',
inset: 0,
pointerEvents: 'auto',
}}
>
{children}
</div>
)
},
}
}

async function fileLikeToDataURI(
file: File | { name: string; contents: Uint8Array<ArrayBufferLike> }
) {
const contents = 'contents' in file ? file.contents : new Uint8Array(await file.arrayBuffer())
return arrayBufferToDataURL(contents, getFileType(file.name))
}

export function isImageFile(filename: string): boolean {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
return imageExtensions.some((ext) => filename.toLowerCase().endsWith(ext))
}

function getFileType(filename: string): string {
const ext = filename.toLowerCase().split('.').pop()
switch (ext) {
case 'jpg':
case 'jpeg':
return 'image/jpeg'
case 'png':
return 'image/png'
case 'gif':
return 'image/gif'
case 'webp':
return 'image/webp'
default:
return 'application/octet-stream'
}
}

function arrayBufferToDataURL(buffer: Uint8Array, mimeType: string): string {
const chunkSize = 65536
let base64 = ''

for (let i = 0; i < buffer.length; i += chunkSize) {
const chunk = buffer.subarray(i, i + chunkSize)
base64 += String.fromCharCode(...chunk)
}

return `data:${mimeType};base64,${btoa(base64)}`
}
Loading

0 comments on commit d9cf738

Please sign in to comment.