feat: add message attachments (wip)

This commit is contained in:
Zoe
2026-02-28 17:19:48 -06:00
parent 5decf9939b
commit 874f0f7397
16 changed files with 780 additions and 50 deletions
+119
View File
@@ -0,0 +1,119 @@
<script setup lang="ts">
import { nanoid } from 'nanoid';
import { assert } from '~~/utils/assert';
const { openDropdown, closeDropdown, dropdownState } = useDropdown();
const inputRef = ref<HTMLInputElement | null>(null);
const files = defineModel<{
id: string;
name: string;
mimeType: string;
status: 'pending' | 'uploaded';
url: string;
}[]>({ required: false, default: [] });
const rawFiles = ref<{
name: string;
type: string;
file: File;
}[]>([]);
const triplit = useTriplitClient();
watch(rawFiles, async (newFiles, oldFiles) => {
const { user } = useAuth();
assert(user.value !== null);
const diff = newFiles.filter(f => !oldFiles.find(o => o.name === f.name));
if (diff.length === 0) return;
for (const file of diff) {
files.value.push({
id: nanoid(),
name: file.name,
mimeType: file.type,
status: 'pending',
url: URL.createObjectURL(file.file),
});
}
await Promise.all(diff.map(async file => {
const { url: uploadUrl, assetUrl } = await $fetch('/api/upload/presigned', {
method: 'POST',
body: JSON.stringify({
file: {
name: file.name,
mimeType: file.type,
}
}),
}) as { url: string; assetUrl: string };
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: file.file,
});
assert(uploadResponse.ok, 'Failed to upload file');
await triplit.insert('files', {
id: files.value.find(f => f.name === file.name)!.id,
userId: user.value!.id,
name: file.name,
mimeType: file.type,
url: assetUrl,
})
files.value = files.value.map(f => {
if (f.name === file.name) {
return {
...f,
status: 'uploaded',
};
}
return f;
});
}));
})
const toggleSelectorDropdown = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (inputRef.value === null) return;
if (dropdownState.open) {
closeDropdown();
return;
}
openDropdown(e, () => [
{ label: "Upload file", icon: "i-mynaui-file-plus", onClick: () => inputRef.value?.click() },
])
};
const handleFileChange = (e: Event) => {
const inputFiles = (e.target! as HTMLInputElement).files;
if (inputFiles === null) return;
console.log(inputFiles);
rawFiles.value = [...inputFiles].map(file => ({
name: file.name,
type: file.type,
file: file,
}));
}
onMounted(() => {
inputRef.value?.addEventListener('change', handleFileChange);
});
onUnmounted(() => {
inputRef.value?.removeEventListener('change', handleFileChange);
});
</script>
<template>
<input ref="inputRef" type="file" multiple class="hidden" />
<button @click="toggleSelectorDropdown"
class="flex items-center justify-center h-8.5 w-8.5 hover:bg-[var(--color-hover)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="pointer-events-none i-mynaui-paperclip text-5 text-[var(--text-secondary)]"></span>
</button>
</template>