From 5cf9751b4114596e83805f70ffc50a2798b7ba45 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 23 Mar 2026 17:33:39 +0200 Subject: [PATCH] feat(attachments): update TypeScript configuration and enhance attachment handling - Upgraded TypeScript target and library to ES2023 for improved language features. - Enhanced TeamProvisioningService to better handle plain text attachments with UTF-8 validation. - Improved TaskCommentInput to differentiate between image and non-image file previews, including a new FileIcon for non-image files. - Refactored attachment handling in useAttachments and useComposerDraft hooks to simplify file processing. - Added validation for empty files in attachmentUtils to improve user feedback on unsupported uploads. --- .../services/team/TeamProvisioningService.ts | 33 ++++++--- .../team/dialogs/TaskCommentInput.tsx | 68 ++++++++++++------- src/renderer/hooks/useAttachments.ts | 10 ++- src/renderer/hooks/useComposerDraft.ts | 10 ++- src/renderer/utils/attachmentUtils.ts | 3 + tsconfig.json | 4 +- 6 files changed, 80 insertions(+), 48 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index dcd1b195..69790de0 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -3624,18 +3624,33 @@ export class TeamProvisioningService { media_type: 'application/pdf', data: att.data, }, + title: att.filename, }); } else if (att.mimeType === 'text/plain') { // Text file → document block with text source (decode base64 → UTF-8) - contentBlocks.push({ - type: 'document', - source: { - type: 'text', - media_type: 'text/plain', - data: Buffer.from(att.data, 'base64').toString('utf-8'), - }, - title: att.filename, - }); + const decoded = Buffer.from(att.data, 'base64').toString('utf-8'); + if (decoded.includes('\uFFFD')) { + // Non-UTF-8 file: fallback to base64 document to avoid garbled content + contentBlocks.push({ + type: 'document', + source: { + type: 'base64', + media_type: 'text/plain', + data: att.data, + }, + title: att.filename, + }); + } else { + contentBlocks.push({ + type: 'document', + source: { + type: 'text', + media_type: 'text/plain', + data: decoded, + }, + title: att.filename, + }); + } } else { // Image (default) → image block contentBlocks.push({ diff --git a/src/renderer/components/team/dialogs/TaskCommentInput.tsx b/src/renderer/components/team/dialogs/TaskCommentInput.tsx index 5694fb7c..55cdcda6 100644 --- a/src/renderer/components/team/dialogs/TaskCommentInput.tsx +++ b/src/renderer/components/team/dialogs/TaskCommentInput.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox'; +import { FileIcon } from '@renderer/components/team/editor/FileIcon'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; @@ -19,7 +20,7 @@ import { stripEncodedTaskReferenceMetadata, } from '@renderer/utils/taskReferenceUtils'; import { MAX_TEXT_LENGTH } from '@shared/constants'; -import { categorizeFile, getEffectiveMimeType } from '@shared/constants/attachments'; +import { categorizeFile, getEffectiveMimeType, isImageMime } from '@shared/constants/attachments'; import { Mic, Paperclip, Send, Trash2, X } from 'lucide-react'; import type { MentionSuggestion } from '@renderer/types/mention'; @@ -254,25 +255,40 @@ export const TaskCommentInput = ({ {/* Pending attachment previews */} {pendingAttachments.length > 0 ? (
- {pendingAttachments.map((att, idx) => ( -
setLightboxIndex(idx)} - > - {att.filename} - -
- ))} + {isImage ? ( + {att.filename} + ) : ( +
+ + + {att.filename} + +
+ )} + +
+ ); + })} ) : null} @@ -280,13 +296,15 @@ export const TaskCommentInput = ({ setLightboxIndex(null)} - slides={pendingAttachments.map((att) => ({ - src: att.previewUrl, - alt: att.filename, - title: att.filename, - }))} + slides={pendingAttachments + .filter((att) => isImageMime(att.mimeType)) + .map((att) => ({ + src: att.previewUrl, + alt: att.filename, + title: att.filename, + }))} index={lightboxIndex} - showCounter={pendingAttachments.length > 1} + showCounter={pendingAttachments.filter((a) => isImageMime(a.mimeType)).length > 1} /> ) : null} diff --git a/src/renderer/hooks/useAttachments.ts b/src/renderer/hooks/useAttachments.ts index 07f024d7..48210114 100644 --- a/src/renderer/hooks/useAttachments.ts +++ b/src/renderer/hooks/useAttachments.ts @@ -270,19 +270,17 @@ export function useAttachments(options?: UseAttachmentsOptions): UseAttachmentsR const items = event.clipboardData?.items; if (!items) return; - const supportedFiles: File[] = []; + const pastedFiles: File[] = []; for (const item of Array.from(items)) { if (item.kind === 'file') { const file = item.getAsFile(); - if (file && categorizeFile(file) !== 'unsupported') { - supportedFiles.push(file); - } + if (file) pastedFiles.push(file); } } - if (supportedFiles.length > 0) { + if (pastedFiles.length > 0) { event.preventDefault(); - void addFiles(supportedFiles); + void addFiles(pastedFiles); } }, [addFiles] diff --git a/src/renderer/hooks/useComposerDraft.ts b/src/renderer/hooks/useComposerDraft.ts index 9b0554a0..81be33f6 100644 --- a/src/renderer/hooks/useComposerDraft.ts +++ b/src/renderer/hooks/useComposerDraft.ts @@ -420,19 +420,17 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult { const items = event.clipboardData?.items; if (!items) return; - const supportedFiles: File[] = []; + const pastedFiles: File[] = []; for (const item of Array.from(items)) { if (item.kind === 'file') { const file = item.getAsFile(); - if (file && categorizeFile(file) !== 'unsupported') { - supportedFiles.push(file); - } + if (file) pastedFiles.push(file); } } - if (supportedFiles.length > 0) { + if (pastedFiles.length > 0) { event.preventDefault(); - void addFiles(supportedFiles); + void addFiles(pastedFiles); } }, [addFiles] diff --git a/src/renderer/utils/attachmentUtils.ts b/src/renderer/utils/attachmentUtils.ts index d1ec8a85..9b23efcf 100644 --- a/src/renderer/utils/attachmentUtils.ts +++ b/src/renderer/utils/attachmentUtils.ts @@ -22,6 +22,9 @@ export function validateAttachment(file: File): { valid: true } | { valid: false if (cat === 'unsupported') { return { valid: false, error: `Unsupported file type: ${file.name}` }; } + if (file.size === 0) { + return { valid: false, error: `File "${file.name}" is empty` }; + } if (file.size > MAX_FILE_SIZE) { return { valid: false, error: `File "${file.name}" exceeds 10MB limit` }; } diff --git a/tsconfig.json b/tsconfig.json index 7711a5ed..d1b421bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2023", "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "jsx": "react-jsx", "strict": true, "esModuleInterop": true,