diff --git a/web/convex/files.ts b/web/convex/files.ts index 55dd9ce..9e7ee74 100644 --- a/web/convex/files.ts +++ b/web/convex/files.ts @@ -1,4 +1,4 @@ -import { action, query } from "./_generated/server"; +import { action } from "./_generated/server"; import { v } from "convex/values"; export const generateUploadUrl = action({ @@ -8,7 +8,7 @@ export const generateUploadUrl = action({ }, }); -export const getUrl = query({ +export const getUrl = action({ args: { storageId: v.id("_storage") }, handler: async (ctx, { storageId }) => { const url = await ctx.storage.getUrl(storageId); diff --git a/web/src/components/tickets/ticket-comments.rich.tsx b/web/src/components/tickets/ticket-comments.rich.tsx index af3029a..2ee6ed3 100644 --- a/web/src/components/tickets/ticket-comments.rich.tsx +++ b/web/src/components/tickets/ticket-comments.rich.tsx @@ -1,11 +1,11 @@ "use client" -import { useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { formatDistanceToNow } from "date-fns" import { ptBR } from "date-fns/locale" import { IconLock, IconMessage } from "@tabler/icons-react" import { FileIcon, Trash2, X } from "lucide-react" -import { useMutation } from "convex/react" +import { useAction, useMutation } from "convex/react" // @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" @@ -179,58 +179,16 @@ export function TicketComments({ ticket }: TicketCommentsProps) { ) : null} {comment.attachments?.length ? (
- {comment.attachments.map((attachment) => { - const name = attachment?.name ?? "" - const url = attachment?.url - const type = attachment?.type ?? "" - const isImage = - (!!type && type.startsWith("image/")) || - /\.(png|jpe?g|gif|webp|svg)$/i.test(name) || - /\.(png|jpe?g|gif|webp|svg)$/i.test(url ?? "") - const openRemovalModal = (event: React.MouseEvent) => { - event.preventDefault() - event.stopPropagation() - setAttachmentToRemove({ commentId: comment.id, attachmentId: attachment.id, name }) - } - return ( -
- {isImage && url ? ( - - ) : ( - - - {url ? Baixar : Pendente} - - )} - -
- {name} -
-
- ) - })} + {comment.attachments.map((attachment) => ( + setPreview(url)} + onRequestRemoval={() => + setAttachmentToRemove({ commentId: comment.id, attachmentId: attachment.id, name: attachment.name }) + } + /> + ))}
) : null} @@ -341,6 +299,9 @@ export function TicketComments({ ticket }: TicketCommentsProps) { !open && setPreview(null)}> + + Visualização de anexo + {preview ? ( <> {/* eslint-disable-next-line @next/next/no-img-element */} @@ -353,3 +314,111 @@ export function TicketComments({ ticket }: TicketCommentsProps) { ) } + +type CommentAttachment = TicketWithDetails["comments"][number]["attachments"][number] + +function CommentAttachmentCard({ + attachment, + onOpenPreview, + onRequestRemoval, +}: { + attachment: CommentAttachment + onOpenPreview: (url: string) => void + onRequestRemoval: () => void +}) { + const getFileUrl = useAction(api.files.getUrl) + const [url, setUrl] = useState(attachment.url ?? null) + const [refreshing, setRefreshing] = useState(false) + const [errored, setErrored] = useState(false) + const hasRefreshedRef = useRef(false) + + useEffect(() => { + setUrl(attachment.url ?? null) + setErrored(false) + hasRefreshedRef.current = false + }, [attachment.id, attachment.url]) + + const ensureUrl = useCallback(async () => { + try { + setRefreshing(true) + const fresh = await getFileUrl({ storageId: attachment.id as Id<"_storage"> }) + if (fresh) { + setUrl(fresh) + setErrored(false) + return fresh + } + } catch (error) { + console.error("Failed to refresh attachment URL", error) + } finally { + setRefreshing(false) + } + return null + }, [attachment.id, getFileUrl]) + + const handleImageError = useCallback(async () => { + if (hasRefreshedRef.current) { + setErrored(true) + return + } + hasRefreshedRef.current = true + const fresh = await ensureUrl() + if (!fresh) { + setErrored(true) + } + }, [ensureUrl]) + + const handlePreview = useCallback(async () => { + const target = url ?? (await ensureUrl()) + if (target) { + onOpenPreview(target) + } + }, [ensureUrl, onOpenPreview, url]) + + const handleDownload = useCallback(async () => { + const target = url ?? (await ensureUrl()) + if (target) { + window.open(target, "_blank", "noopener,noreferrer") + } + }, [ensureUrl, url]) + + const name = attachment.name ?? "" + const type = attachment.type ?? "" + const isImage = + (!!type && type.startsWith("image/")) || + /\.(png|jpe?g|gif|webp|svg)$/i.test(name) || + /\.(png|jpe?g|gif|webp|svg)$/i.test(url ?? "") + + const showImage = isImage && url && !errored + + return ( +
+ {showImage ? ( + + ) : ( + + )} + +
{name}
+
+ ) +}