diff --git a/src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx b/src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx index a1825165a77..c9eab01cdcf 100644 --- a/src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx +++ b/src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx @@ -112,6 +112,7 @@ describe('SidebarFeedbackDialog image submission', () => { }) await waitFor(() => expect((send as HTMLButtonElement).disabled).toBe(false)) + expect(screen.getByRole('button', { name: 'Remove shot.png' })).toBeTruthy() fireEvent.click(send) await waitFor(() => expect(mocks.submit).toHaveBeenCalledTimes(1)) expect(mocks.submit.mock.calls[0]?.[0].images).toEqual([ diff --git a/src/renderer/src/components/sidebar/SidebarFeedbackImageAttachments.tsx b/src/renderer/src/components/sidebar/SidebarFeedbackImageAttachments.tsx index f8560005d62..e06c904e84a 100644 --- a/src/renderer/src/components/sidebar/SidebarFeedbackImageAttachments.tsx +++ b/src/renderer/src/components/sidebar/SidebarFeedbackImageAttachments.tsx @@ -84,7 +84,8 @@ export function SidebarFeedbackImageAttachments({ type="button" aria-label={translate( 'auto.components.sidebar.SidebarFeedbackImageAttachments.removeImage', - 'Remove image' + 'Remove {{fileName}}', + { fileName: image.name } )} disabled={disabled} onClick={() => onRemove(image.id)} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 7ae60500674..82f36e8d41a 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -658,6 +658,18 @@ }, "pluginCommandKeybindings": { "group": "Plugins" + }, + "feedback": { + "image": { + "attachments": { + "fallbackName": "Image attachment", + "unsupportedType": "{{fileName}} is not a supported image type.", + "empty": "{{fileName}} is empty.", + "tooLarge": "{{fileName}} is larger than {{maxSize}}.", + "tooMany": "You can attach up to {{maxCount}} images.", + "additionalErrors": "{{count}} additional images could not be attached." + } + } } }, "hooks": { @@ -5025,7 +5037,7 @@ "SidebarFeedbackImageAttachments": { "screenshotsHint": "Attach up to {count} screenshots", "attachImages": "Attach", - "removeImage": "Remove image" + "removeImage": "Remove {{fileName}}" } }, "shared": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index aa162fdd2ee..763449df30d 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -635,6 +635,18 @@ }, "pluginCommandKeybindings": { "group": "Plugins" + }, + "feedback": { + "image": { + "attachments": { + "fallbackName": "Image attachment", + "unsupportedType": "{{fileName}} is not a supported image type.", + "empty": "{{fileName}} is empty.", + "tooLarge": "{{fileName}} is larger than {{maxSize}}.", + "tooMany": "You can attach up to {{maxCount}} images.", + "additionalErrors": "{{count}} additional images could not be attached." + } + } } }, "hooks": { @@ -5002,7 +5014,7 @@ "SidebarFeedbackImageAttachments": { "screenshotsHint": "Attach up to {count} screenshots", "attachImages": "Attach", - "removeImage": "Remove image" + "removeImage": "Remove {{fileName}}" } }, "shared": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 5cc6367a5ce..df7dac80988 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -635,6 +635,18 @@ }, "pluginCommandKeybindings": { "group": "Plugins" + }, + "feedback": { + "image": { + "attachments": { + "fallbackName": "Image attachment", + "unsupportedType": "{{fileName}} is not a supported image type.", + "empty": "{{fileName}} is empty.", + "tooLarge": "{{fileName}} is larger than {{maxSize}}.", + "tooMany": "You can attach up to {{maxCount}} images.", + "additionalErrors": "{{count}} additional images could not be attached." + } + } } }, "hooks": { @@ -5002,7 +5014,7 @@ "SidebarFeedbackImageAttachments": { "screenshotsHint": "Attach up to {count} screenshots", "attachImages": "Attach", - "removeImage": "Remove image" + "removeImage": "Remove {{fileName}}" } }, "shared": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 2a32923d257..424cace63db 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -635,6 +635,18 @@ }, "pluginCommandKeybindings": { "group": "Plugins" + }, + "feedback": { + "image": { + "attachments": { + "fallbackName": "Image attachment", + "unsupportedType": "{{fileName}} is not a supported image type.", + "empty": "{{fileName}} is empty.", + "tooLarge": "{{fileName}} is larger than {{maxSize}}.", + "tooMany": "You can attach up to {{maxCount}} images.", + "additionalErrors": "{{count}} additional images could not be attached." + } + } } }, "hooks": { @@ -5002,7 +5014,7 @@ "SidebarFeedbackImageAttachments": { "screenshotsHint": "Attach up to {count} screenshots", "attachImages": "Attach", - "removeImage": "Remove image" + "removeImage": "Remove {{fileName}}" } }, "shared": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index c33d51a0f04..b7fb100e2f5 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -635,6 +635,18 @@ }, "pluginCommandKeybindings": { "group": "Plugins" + }, + "feedback": { + "image": { + "attachments": { + "fallbackName": "Image attachment", + "unsupportedType": "{{fileName}} is not a supported image type.", + "empty": "{{fileName}} is empty.", + "tooLarge": "{{fileName}} is larger than {{maxSize}}.", + "tooMany": "You can attach up to {{maxCount}} images.", + "additionalErrors": "{{count}} additional images could not be attached." + } + } } }, "hooks": { @@ -5002,7 +5014,7 @@ "SidebarFeedbackImageAttachments": { "screenshotsHint": "Attach up to {count} screenshots", "attachImages": "Attach", - "removeImage": "Remove image" + "removeImage": "Remove {{fileName}}" } }, "shared": { diff --git a/src/renderer/src/lib/feedback-image-attachments.test.ts b/src/renderer/src/lib/feedback-image-attachments.test.ts index dcf11569940..353931d1742 100644 --- a/src/renderer/src/lib/feedback-image-attachments.test.ts +++ b/src/renderer/src/lib/feedback-image-attachments.test.ts @@ -68,6 +68,19 @@ describe('readFeedbackImageFiles', () => { expect(errors).toEqual(['notes.pdf is not a supported image type.']) }) + it('caps rejection detail so a large drop cannot mount one toast per file', async () => { + const files = Array.from( + { length: 100 }, + (_, index) => new File(['x'], `image-${index}.svg`, { type: 'image/svg+xml' }) + ) + + const { images, errors } = await readFeedbackImageFiles(files, 0) + + expect(images).toEqual([]) + expect(errors).toHaveLength(5) + expect(errors.at(-1)).toBe('96 additional images could not be attached.') + }) + it('reports an oversized image instead of skipping it', async () => { const { images, errors } = await readFeedbackImageFiles( [pngFile('huge.png', MAX_FEEDBACK_IMAGE_BYTES + 1)], diff --git a/src/renderer/src/lib/feedback-image-attachments.ts b/src/renderer/src/lib/feedback-image-attachments.ts index 78786e4d361..7e949005118 100644 --- a/src/renderer/src/lib/feedback-image-attachments.ts +++ b/src/renderer/src/lib/feedback-image-attachments.ts @@ -1,3 +1,4 @@ +import { translate } from '@/i18n/i18n' import { createBrowserUuid } from './browser-uuid' export const MAX_FEEDBACK_IMAGE_COUNT = 4 @@ -10,6 +11,7 @@ export const SUPPORTED_FEEDBACK_IMAGE_TYPES = [ ] as const export const FEEDBACK_IMAGE_FILE_ACCEPT = SUPPORTED_FEEDBACK_IMAGE_TYPES.join(',') +const MAX_FEEDBACK_IMAGE_DETAIL_ERRORS = 4 export type FeedbackImageDraft = { id: string @@ -50,6 +52,12 @@ export function formatFeedbackImageSize(bytes: number): string { : `${Math.max(1, Math.round(bytes / 1024))} KB` } +function feedbackImageDisplayName(file: File): string { + return ( + file.name || translate('auto.lib.feedback.image.attachments.fallbackName', 'Image attachment') + ) +} + /** * Converts picked/pasted/dropped files into drafts. Rejections come back as * messages rather than being skipped, because silently dropping an attachment @@ -62,25 +70,57 @@ export async function readFeedbackImageFiles( const images: FeedbackImageDraft[] = [] const errors: string[] = [] let remaining = MAX_FEEDBACK_IMAGE_COUNT - existingCount + let omittedErrorCount = 0 + const addError = (createMessage: () => string): void => { + if (errors.length < MAX_FEEDBACK_IMAGE_DETAIL_ERRORS) { + errors.push(createMessage()) + } else { + omittedErrorCount += 1 + } + } try { for (const file of files) { + const fileName = feedbackImageDisplayName(file) if (!isSupportedType(file.type)) { - errors.push(`${file.name || 'Image'} is not a supported image type.`) + addError(() => + translate( + 'auto.lib.feedback.image.attachments.unsupportedType', + '{{fileName}} is not a supported image type.', + { fileName } + ) + ) continue } if (file.size === 0) { - errors.push(`${file.name || 'Image'} is empty.`) + addError(() => + translate('auto.lib.feedback.image.attachments.empty', '{{fileName}} is empty.', { + fileName + }) + ) continue } if (file.size > MAX_FEEDBACK_IMAGE_BYTES) { - errors.push( - `${file.name || 'Image'} is larger than ${formatFeedbackImageSize(MAX_FEEDBACK_IMAGE_BYTES)}.` + addError(() => + translate( + 'auto.lib.feedback.image.attachments.tooLarge', + '{{fileName}} is larger than {{maxSize}}.', + { + fileName, + maxSize: formatFeedbackImageSize(MAX_FEEDBACK_IMAGE_BYTES) + } + ) ) continue } if (remaining <= 0) { - errors.push(`You can attach up to ${MAX_FEEDBACK_IMAGE_COUNT} images.`) + addError(() => + translate( + 'auto.lib.feedback.image.attachments.tooMany', + 'You can attach up to {{maxCount}} images.', + { maxCount: MAX_FEEDBACK_IMAGE_COUNT } + ) + ) break } remaining -= 1 @@ -88,7 +128,7 @@ export async function readFeedbackImageFiles( // Why: crypto.randomUUID is undefined in non-secure browser contexts (LAN // web client over plain HTTP); createBrowserUuid falls back safely. id: `${file.name}-${file.size}-${createBrowserUuid()}`, - name: file.name || 'pasted-image', + name: fileName, contentType: file.type, bytes: file.size, data: new Uint8Array(await file.arrayBuffer()), @@ -102,6 +142,16 @@ export async function readFeedbackImageFiles( throw error } + if (omittedErrorCount > 0) { + errors.push( + translate( + 'auto.lib.feedback.image.attachments.additionalErrors', + '{{count}} additional images could not be attached.', + { count: omittedErrorCount } + ) + ) + } + return { images, errors } }