From acd2c4f5164d31529ddc0305a05fdabeabf2347c Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 25 Mar 2026 15:05:05 +0100 Subject: [PATCH 01/26] feat: track upload progress in attachment preview components --- .../AttachmentUploadProgressIndicator.tsx | 45 ++++++++++++++++ .../AudioAttachmentPreview.tsx | 33 ++++++++++-- .../FileAttachmentPreview.tsx | 33 ++++++++++-- .../MediaAttachmentPreview.tsx | 11 +++- .../utils/uploadProgress.ts | 50 +++++++++++++++++ .../__tests__/AttachmentPreviewList.test.tsx | 53 +++++++++++++++++++ .../__tests__/MessageInput.test.tsx | 16 ++++-- src/components/MessageComposer/icons.tsx | 53 +++++++++++++++++++ .../styling/AttachmentPreview.scss | 33 +++++++++++- src/context/ComponentContext.tsx | 3 ++ src/i18n/de.json | 1 + src/i18n/en.json | 1 + src/i18n/es.json | 1 + src/i18n/fr.json | 1 + src/i18n/hi.json | 1 + src/i18n/it.json | 1 + src/i18n/ja.json | 1 + src/i18n/ko.json | 1 + src/i18n/nl.json | 1 + src/i18n/pt.json | 1 + src/i18n/ru.json | 1 + src/i18n/tr.json | 1 + 22 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx create mode 100644 src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx new file mode 100644 index 0000000000..7d82134b7c --- /dev/null +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx @@ -0,0 +1,45 @@ +import clsx from 'clsx'; +import React, { type ReactNode } from 'react'; + +import { useComponentContext } from '../../../context'; +import { UploadProgress as DefaultUploadProgress, LoadingIndicatorIcon } from '../icons'; +import { clampUploadPercent } from './utils/uploadProgress'; + +export type AttachmentUploadProgressVariant = 'inline' | 'overlay'; + +export type AttachmentUploadProgressIndicatorProps = { + className?: string; + /** Shown when `uploadProgress` is `undefined` (e.g. progress tracking disabled). */ + fallback?: ReactNode; + uploadProgress?: number; + variant: AttachmentUploadProgressVariant; +}; + +export const AttachmentUploadProgressIndicator = ({ + className, + fallback, + uploadProgress, + variant, +}: AttachmentUploadProgressIndicatorProps) => { + const { UploadProgress = DefaultUploadProgress } = useComponentContext( + 'AttachmentUploadProgressIndicator', + ); + + if (uploadProgress === undefined) { + return <>{fallback ?? }; + } + + const percent = Math.round(clampUploadPercent(uploadProgress)); + + return ( +
+ +
+ ); +}; diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index 4c2d940352..41a4fc8060 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -7,7 +7,7 @@ import { import { useTranslationContext } from '../../../context'; import React, { useEffect } from 'react'; import clsx from 'clsx'; -import { LoadingIndicatorIcon } from '../icons'; +import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { FileSizeIndicator } from '../../Attachment'; @@ -21,6 +21,11 @@ import { } from '../../AudioPlayback'; import { useAudioPlayer } from '../../AudioPlayback/WithAudioPlayback'; import { useStateStore } from '../../../store'; +import { + formatUploadByteFraction, + readUploadProgress, + resolveAttachmentFullByteSize, +} from './utils/uploadProgress'; export type AudioAttachmentPreviewProps> = UploadAttachmentPreviewProps< @@ -44,6 +49,12 @@ export const AudioAttachmentPreview = ({ const { t } = useTranslationContext(); const { id, previewUri, uploadPermissionCheck, uploadState } = attachment.localMetadata ?? {}; + const uploadProgress = readUploadProgress(attachment.localMetadata); + const fullBytes = resolveAttachmentFullByteSize(attachment); + const showUploadFraction = + uploadState === 'uploading' && + uploadProgress !== undefined && + fullBytes !== undefined; const url = attachment.asset_url || previewUri; const audioPlayer = useAudioPlayer({ @@ -93,11 +104,27 @@ export const AudioAttachmentPreview = ({ {isVoiceRecordingAttachment(attachment) ? t('Voice message') : attachment.title}
- {uploadState === 'uploading' && } + {uploadState === 'uploading' && ( + + )} {showProgressControls ? ( <> {!resolvedDuration && !progressPercent && !isPlaying && ( - + <> + {showUploadFraction ? ( + + {formatUploadByteFraction(uploadProgress, fullBytes)} + + ) : ( + + )} + )} {hasWaveform ? ( <> diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index 01103c5932..0d2b02fa67 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -1,12 +1,16 @@ import React from 'react'; import { useTranslationContext } from '../../../context'; import { FileIcon } from '../../FileIcon'; -import { LoadingIndicatorIcon } from '../icons'; - +import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import type { LocalAudioAttachment, LocalFileAttachment } from 'stream-chat'; import type { UploadAttachmentPreviewProps } from './types'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; +import { + formatUploadByteFraction, + readUploadProgress, + resolveAttachmentFullByteSize, +} from './utils/uploadProgress'; import { FileSizeIndicator } from '../../Attachment'; import { IconExclamationMark, IconExclamationTriangleFill } from '../../Icons'; @@ -22,6 +26,12 @@ export const FileAttachmentPreview = ({ }: FileAttachmentPreviewProps) => { const { t } = useTranslationContext('FilePreview'); const { id, uploadPermissionCheck, uploadState } = attachment.localMetadata ?? {}; + const uploadProgress = readUploadProgress(attachment.localMetadata); + const fullBytes = resolveAttachmentFullByteSize(attachment); + const showUploadFraction = + uploadState === 'uploading' && + uploadProgress !== undefined && + fullBytes !== undefined; const hasSizeLimitError = uploadPermissionCheck?.reason === 'size_limit'; const hasFatalError = uploadState === 'blocked' || hasSizeLimitError; @@ -43,8 +53,23 @@ export const FileAttachmentPreview = ({ {attachment.title}
- {uploadState === 'uploading' && } - {!hasError && } + {uploadState === 'uploading' && ( + + )} + {!hasError && showUploadFraction && ( + + {formatUploadByteFraction(uploadProgress, fullBytes)} + + )} + {!hasError && !showUploadFraction && ( + + )} {hasFatalError && (
diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index 8336ab399b..f613dd858d 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -17,8 +17,10 @@ import clsx from 'clsx'; import { IconExclamationMark, IconRetry, IconVideoFill } from '../../Icons'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { Button } from '../../Button'; +import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { LoadingIndicator as DefaultLoadingIndicator } from '../../Loading'; +import { readUploadProgress } from './utils/uploadProgress'; export type MediaAttachmentPreviewProps> = UploadAttachmentPreviewProps< @@ -39,6 +41,7 @@ export const MediaAttachmentPreview = ({ const [thumbnailPreviewError, setThumbnailPreviewError] = useState(false); const { id, uploadPermissionCheck, uploadState } = attachment.localMetadata ?? {}; + const uploadProgress = readUploadProgress(attachment.localMetadata); const isUploading = uploadState === 'uploading'; const handleThumbnailLoadError = useCallback(() => setThumbnailPreviewError(true), []); @@ -94,7 +97,13 @@ export const MediaAttachmentPreview = ({ )}
- {isUploading && } + {isUploading && ( + } + uploadProgress={uploadProgress} + variant='overlay' + /> + )} {isVideoAttachment(attachment) && !hasUploadError && diff --git a/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts b/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts new file mode 100644 index 0000000000..6f9e7acca2 --- /dev/null +++ b/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts @@ -0,0 +1,50 @@ +import { prettifyFileSize } from '../../hooks/utils'; + +export function readUploadProgress( + localMetadata: { uploadProgress?: unknown } | null | undefined, +): number | undefined { + if (!localMetadata) return undefined; + const { uploadProgress } = localMetadata; + if (uploadProgress === undefined) return undefined; + if (typeof uploadProgress !== 'number' || !Number.isFinite(uploadProgress)) + return undefined; + return uploadProgress; +} + +export function clampUploadPercent(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.min(100, Math.max(0, value)); +} + +function safePrettifyFileSize(bytes: number, maximumFractionDigits?: number): string { + if (!Number.isFinite(bytes) || bytes < 0) return ''; + if (bytes === 0) return '0 B'; + return prettifyFileSize(bytes, maximumFractionDigits); +} + +export function formatUploadByteFraction( + uploadPercent: number, + fullBytes: number, + maximumFractionDigits?: number, +): string { + const clamped = clampUploadPercent(uploadPercent); + const uploaded = Math.round((clamped / 100) * fullBytes); + return `${safePrettifyFileSize(uploaded, maximumFractionDigits)} / ${safePrettifyFileSize(fullBytes, maximumFractionDigits)}`; +} + +export function resolveAttachmentFullByteSize(attachment: { + file_size?: number | string; + localMetadata?: { file?: { size?: unknown } } | null; +}): number | undefined { + const fromFile = attachment.localMetadata?.file?.size; + if (typeof fromFile === 'number' && Number.isFinite(fromFile) && fromFile >= 0) { + return fromFile; + } + const raw = attachment.file_size; + if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw; + if (typeof raw === 'string') { + const n = parseFloat(raw); + if (Number.isFinite(n) && n >= 0) return n; + } + return undefined; +} diff --git a/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx b/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx index 88bde92238..c00059efd0 100644 --- a/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx +++ b/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx @@ -341,6 +341,59 @@ describe('AttachmentPreviewList', () => { }, ); + describe('upload progress UI', () => { + it('shows spinner while uploading when uploadProgress is omitted', async () => { + await renderComponent({ + attachments: [ + { + ...generateFileAttachment({ title: 'f.pdf' }), + localMetadata: { id: 'a1', uploadState: 'uploading' }, + }, + ], + }); + + expect(screen.getByTestId(LOADING_INDICATOR_TEST_ID)).toBeInTheDocument(); + expect( + screen.queryByTestId('attachment-upload-progress-ring'), + ).not.toBeInTheDocument(); + }); + + it('shows ring while uploading when uploadProgress is numeric', async () => { + await renderComponent({ + attachments: [ + { + ...generateImageAttachment({ fallback: 'img.png' }), + localMetadata: { + id: 'a1', + uploadProgress: 42, + uploadState: 'uploading', + }, + }, + ], + }); + + expect(screen.getByTestId('attachment-upload-progress-ring')).toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_INDICATOR_TEST_ID)).not.toBeInTheDocument(); + }); + + it('shows uploaded size fraction for file attachments when progress is tracked', async () => { + await renderComponent({ + attachments: [ + { + ...generateFileAttachment({ file_size: 1000, title: 'sized.pdf' }), + localMetadata: { + id: 'a1', + uploadProgress: 50, + uploadState: 'uploading', + }, + }, + ], + }); + + expect(screen.getByTestId('upload-size-fraction')).toHaveTextContent(/\s*\/\s*/); + }); + }); + it('should render custom BaseImage component', async () => { const BaseImage = (props) => ; const { container } = await renderComponent({ diff --git a/src/components/MessageComposer/__tests__/MessageInput.test.tsx b/src/components/MessageComposer/__tests__/MessageInput.test.tsx index 320ba6e858..7a6dd04448 100644 --- a/src/components/MessageComposer/__tests__/MessageInput.test.tsx +++ b/src/components/MessageComposer/__tests__/MessageInput.test.tsx @@ -345,6 +345,16 @@ const setupUploadRejected = async (error: unknown) => { return { customChannel, customClient, sendFileSpy, sendImageSpy }; }; +/** `channel.sendImage` / `channel.sendFile` pass upload options (e.g. `onUploadProgress`) after the file. */ +const expectChannelUploadCall = (spy, expectedFile) => { + expect(spy).toHaveBeenCalled(); + const callArgs = spy.mock.calls[0]; + expect(callArgs[0]).toBe(expectedFile); + expect(callArgs[callArgs.length - 1]).toEqual( + expect.objectContaining({ onUploadProgress: expect.any(Function) }), + ); +}; + const renderWithActiveCooldown = async ({ messageInputProps = {} } = {}) => { const { channels: [channel], @@ -562,8 +572,8 @@ describe(`MessageInputFlat`, () => { }); const filenameTexts = await screen.findAllByTitle(filename); await waitFor(() => { - expect(sendFileSpy).toHaveBeenCalledWith(file); - expect(sendImageSpy).toHaveBeenCalledWith(image); + expectChannelUploadCall(sendFileSpy, file); + expectChannelUploadCall(sendImageSpy, image); expect(screen.getByTestId(IMAGE_PREVIEW_TEST_ID)).toBeInTheDocument(); expect(screen.getByTestId(FILE_PREVIEW_TEST_ID)).toBeInTheDocument(); filenameTexts.forEach((filenameText) => expect(filenameText).toBeInTheDocument()); @@ -634,7 +644,7 @@ describe(`MessageInputFlat`, () => { dropFile(file, formElement); }); await waitFor(() => { - expect(sendImageSpy).toHaveBeenCalledWith(file); + expectChannelUploadCall(sendImageSpy, file); }); const results = await axe(container); expect(results).toHaveNoViolations(); diff --git a/src/components/MessageComposer/icons.tsx b/src/components/MessageComposer/icons.tsx index 94b8de7037..ae8dc9671f 100644 --- a/src/components/MessageComposer/icons.tsx +++ b/src/components/MessageComposer/icons.tsx @@ -3,6 +3,59 @@ import { nanoid } from 'nanoid'; import { useTranslationContext } from '../../context/TranslationContext'; +const UPLOAD_RING_RADIUS = 12; +const UPLOAD_RING_CIRCUMFERENCE = 2 * Math.PI * UPLOAD_RING_RADIUS; + +export type UploadProgressProps = { + /** Clamped 0–100 upload completion. */ + percent: number; +}; + +/** Determinate circular upload progress ring (not a spinner; uses `str-chat__upload-progress`, not the loading-indicator spinner). */ +export const UploadProgress = ({ percent }: UploadProgressProps) => { + const { t } = useTranslationContext('UploadProgress'); + const dashOffset = UPLOAD_RING_CIRCUMFERENCE * (1 - percent / 100); + + return ( +
+ + + + +
+ ); +}; + export const LoadingIndicatorIcon = () => { const id = useMemo(() => nanoid(), []); diff --git a/src/components/MessageComposer/styling/AttachmentPreview.scss b/src/components/MessageComposer/styling/AttachmentPreview.scss index 5ca9af3165..046db91c38 100644 --- a/src/components/MessageComposer/styling/AttachmentPreview.scss +++ b/src/components/MessageComposer/styling/AttachmentPreview.scss @@ -76,6 +76,19 @@ --str-chat__attachment-preview-file-fatal-error-color: var(--color-accent-error); + .str-chat__attachment-upload-progress { + color: var(--str-chat__primary-color); + + .str-chat__upload-progress { + width: 100%; + height: 100%; + } + + svg { + display: block; + } + } + .str-chat__message-composer-voice-preview-slot { display: flex; align-items: center; @@ -210,7 +223,9 @@ ); } - .str-chat__icon.str-chat__loading-indicator { + .str-chat__loading-indicator, + .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--overlay, + .str-chat__icon--exclamation-circle { width: var(--icon-size-sm); height: var(--icon-size-sm); position: absolute; @@ -234,6 +249,11 @@ background: var(--badge-bg-error); color: var(--badge-text-on-accent); } + + .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--overlay { + border: none; + border-radius: 0; + } } .str-chat__attachment-preview-media--upload-error { @@ -283,11 +303,20 @@ font-size: var(--typography-font-size-xs); line-height: var(--typography-line-height-tight); - .str-chat__icon.str-chat__loading-indicator { + .str-chat__loading-indicator, + .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--inline { width: var(--icon-size-sm); height: var(--icon-size-sm); } + .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--inline { + flex-shrink: 0; + } + + .str-chat__attachment-preview-file__upload-size-fraction { + white-space: nowrap; + } + .str-chat__attachment-preview-file__fatal-error { display: flex; align-items: center; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index d2466c012e..1122c5e232 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -74,6 +74,7 @@ import type { VideoPlayerProps } from '../components/VideoPlayer'; import type { EditedMessagePreviewProps } from '../components/MessageComposer/EditedMessagePreview'; import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; +import type { UploadProgressProps } from '../components/MessageComposer/icons'; export type ComponentContextValue = { /** Custom UI component to display additional message composer action buttons left to the textarea, defaults to and accepts same props as: [AdditionalMessageComposerActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/MessageComposerActions.tsx) */ @@ -148,6 +149,8 @@ export type ComponentContextValue = { LoadingErrorIndicator?: React.ComponentType; /** Custom UI component to render while the `MessageList` is loading new messages, defaults to and accepts same props as: [LoadingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/LoadingIndicator.tsx) */ LoadingIndicator?: React.ComponentType; + /** Custom UI component for determinate attachment upload progress (0–100) in attachment previews, defaults to and accepts same props as: [UploadProgress](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/icons.tsx) */ + UploadProgress?: React.ComponentType; /** Custom UI component to display a message in the standard `MessageList`, defaults to and accepts the same props as: [MessageUI](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageUI.tsx) */ Message?: React.ComponentType; /** Custom UI component for message actions popup, accepts no props, all the defaults are set within [MessageActions (unstable)](https://github.com/GetStream/stream-chat-react/blob/master/src/experimental/MessageActions/MessageActions.tsx) */ diff --git a/src/i18n/de.json b/src/i18n/de.json index b43c4eb31b..980fafd27c 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -102,6 +102,7 @@ "aria/Unblock User": "Benutzer entsperren", "aria/Unmute User": "Stummschaltung aufheben", "aria/Unpin Message": "Anheftung aufheben", + "aria/Upload progress": "{{percent}} Prozent hochgeladen", "Ask a question": "Eine Frage stellen", "Attach": "Anhängen", "Attach files": "Dateien anhängen", diff --git a/src/i18n/en.json b/src/i18n/en.json index 0cea529a0b..077ff962ee 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -102,6 +102,7 @@ "aria/Unblock User": "Unblock User", "aria/Unmute User": "Unmute User", "aria/Unpin Message": "Unpin Message", + "aria/Upload progress": "{{percent}} percent uploaded", "Ask a question": "Ask a Question", "Attach": "Attach", "Attach files": "Attach files", diff --git a/src/i18n/es.json b/src/i18n/es.json index 88104736d9..4be4f3a075 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -110,6 +110,7 @@ "aria/Unblock User": "Desbloquear usuario", "aria/Unmute User": "Activar sonido", "aria/Unpin Message": "Desfijar mensaje", + "aria/Upload progress": "{{percent}} por ciento cargado", "Ask a question": "Hacer una pregunta", "Attach": "Adjuntar", "Attach files": "Adjuntar archivos", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index f63942d54a..17cf966fb3 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -110,6 +110,7 @@ "aria/Unblock User": "Débloquer l'utilisateur", "aria/Unmute User": "Désactiver muet", "aria/Unpin Message": "Détacher le message", + "aria/Upload progress": "{{percent}} pour cent téléchargés", "Ask a question": "Poser une question", "Attach": "Joindre", "Attach files": "Joindre des fichiers", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index ad2da9d823..df6222306f 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -102,6 +102,7 @@ "aria/Unblock User": "उपयोगकर्ता अनब्लॉक करें", "aria/Unmute User": "उपयोगकर्ता अनम्यूट करें", "aria/Unpin Message": "संदेश अनपिन करें", + "aria/Upload progress": "{{percent}} प्रतिशत अपलोड हो गया", "Ask a question": "एक प्रश्न पूछें", "Attach": "संलग्न करें", "Attach files": "फाइल्स अटैच करे", diff --git a/src/i18n/it.json b/src/i18n/it.json index 9204470ceb..a65c6381fd 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -110,6 +110,7 @@ "aria/Unblock User": "Sblocca utente", "aria/Unmute User": "Riattiva il notifiche", "aria/Unpin Message": "Rimuovi messaggio appuntato", + "aria/Upload progress": "{{percent}} percento caricato", "Ask a question": "Fai una domanda", "Attach": "Allega", "Attach files": "Allega file", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 063b2c0cc2..9767bbe42f 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -101,6 +101,7 @@ "aria/Unblock User": "ユーザーのブロックを解除", "aria/Unmute User": "無音を解除する", "aria/Unpin Message": "ピンを解除", + "aria/Upload progress": "{{percent}}パーセントアップロード済み", "Ask a question": "質問する", "Attach": "添付", "Attach files": "ファイルを添付する", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 069d865b86..3a5f1a9d2b 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -101,6 +101,7 @@ "aria/Unblock User": "사용자 차단 해제", "aria/Unmute User": "음소거 해제", "aria/Unpin Message": "핀 해제", + "aria/Upload progress": "{{percent}}퍼센트 업로드됨", "Ask a question": "질문하기", "Attach": "첨부", "Attach files": "파일 첨부", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 1f64ceedf8..4c20a3b762 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -102,6 +102,7 @@ "aria/Unblock User": "Gebruiker deblokkeren", "aria/Unmute User": "Dempen opheffen", "aria/Unpin Message": "Losmaken", + "aria/Upload progress": "{{percent}} procent geüpload", "Ask a question": "Stel een vraag", "Attach": "Bijvoegen", "Attach files": "Bijlage toevoegen", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 0d01d06093..93f1274dae 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -110,6 +110,7 @@ "aria/Unblock User": "Desbloquear usuário", "aria/Unmute User": "Ativar som", "aria/Unpin Message": "Desfixar mensagem", + "aria/Upload progress": "{{percent}} por cento carregado", "Ask a question": "Faça uma pergunta", "Attach": "Anexar", "Attach files": "Anexar arquivos", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 068ae9070a..68bb9fcd03 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -119,6 +119,7 @@ "aria/Unblock User": "Разблокировать пользователя", "aria/Unmute User": "Включить уведомления", "aria/Unpin Message": "Открепить сообщение", + "aria/Upload progress": "{{percent}} процентов загружено", "Ask a question": "Задать вопрос", "Attach": "Прикрепить", "Attach files": "Прикрепить файлы", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 0576ebd10c..60d6ff1ece 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -102,6 +102,7 @@ "aria/Unblock User": "Kullanıcının engelini kaldır", "aria/Unmute User": "Sesini aç", "aria/Unpin Message": "Sabitlemeyi kaldır", + "aria/Upload progress": "Yüzde {{percent}} karşıya yüklendi", "Ask a question": "Bir soru sor", "Attach": "Ekle", "Attach files": "Dosya ekle", From b1a787c8e3b0bfe9b054d5650afb72842024fc5f Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 26 Mar 2026 15:43:09 +0100 Subject: [PATCH 02/26] refactor: uploadProgress renamed to Progress and moved outside of message composer --- src/components/Loading/ProgressIndicator.tsx | 56 +++++++++++++++++++ src/components/Loading/index.ts | 1 + .../Loading/styling/ProgressIndicator.scss | 8 +++ src/components/Loading/styling/index.scss | 1 + .../AttachmentUploadProgressIndicator.tsx | 7 ++- .../__tests__/AttachmentPreviewList.test.tsx | 6 +- src/components/MessageComposer/icons.tsx | 53 ------------------ .../styling/AttachmentPreview.scss | 9 --- src/context/ComponentContext.tsx | 6 +- src/i18n/de.json | 2 +- src/i18n/en.json | 2 +- src/i18n/es.json | 2 +- src/i18n/fr.json | 2 +- src/i18n/hi.json | 2 +- src/i18n/it.json | 2 +- src/i18n/ja.json | 2 +- src/i18n/ko.json | 2 +- src/i18n/nl.json | 2 +- src/i18n/pt.json | 2 +- src/i18n/ru.json | 2 +- src/i18n/tr.json | 2 +- 21 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 src/components/Loading/ProgressIndicator.tsx create mode 100644 src/components/Loading/styling/ProgressIndicator.scss diff --git a/src/components/Loading/ProgressIndicator.tsx b/src/components/Loading/ProgressIndicator.tsx new file mode 100644 index 0000000000..08ee89f81f --- /dev/null +++ b/src/components/Loading/ProgressIndicator.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { useTranslationContext } from '../../context/TranslationContext'; + +const RING_RADIUS = 12; +const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS; + +export type ProgressIndicatorProps = { + /** Clamped 0–100 completion. */ + percent: number; +}; + +/** Circular progress indicator with input from 0 to 100. */ +export const ProgressIndicator = ({ percent }: ProgressIndicatorProps) => { + const { t } = useTranslationContext('ProgressIndicator'); + const dashOffset = RING_CIRCUMFERENCE * (1 - percent / 100); + + return ( +
+ + + + +
+ ); +}; diff --git a/src/components/Loading/index.ts b/src/components/Loading/index.ts index 75b5c332f4..65c86cdc64 100644 --- a/src/components/Loading/index.ts +++ b/src/components/Loading/index.ts @@ -2,3 +2,4 @@ export * from './LoadingChannel'; export * from './LoadingChannels'; export * from './LoadingErrorIndicator'; export * from './LoadingIndicator'; +export * from './ProgressIndicator'; diff --git a/src/components/Loading/styling/ProgressIndicator.scss b/src/components/Loading/styling/ProgressIndicator.scss new file mode 100644 index 0000000000..b029177fa3 --- /dev/null +++ b/src/components/Loading/styling/ProgressIndicator.scss @@ -0,0 +1,8 @@ +.str-chat__progress-indicator { + width: 100%; + height: 100%; + + svg { + display: block; + } +} diff --git a/src/components/Loading/styling/index.scss b/src/components/Loading/styling/index.scss index 1a37d43288..e3b441c010 100644 --- a/src/components/Loading/styling/index.scss +++ b/src/components/Loading/styling/index.scss @@ -1,2 +1,3 @@ @use 'LoadingChannels'; @use 'LoadingIndicator'; +@use 'ProgressIndicator'; diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx index 7d82134b7c..736f7d431c 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx @@ -1,8 +1,9 @@ import clsx from 'clsx'; import React, { type ReactNode } from 'react'; +import { ProgressIndicator as DefaultProgressIndicator } from '../../Loading'; import { useComponentContext } from '../../../context'; -import { UploadProgress as DefaultUploadProgress, LoadingIndicatorIcon } from '../icons'; +import { LoadingIndicatorIcon } from '../icons'; import { clampUploadPercent } from './utils/uploadProgress'; export type AttachmentUploadProgressVariant = 'inline' | 'overlay'; @@ -21,7 +22,7 @@ export const AttachmentUploadProgressIndicator = ({ uploadProgress, variant, }: AttachmentUploadProgressIndicatorProps) => { - const { UploadProgress = DefaultUploadProgress } = useComponentContext( + const { ProgressIndicator = DefaultProgressIndicator } = useComponentContext( 'AttachmentUploadProgressIndicator', ); @@ -39,7 +40,7 @@ export const AttachmentUploadProgressIndicator = ({ className, )} > - +
); }; diff --git a/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx b/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx index c00059efd0..37a9a8d1da 100644 --- a/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx +++ b/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx @@ -353,9 +353,7 @@ describe('AttachmentPreviewList', () => { }); expect(screen.getByTestId(LOADING_INDICATOR_TEST_ID)).toBeInTheDocument(); - expect( - screen.queryByTestId('attachment-upload-progress-ring'), - ).not.toBeInTheDocument(); + expect(screen.queryByTestId('progress-ring')).not.toBeInTheDocument(); }); it('shows ring while uploading when uploadProgress is numeric', async () => { @@ -372,7 +370,7 @@ describe('AttachmentPreviewList', () => { ], }); - expect(screen.getByTestId('attachment-upload-progress-ring')).toBeInTheDocument(); + expect(screen.getByTestId('progress-ring')).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_INDICATOR_TEST_ID)).not.toBeInTheDocument(); }); diff --git a/src/components/MessageComposer/icons.tsx b/src/components/MessageComposer/icons.tsx index ae8dc9671f..94b8de7037 100644 --- a/src/components/MessageComposer/icons.tsx +++ b/src/components/MessageComposer/icons.tsx @@ -3,59 +3,6 @@ import { nanoid } from 'nanoid'; import { useTranslationContext } from '../../context/TranslationContext'; -const UPLOAD_RING_RADIUS = 12; -const UPLOAD_RING_CIRCUMFERENCE = 2 * Math.PI * UPLOAD_RING_RADIUS; - -export type UploadProgressProps = { - /** Clamped 0–100 upload completion. */ - percent: number; -}; - -/** Determinate circular upload progress ring (not a spinner; uses `str-chat__upload-progress`, not the loading-indicator spinner). */ -export const UploadProgress = ({ percent }: UploadProgressProps) => { - const { t } = useTranslationContext('UploadProgress'); - const dashOffset = UPLOAD_RING_CIRCUMFERENCE * (1 - percent / 100); - - return ( -
- - - - -
- ); -}; - export const LoadingIndicatorIcon = () => { const id = useMemo(() => nanoid(), []); diff --git a/src/components/MessageComposer/styling/AttachmentPreview.scss b/src/components/MessageComposer/styling/AttachmentPreview.scss index 046db91c38..d1a119c3d8 100644 --- a/src/components/MessageComposer/styling/AttachmentPreview.scss +++ b/src/components/MessageComposer/styling/AttachmentPreview.scss @@ -78,15 +78,6 @@ .str-chat__attachment-upload-progress { color: var(--str-chat__primary-color); - - .str-chat__upload-progress { - width: 100%; - height: 100%; - } - - svg { - display: block; - } } .str-chat__message-composer-voice-preview-slot { diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 1122c5e232..d5c7926c74 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -74,7 +74,7 @@ import type { VideoPlayerProps } from '../components/VideoPlayer'; import type { EditedMessagePreviewProps } from '../components/MessageComposer/EditedMessagePreview'; import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; -import type { UploadProgressProps } from '../components/MessageComposer/icons'; +import type { ProgressIndicatorProps } from '../components/Loading/ProgressIndicator'; export type ComponentContextValue = { /** Custom UI component to display additional message composer action buttons left to the textarea, defaults to and accepts same props as: [AdditionalMessageComposerActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/MessageComposerActions.tsx) */ @@ -149,8 +149,8 @@ export type ComponentContextValue = { LoadingErrorIndicator?: React.ComponentType; /** Custom UI component to render while the `MessageList` is loading new messages, defaults to and accepts same props as: [LoadingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/LoadingIndicator.tsx) */ LoadingIndicator?: React.ComponentType; - /** Custom UI component for determinate attachment upload progress (0–100) in attachment previews, defaults to and accepts same props as: [UploadProgress](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/icons.tsx) */ - UploadProgress?: React.ComponentType; + /** Custom UI component for determinate progress (0–100), defaults to and accepts same props as: [ProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/ProgressIndicator.tsx) */ + ProgressIndicator?: React.ComponentType; /** Custom UI component to display a message in the standard `MessageList`, defaults to and accepts the same props as: [MessageUI](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageUI.tsx) */ Message?: React.ComponentType; /** Custom UI component for message actions popup, accepts no props, all the defaults are set within [MessageActions (unstable)](https://github.com/GetStream/stream-chat-react/blob/master/src/experimental/MessageActions/MessageActions.tsx) */ diff --git a/src/i18n/de.json b/src/i18n/de.json index 980fafd27c..7a89a9d386 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -84,6 +84,7 @@ "aria/Open Reaction Selector": "Reaktionsauswahl öffnen", "aria/Open Thread": "Thread öffnen", "aria/Pin Message": "Nachricht anheften", + "aria/Progress": "{{percent}} Prozent abgeschlossen", "aria/Quote Message": "Nachricht zitieren", "aria/Reaction list": "Reaktionsliste", "aria/Remind Me Message": "Erinnern", @@ -102,7 +103,6 @@ "aria/Unblock User": "Benutzer entsperren", "aria/Unmute User": "Stummschaltung aufheben", "aria/Unpin Message": "Anheftung aufheben", - "aria/Upload progress": "{{percent}} Prozent hochgeladen", "Ask a question": "Eine Frage stellen", "Attach": "Anhängen", "Attach files": "Dateien anhängen", diff --git a/src/i18n/en.json b/src/i18n/en.json index 077ff962ee..b714fa8d18 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -84,6 +84,7 @@ "aria/Open Reaction Selector": "Open Reaction Selector", "aria/Open Thread": "Open Thread", "aria/Pin Message": "Pin Message", + "aria/Progress": "{{percent}} percent complete", "aria/Quote Message": "Quote Message", "aria/Reaction list": "Reaction list", "aria/Remind Me Message": "Remind Me Message", @@ -102,7 +103,6 @@ "aria/Unblock User": "Unblock User", "aria/Unmute User": "Unmute User", "aria/Unpin Message": "Unpin Message", - "aria/Upload progress": "{{percent}} percent uploaded", "Ask a question": "Ask a Question", "Attach": "Attach", "Attach files": "Attach files", diff --git a/src/i18n/es.json b/src/i18n/es.json index 4be4f3a075..f15763c177 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -92,6 +92,7 @@ "aria/Open Reaction Selector": "Abrir selector de reacciones", "aria/Open Thread": "Abrir hilo", "aria/Pin Message": "Fijar mensaje", + "aria/Progress": "{{percent}} por ciento completado", "aria/Quote Message": "Citar mensaje", "aria/Reaction list": "Lista de reacciones", "aria/Remind Me Message": "Recordarme", @@ -110,7 +111,6 @@ "aria/Unblock User": "Desbloquear usuario", "aria/Unmute User": "Activar sonido", "aria/Unpin Message": "Desfijar mensaje", - "aria/Upload progress": "{{percent}} por ciento cargado", "Ask a question": "Hacer una pregunta", "Attach": "Adjuntar", "Attach files": "Adjuntar archivos", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 17cf966fb3..9eda19a641 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -92,6 +92,7 @@ "aria/Open Reaction Selector": "Ouvrir le sélecteur de réactions", "aria/Open Thread": "Ouvrir le fil", "aria/Pin Message": "Épingler le message", + "aria/Progress": "{{percent}} pour cent terminé", "aria/Quote Message": "Citer le message", "aria/Reaction list": "Liste des réactions", "aria/Remind Me Message": "Me rappeler", @@ -110,7 +111,6 @@ "aria/Unblock User": "Débloquer l'utilisateur", "aria/Unmute User": "Désactiver muet", "aria/Unpin Message": "Détacher le message", - "aria/Upload progress": "{{percent}} pour cent téléchargés", "Ask a question": "Poser une question", "Attach": "Joindre", "Attach files": "Joindre des fichiers", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index df6222306f..f1f0bfa85a 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -84,6 +84,7 @@ "aria/Open Reaction Selector": "प्रतिक्रिया चयनकर्ता खोलें", "aria/Open Thread": "थ्रेड खोलें", "aria/Pin Message": "संदेश पिन करें", + "aria/Progress": "{{percent}} प्रतिशत पूर्ण", "aria/Quote Message": "संदेश उद्धरण", "aria/Reaction list": "प्रतिक्रिया सूची", "aria/Remind Me Message": "मुझे याद दिलाएं", @@ -102,7 +103,6 @@ "aria/Unblock User": "उपयोगकर्ता अनब्लॉक करें", "aria/Unmute User": "उपयोगकर्ता अनम्यूट करें", "aria/Unpin Message": "संदेश अनपिन करें", - "aria/Upload progress": "{{percent}} प्रतिशत अपलोड हो गया", "Ask a question": "एक प्रश्न पूछें", "Attach": "संलग्न करें", "Attach files": "फाइल्स अटैच करे", diff --git a/src/i18n/it.json b/src/i18n/it.json index a65c6381fd..db48024ae0 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -92,6 +92,7 @@ "aria/Open Reaction Selector": "Apri il selettore di reazione", "aria/Open Thread": "Apri discussione", "aria/Pin Message": "Appunta messaggio", + "aria/Progress": "{{percent}} percento completato", "aria/Quote Message": "Citazione messaggio", "aria/Reaction list": "Elenco delle reazioni", "aria/Remind Me Message": "Ricordami", @@ -110,7 +111,6 @@ "aria/Unblock User": "Sblocca utente", "aria/Unmute User": "Riattiva il notifiche", "aria/Unpin Message": "Rimuovi messaggio appuntato", - "aria/Upload progress": "{{percent}} percento caricato", "Ask a question": "Fai una domanda", "Attach": "Allega", "Attach files": "Allega file", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 9767bbe42f..da4796d929 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -83,6 +83,7 @@ "aria/Open Reaction Selector": "リアクションセレクターを開く", "aria/Open Thread": "スレッドを開く", "aria/Pin Message": "メッセージをピン", + "aria/Progress": "{{percent}}パーセント完了", "aria/Quote Message": "メッセージを引用", "aria/Reaction list": "リアクション一覧", "aria/Remind Me Message": "リマインダー", @@ -101,7 +102,6 @@ "aria/Unblock User": "ユーザーのブロックを解除", "aria/Unmute User": "無音を解除する", "aria/Unpin Message": "ピンを解除", - "aria/Upload progress": "{{percent}}パーセントアップロード済み", "Ask a question": "質問する", "Attach": "添付", "Attach files": "ファイルを添付する", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 3a5f1a9d2b..866dd24679 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -83,6 +83,7 @@ "aria/Open Reaction Selector": "반응 선택기 열기", "aria/Open Thread": "스레드 열기", "aria/Pin Message": "메시지 고정", + "aria/Progress": "{{percent}}퍼센트 완료", "aria/Quote Message": "메시지 인용", "aria/Reaction list": "반응 목록", "aria/Remind Me Message": "알림 설정", @@ -101,7 +102,6 @@ "aria/Unblock User": "사용자 차단 해제", "aria/Unmute User": "음소거 해제", "aria/Unpin Message": "핀 해제", - "aria/Upload progress": "{{percent}}퍼센트 업로드됨", "Ask a question": "질문하기", "Attach": "첨부", "Attach files": "파일 첨부", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 4c20a3b762..e08873ee1b 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -84,6 +84,7 @@ "aria/Open Reaction Selector": "Reactiekiezer openen", "aria/Open Thread": "Draad openen", "aria/Pin Message": "Bericht vastmaken", + "aria/Progress": "{{percent}} procent voltooid", "aria/Quote Message": "Bericht citeren", "aria/Reaction list": "Reactielijst", "aria/Remind Me Message": "Herinner mij", @@ -102,7 +103,6 @@ "aria/Unblock User": "Gebruiker deblokkeren", "aria/Unmute User": "Dempen opheffen", "aria/Unpin Message": "Losmaken", - "aria/Upload progress": "{{percent}} procent geüpload", "Ask a question": "Stel een vraag", "Attach": "Bijvoegen", "Attach files": "Bijlage toevoegen", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 93f1274dae..3ad2f1c021 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -92,6 +92,7 @@ "aria/Open Reaction Selector": "Abrir seletor de reações", "aria/Open Thread": "Abrir tópico", "aria/Pin Message": "Fixar mensagem", + "aria/Progress": "{{percent}} por cento concluído", "aria/Quote Message": "Citar mensagem", "aria/Reaction list": "Lista de reações", "aria/Remind Me Message": "Lembrar-me", @@ -110,7 +111,6 @@ "aria/Unblock User": "Desbloquear usuário", "aria/Unmute User": "Ativar som", "aria/Unpin Message": "Desfixar mensagem", - "aria/Upload progress": "{{percent}} por cento carregado", "Ask a question": "Faça uma pergunta", "Attach": "Anexar", "Attach files": "Anexar arquivos", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 68bb9fcd03..8297abe3db 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -101,6 +101,7 @@ "aria/Open Reaction Selector": "Открыть селектор реакций", "aria/Open Thread": "Открыть тему", "aria/Pin Message": "Закрепить сообщение", + "aria/Progress": "{{percent}} процентов завершено", "aria/Quote Message": "Цитировать сообщение", "aria/Reaction list": "Список реакций", "aria/Remind Me Message": "Напомнить мне", @@ -119,7 +120,6 @@ "aria/Unblock User": "Разблокировать пользователя", "aria/Unmute User": "Включить уведомления", "aria/Unpin Message": "Открепить сообщение", - "aria/Upload progress": "{{percent}} процентов загружено", "Ask a question": "Задать вопрос", "Attach": "Прикрепить", "Attach files": "Прикрепить файлы", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 60d6ff1ece..ab307b8bb2 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -84,6 +84,7 @@ "aria/Open Reaction Selector": "Tepki Seçiciyi Aç", "aria/Open Thread": "Konuyu Aç", "aria/Pin Message": "Mesajı sabitle", + "aria/Progress": "Yüzde {{percent}} tamamlandı", "aria/Quote Message": "Mesajı alıntıla", "aria/Reaction list": "Tepki listesi", "aria/Remind Me Message": "Hatırlat", @@ -102,7 +103,6 @@ "aria/Unblock User": "Kullanıcının engelini kaldır", "aria/Unmute User": "Sesini aç", "aria/Unpin Message": "Sabitlemeyi kaldır", - "aria/Upload progress": "Yüzde {{percent}} karşıya yüklendi", "Ask a question": "Bir soru sor", "Attach": "Ekle", "Attach files": "Dosya ekle", From 0663ad000b6cb3841e0fdfd5f038eec5ea4e1a57 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 27 Mar 2026 09:00:23 +0100 Subject: [PATCH 03/26] refactor: remove unnecessary helpers --- .../AttachmentUploadProgressIndicator.tsx | 5 +---- .../AudioAttachmentPreview.tsx | 4 +--- .../FileAttachmentPreview.tsx | 5 ++--- .../MediaAttachmentPreview.tsx | 5 ++--- .../utils/uploadProgress.ts | 19 +------------------ 5 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx index 736f7d431c..a5f95727bd 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx @@ -4,7 +4,6 @@ import React, { type ReactNode } from 'react'; import { ProgressIndicator as DefaultProgressIndicator } from '../../Loading'; import { useComponentContext } from '../../../context'; import { LoadingIndicatorIcon } from '../icons'; -import { clampUploadPercent } from './utils/uploadProgress'; export type AttachmentUploadProgressVariant = 'inline' | 'overlay'; @@ -30,8 +29,6 @@ export const AttachmentUploadProgressIndicator = ({ return <>{fallback ?? }; } - const percent = Math.round(clampUploadPercent(uploadProgress)); - return (
- +
); }; diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index 41a4fc8060..556f9d8e3a 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -23,7 +23,6 @@ import { useAudioPlayer } from '../../AudioPlayback/WithAudioPlayback'; import { useStateStore } from '../../../store'; import { formatUploadByteFraction, - readUploadProgress, resolveAttachmentFullByteSize, } from './utils/uploadProgress'; @@ -47,9 +46,8 @@ export const AudioAttachmentPreview = ({ removeAttachments, }: AudioAttachmentPreviewProps) => { const { t } = useTranslationContext(); - const { id, previewUri, uploadPermissionCheck, uploadState } = + const { id, previewUri, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; - const uploadProgress = readUploadProgress(attachment.localMetadata); const fullBytes = resolveAttachmentFullByteSize(attachment); const showUploadFraction = uploadState === 'uploading' && diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index 0d2b02fa67..afe1eb1834 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -8,7 +8,6 @@ import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton' import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { formatUploadByteFraction, - readUploadProgress, resolveAttachmentFullByteSize, } from './utils/uploadProgress'; import { FileSizeIndicator } from '../../Attachment'; @@ -25,8 +24,8 @@ export const FileAttachmentPreview = ({ removeAttachments, }: FileAttachmentPreviewProps) => { const { t } = useTranslationContext('FilePreview'); - const { id, uploadPermissionCheck, uploadState } = attachment.localMetadata ?? {}; - const uploadProgress = readUploadProgress(attachment.localMetadata); + const { id, uploadPermissionCheck, uploadProgress, uploadState } = + attachment.localMetadata ?? {}; const fullBytes = resolveAttachmentFullByteSize(attachment); const showUploadFraction = uploadState === 'uploading' && diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index f613dd858d..ee70aa2661 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -20,7 +20,6 @@ import { Button } from '../../Button'; import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { LoadingIndicator as DefaultLoadingIndicator } from '../../Loading'; -import { readUploadProgress } from './utils/uploadProgress'; export type MediaAttachmentPreviewProps> = UploadAttachmentPreviewProps< @@ -40,8 +39,8 @@ export const MediaAttachmentPreview = ({ useComponentContext(); const [thumbnailPreviewError, setThumbnailPreviewError] = useState(false); - const { id, uploadPermissionCheck, uploadState } = attachment.localMetadata ?? {}; - const uploadProgress = readUploadProgress(attachment.localMetadata); + const { id, uploadPermissionCheck, uploadProgress, uploadState } = + attachment.localMetadata ?? {}; const isUploading = uploadState === 'uploading'; const handleThumbnailLoadError = useCallback(() => setThumbnailPreviewError(true), []); diff --git a/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts b/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts index 6f9e7acca2..e1ef330a30 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts +++ b/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts @@ -1,21 +1,5 @@ import { prettifyFileSize } from '../../hooks/utils'; -export function readUploadProgress( - localMetadata: { uploadProgress?: unknown } | null | undefined, -): number | undefined { - if (!localMetadata) return undefined; - const { uploadProgress } = localMetadata; - if (uploadProgress === undefined) return undefined; - if (typeof uploadProgress !== 'number' || !Number.isFinite(uploadProgress)) - return undefined; - return uploadProgress; -} - -export function clampUploadPercent(value: number): number { - if (!Number.isFinite(value)) return 0; - return Math.min(100, Math.max(0, value)); -} - function safePrettifyFileSize(bytes: number, maximumFractionDigits?: number): string { if (!Number.isFinite(bytes) || bytes < 0) return ''; if (bytes === 0) return '0 B'; @@ -27,8 +11,7 @@ export function formatUploadByteFraction( fullBytes: number, maximumFractionDigits?: number, ): string { - const clamped = clampUploadPercent(uploadPercent); - const uploaded = Math.round((clamped / 100) * fullBytes); + const uploaded = Math.round((uploadPercent / 100) * fullBytes); return `${safePrettifyFileSize(uploaded, maximumFractionDigits)} / ${safePrettifyFileSize(fullBytes, maximumFractionDigits)}`; } From 770505559261e90486cb6c948908c2c480dc1fd2 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 27 Mar 2026 09:47:38 +0100 Subject: [PATCH 04/26] refactor: create AttachmentUploadedSizeIndicator component --- .../AttachmentUploadedSizeIndicator.tsx | 74 ++++++++++ .../AudioAttachmentPreview.tsx | 23 +-- .../FileAttachmentPreview.tsx | 24 +--- .../utils/uploadProgress.ts | 33 ----- .../AttachmentUploadedSizeIndicator.test.tsx | 136 ++++++++++++++++++ 5 files changed, 214 insertions(+), 76 deletions(-) create mode 100644 src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx delete mode 100644 src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts create mode 100644 src/components/MessageComposer/__tests__/AttachmentUploadedSizeIndicator.test.tsx diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx new file mode 100644 index 0000000000..b0e2b8e980 --- /dev/null +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { FileSizeIndicator } from '../../Attachment'; +import { prettifyFileSize } from '../hooks/utils'; + +function safePrettifyFileSize(bytes: number, maximumFractionDigits?: number): string { + if (!Number.isFinite(bytes) || bytes < 0) return ''; + if (bytes === 0) return '0 B'; + return prettifyFileSize(bytes, maximumFractionDigits); +} + +function formatUploadByteFraction( + uploadPercent: number, + fullBytes: number, + maximumFractionDigits?: number, +): string { + const uploaded = Math.round((uploadPercent / 100) * fullBytes); + return `${safePrettifyFileSize(uploaded, maximumFractionDigits)} / ${safePrettifyFileSize(fullBytes, maximumFractionDigits)}`; +} + +function resolveAttachmentFullByteSize(attachment: { + file_size?: number | string; + localMetadata?: { file?: { size?: unknown } } | null; +}): number | undefined { + const fromFile = attachment.localMetadata?.file?.size; + if (typeof fromFile === 'number' && Number.isFinite(fromFile) && fromFile >= 0) { + return fromFile; + } + const raw = attachment.file_size; + if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw; + if (typeof raw === 'string') { + const n = parseFloat(raw); + if (Number.isFinite(n) && n >= 0) return n; + } + return undefined; +} + +export type AttachmentUploadedSizeIndicatorProps = { + attachment: { + file_size?: number | string; + localMetadata?: { + file?: { size?: unknown }; + uploadProgress?: number; + uploadState?: string; + } | null; + }; +}; + +export const AttachmentUploadedSizeIndicator = ({ + attachment, +}: AttachmentUploadedSizeIndicatorProps) => { + const { uploadProgress, uploadState } = attachment.localMetadata ?? {}; + const fullBytes = resolveAttachmentFullByteSize(attachment); + + if ( + uploadState === 'uploading' && + uploadProgress !== undefined && + fullBytes !== undefined + ) { + return ( + + {formatUploadByteFraction(uploadProgress, fullBytes)} + + ); + } + + if (uploadState === 'finished') { + return ; + } + + return null; +}; diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index 556f9d8e3a..e771bf3176 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -21,10 +21,7 @@ import { } from '../../AudioPlayback'; import { useAudioPlayer } from '../../AudioPlayback/WithAudioPlayback'; import { useStateStore } from '../../../store'; -import { - formatUploadByteFraction, - resolveAttachmentFullByteSize, -} from './utils/uploadProgress'; +import { AttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; export type AudioAttachmentPreviewProps> = UploadAttachmentPreviewProps< @@ -48,11 +45,6 @@ export const AudioAttachmentPreview = ({ const { t } = useTranslationContext(); const { id, previewUri, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; - const fullBytes = resolveAttachmentFullByteSize(attachment); - const showUploadFraction = - uploadState === 'uploading' && - uploadProgress !== undefined && - fullBytes !== undefined; const url = attachment.asset_url || previewUri; const audioPlayer = useAudioPlayer({ @@ -111,18 +103,7 @@ export const AudioAttachmentPreview = ({ {showProgressControls ? ( <> {!resolvedDuration && !progressPercent && !isPlaying && ( - <> - {showUploadFraction ? ( - - {formatUploadByteFraction(uploadProgress, fullBytes)} - - ) : ( - - )} - + )} {hasWaveform ? ( <> diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index afe1eb1834..ee69d34545 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -2,15 +2,11 @@ import React from 'react'; import { useTranslationContext } from '../../../context'; import { FileIcon } from '../../FileIcon'; import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; +import { AttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; import type { LocalAudioAttachment, LocalFileAttachment } from 'stream-chat'; import type { UploadAttachmentPreviewProps } from './types'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; -import { - formatUploadByteFraction, - resolveAttachmentFullByteSize, -} from './utils/uploadProgress'; -import { FileSizeIndicator } from '../../Attachment'; import { IconExclamationMark, IconExclamationTriangleFill } from '../../Icons'; export type FileAttachmentPreviewProps = @@ -26,16 +22,10 @@ export const FileAttachmentPreview = ({ const { t } = useTranslationContext('FilePreview'); const { id, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; - const fullBytes = resolveAttachmentFullByteSize(attachment); - const showUploadFraction = - uploadState === 'uploading' && - uploadProgress !== undefined && - fullBytes !== undefined; const hasSizeLimitError = uploadPermissionCheck?.reason === 'size_limit'; const hasFatalError = uploadState === 'blocked' || hasSizeLimitError; const hasRetriableError = uploadState === 'failed' && !!handleRetry; - const hasError = hasRetriableError || hasFatalError; return ( )} - {!hasError && showUploadFraction && ( - - {formatUploadByteFraction(uploadProgress, fullBytes)} - - )} - {!hasError && !showUploadFraction && ( - - )} + {hasFatalError && (
diff --git a/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts b/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts deleted file mode 100644 index e1ef330a30..0000000000 --- a/src/components/MessageComposer/AttachmentPreviewList/utils/uploadProgress.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { prettifyFileSize } from '../../hooks/utils'; - -function safePrettifyFileSize(bytes: number, maximumFractionDigits?: number): string { - if (!Number.isFinite(bytes) || bytes < 0) return ''; - if (bytes === 0) return '0 B'; - return prettifyFileSize(bytes, maximumFractionDigits); -} - -export function formatUploadByteFraction( - uploadPercent: number, - fullBytes: number, - maximumFractionDigits?: number, -): string { - const uploaded = Math.round((uploadPercent / 100) * fullBytes); - return `${safePrettifyFileSize(uploaded, maximumFractionDigits)} / ${safePrettifyFileSize(fullBytes, maximumFractionDigits)}`; -} - -export function resolveAttachmentFullByteSize(attachment: { - file_size?: number | string; - localMetadata?: { file?: { size?: unknown } } | null; -}): number | undefined { - const fromFile = attachment.localMetadata?.file?.size; - if (typeof fromFile === 'number' && Number.isFinite(fromFile) && fromFile >= 0) { - return fromFile; - } - const raw = attachment.file_size; - if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw; - if (typeof raw === 'string') { - const n = parseFloat(raw); - if (Number.isFinite(n) && n >= 0) return n; - } - return undefined; -} diff --git a/src/components/MessageComposer/__tests__/AttachmentUploadedSizeIndicator.test.tsx b/src/components/MessageComposer/__tests__/AttachmentUploadedSizeIndicator.test.tsx new file mode 100644 index 0000000000..59ab507313 --- /dev/null +++ b/src/components/MessageComposer/__tests__/AttachmentUploadedSizeIndicator.test.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { AttachmentUploadedSizeIndicator } from '../AttachmentPreviewList/AttachmentUploadedSizeIndicator'; + +describe('AttachmentUploadedSizeIndicator', () => { + it('renders nothing when upload state is not uploading or finished', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toBeNull(); + }); + + it('renders nothing when uploading without uploadProgress', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toBeNull(); + }); + + it('renders nothing when uploading without a resolvable full byte size', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toBeNull(); + }); + + it('renders upload size fraction when uploading with numeric file_size and progress', () => { + render( + , + ); + + expect(screen.getByTestId('upload-size-fraction')).toHaveTextContent( + '500 B / 1.00e+3 B', + ); + expect(screen.getByTestId('upload-size-fraction')).toHaveClass( + 'str-chat__attachment-preview-file__upload-size-fraction', + ); + }); + + it('parses string file_size for the upload fraction', () => { + render( + , + ); + + expect(screen.getByTestId('upload-size-fraction')).toHaveTextContent( + '500 B / 1.00e+3 B', + ); + }); + + it('prefers localMetadata.file.size over file_size when both are present', () => { + render( + , + ); + + expect(screen.getByTestId('upload-size-fraction')).toHaveTextContent('100 B / 200 B'); + }); + + it('renders FileSizeIndicator when upload is finished', () => { + render( + , + ); + + expect(screen.getByTestId('file-size-indicator')).toHaveTextContent('1.00 kB'); + }); + + it('renders nothing when finished but file_size is missing or invalid', () => { + const { container: missing } = render( + , + ); + expect(missing.firstChild).toBeNull(); + + const { container: nanString } = render( + , + ); + expect(nanString.firstChild).toBeNull(); + }); +}); From 2855009e41b156b220d60a93c49535627b247870 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 27 Mar 2026 10:50:13 +0100 Subject: [PATCH 05/26] refactor: more meaningful name for "x percent complete" i18n key --- src/components/Loading/ProgressIndicator.tsx | 2 +- src/i18n/de.json | 2 +- src/i18n/en.json | 2 +- src/i18n/es.json | 2 +- src/i18n/fr.json | 2 +- src/i18n/hi.json | 2 +- src/i18n/it.json | 2 +- src/i18n/ja.json | 2 +- src/i18n/ko.json | 2 +- src/i18n/nl.json | 2 +- src/i18n/pt.json | 2 +- src/i18n/ru.json | 2 +- src/i18n/tr.json | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/Loading/ProgressIndicator.tsx b/src/components/Loading/ProgressIndicator.tsx index 08ee89f81f..cce1cd18ca 100644 --- a/src/components/Loading/ProgressIndicator.tsx +++ b/src/components/Loading/ProgressIndicator.tsx @@ -18,7 +18,7 @@ export const ProgressIndicator = ({ percent }: ProgressIndicatorProps) => { return (
Date: Wed, 1 Apr 2026 16:37:44 +0200 Subject: [PATCH 06/26] chore: translation rebuilt --- src/i18n/de.json | 2 +- src/i18n/en.json | 2 +- src/i18n/es.json | 2 +- src/i18n/fr.json | 2 +- src/i18n/hi.json | 2 +- src/i18n/it.json | 2 +- src/i18n/ja.json | 2 +- src/i18n/ko.json | 2 +- src/i18n/nl.json | 2 +- src/i18n/pt.json | 2 +- src/i18n/ru.json | 2 +- src/i18n/tr.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/i18n/de.json b/src/i18n/de.json index 162bb25f55..c9ac9f639f 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -83,8 +83,8 @@ "aria/Open Message Actions Menu": "Nachrichtenaktionsmenü öffnen", "aria/Open Reaction Selector": "Reaktionsauswahl öffnen", "aria/Open Thread": "Thread öffnen", - "aria/Pin Message": "Nachricht anheften", "aria/Percent complete": "{{percent}} Prozent abgeschlossen", + "aria/Pin Message": "Nachricht anheften", "aria/Quote Message": "Nachricht zitieren", "aria/Reaction list": "Reaktionsliste", "aria/Remind Me Message": "Erinnern", diff --git a/src/i18n/en.json b/src/i18n/en.json index 59c7ea5023..e0a1f27e8f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -83,8 +83,8 @@ "aria/Open Message Actions Menu": "Open Message Actions Menu", "aria/Open Reaction Selector": "Open Reaction Selector", "aria/Open Thread": "Open Thread", - "aria/Pin Message": "Pin Message", "aria/Percent complete": "{{percent}} percent complete", + "aria/Pin Message": "Pin Message", "aria/Quote Message": "Quote Message", "aria/Reaction list": "Reaction list", "aria/Remind Me Message": "Remind Me Message", diff --git a/src/i18n/es.json b/src/i18n/es.json index 36297c99ca..e5e5d4e380 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -91,8 +91,8 @@ "aria/Open Message Actions Menu": "Abrir menú de acciones de mensaje", "aria/Open Reaction Selector": "Abrir selector de reacciones", "aria/Open Thread": "Abrir hilo", - "aria/Pin Message": "Fijar mensaje", "aria/Percent complete": "{{percent}} por ciento completado", + "aria/Pin Message": "Fijar mensaje", "aria/Quote Message": "Citar mensaje", "aria/Reaction list": "Lista de reacciones", "aria/Remind Me Message": "Recordarme", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index b9f41fd082..3c54f13c12 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -91,8 +91,8 @@ "aria/Open Message Actions Menu": "Ouvrir le menu des actions du message", "aria/Open Reaction Selector": "Ouvrir le sélecteur de réactions", "aria/Open Thread": "Ouvrir le fil", - "aria/Pin Message": "Épingler le message", "aria/Percent complete": "{{percent}} pour cent terminé", + "aria/Pin Message": "Épingler le message", "aria/Quote Message": "Citer le message", "aria/Reaction list": "Liste des réactions", "aria/Remind Me Message": "Me rappeler", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index 9c924d5158..aec01fb3d8 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -83,8 +83,8 @@ "aria/Open Message Actions Menu": "संदेश क्रिया मेन्यू खोलें", "aria/Open Reaction Selector": "प्रतिक्रिया चयनकर्ता खोलें", "aria/Open Thread": "थ्रेड खोलें", - "aria/Pin Message": "संदेश पिन करें", "aria/Percent complete": "{{percent}} प्रतिशत पूर्ण", + "aria/Pin Message": "संदेश पिन करें", "aria/Quote Message": "संदेश उद्धरण", "aria/Reaction list": "प्रतिक्रिया सूची", "aria/Remind Me Message": "मुझे याद दिलाएं", diff --git a/src/i18n/it.json b/src/i18n/it.json index ed2e6e669a..a9d8407317 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -91,8 +91,8 @@ "aria/Open Message Actions Menu": "Apri il menu delle azioni di messaggio", "aria/Open Reaction Selector": "Apri il selettore di reazione", "aria/Open Thread": "Apri discussione", - "aria/Pin Message": "Appunta messaggio", "aria/Percent complete": "{{percent}} percento completato", + "aria/Pin Message": "Appunta messaggio", "aria/Quote Message": "Citazione messaggio", "aria/Reaction list": "Elenco delle reazioni", "aria/Remind Me Message": "Ricordami", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index c7a1af3c01..525204ee79 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -82,8 +82,8 @@ "aria/Open Message Actions Menu": "メッセージアクションメニューを開く", "aria/Open Reaction Selector": "リアクションセレクターを開く", "aria/Open Thread": "スレッドを開く", - "aria/Pin Message": "メッセージをピン", "aria/Percent complete": "{{percent}}パーセント完了", + "aria/Pin Message": "メッセージをピン", "aria/Quote Message": "メッセージを引用", "aria/Reaction list": "リアクション一覧", "aria/Remind Me Message": "リマインダー", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 1b9d2cdeb6..e9d3e6357f 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -82,8 +82,8 @@ "aria/Open Message Actions Menu": "메시지 액션 메뉴 열기", "aria/Open Reaction Selector": "반응 선택기 열기", "aria/Open Thread": "스레드 열기", - "aria/Pin Message": "메시지 고정", "aria/Percent complete": "{{percent}}퍼센트 완료", + "aria/Pin Message": "메시지 고정", "aria/Quote Message": "메시지 인용", "aria/Reaction list": "반응 목록", "aria/Remind Me Message": "알림 설정", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 6994e7a27b..77bd5dc26d 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -83,8 +83,8 @@ "aria/Open Message Actions Menu": "Menu voor berichtacties openen", "aria/Open Reaction Selector": "Reactiekiezer openen", "aria/Open Thread": "Draad openen", - "aria/Pin Message": "Bericht vastmaken", "aria/Percent complete": "{{percent}} procent voltooid", + "aria/Pin Message": "Bericht vastmaken", "aria/Quote Message": "Bericht citeren", "aria/Reaction list": "Reactielijst", "aria/Remind Me Message": "Herinner mij", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 2898e63d1a..663bca08ad 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -91,8 +91,8 @@ "aria/Open Message Actions Menu": "Abrir menu de ações de mensagem", "aria/Open Reaction Selector": "Abrir seletor de reações", "aria/Open Thread": "Abrir tópico", - "aria/Pin Message": "Fixar mensagem", "aria/Percent complete": "{{percent}} por cento concluído", + "aria/Pin Message": "Fixar mensagem", "aria/Quote Message": "Citar mensagem", "aria/Reaction list": "Lista de reações", "aria/Remind Me Message": "Lembrar-me", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index a3fe3ae6ef..aa0b92f294 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -100,8 +100,8 @@ "aria/Open Message Actions Menu": "Открыть меню действий с сообщениями", "aria/Open Reaction Selector": "Открыть селектор реакций", "aria/Open Thread": "Открыть тему", - "aria/Pin Message": "Закрепить сообщение", "aria/Percent complete": "{{percent}} процентов завершено", + "aria/Pin Message": "Закрепить сообщение", "aria/Quote Message": "Цитировать сообщение", "aria/Reaction list": "Список реакций", "aria/Remind Me Message": "Напомнить мне", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 9e7b969824..c792c01d3e 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -83,8 +83,8 @@ "aria/Open Message Actions Menu": "Mesaj İşlemleri Menüsünü Aç", "aria/Open Reaction Selector": "Tepki Seçiciyi Aç", "aria/Open Thread": "Konuyu Aç", - "aria/Pin Message": "Mesajı sabitle", "aria/Percent complete": "Yüzde {{percent}} tamamlandı", + "aria/Pin Message": "Mesajı sabitle", "aria/Quote Message": "Mesajı alıntıla", "aria/Reaction list": "Tepki listesi", "aria/Remind Me Message": "Hatırlat", From 5685c59ab19b5348f08d5ca6244acc36f3fb7ffe Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 7 Apr 2026 11:28:54 +0200 Subject: [PATCH 07/26] Apply suggestion from @MartinCupela Co-authored-by: MartinCupela <32706194+MartinCupela@users.noreply.github.com> --- .../AttachmentUploadProgressIndicator.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx index a5f95727bd..4c158d1816 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx @@ -21,9 +21,7 @@ export const AttachmentUploadProgressIndicator = ({ uploadProgress, variant, }: AttachmentUploadProgressIndicatorProps) => { - const { ProgressIndicator = DefaultProgressIndicator } = useComponentContext( - 'AttachmentUploadProgressIndicator', - ); + const { ProgressIndicator = DefaultProgressIndicator } = useComponentContext(); if (uploadProgress === undefined) { return <>{fallback ?? }; From aecaa9ca96d5a0c75548f2dbca3099077491c9c0 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 7 Apr 2026 12:03:32 +0200 Subject: [PATCH 08/26] fix: remove unnecessary variant prop --- .../AttachmentUploadProgressIndicator.tsx | 12 +----------- .../AttachmentPreviewList/AudioAttachmentPreview.tsx | 6 +----- .../AttachmentPreviewList/FileAttachmentPreview.tsx | 5 +---- .../AttachmentPreviewList/MediaAttachmentPreview.tsx | 1 - .../MessageComposer/styling/AttachmentPreview.scss | 11 ++++------- 5 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx index 4c158d1816..b9e3d21e8a 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx @@ -5,21 +5,17 @@ import { ProgressIndicator as DefaultProgressIndicator } from '../../Loading'; import { useComponentContext } from '../../../context'; import { LoadingIndicatorIcon } from '../icons'; -export type AttachmentUploadProgressVariant = 'inline' | 'overlay'; - export type AttachmentUploadProgressIndicatorProps = { className?: string; /** Shown when `uploadProgress` is `undefined` (e.g. progress tracking disabled). */ fallback?: ReactNode; uploadProgress?: number; - variant: AttachmentUploadProgressVariant; }; export const AttachmentUploadProgressIndicator = ({ className, fallback, uploadProgress, - variant, }: AttachmentUploadProgressIndicatorProps) => { const { ProgressIndicator = DefaultProgressIndicator } = useComponentContext(); @@ -28,13 +24,7 @@ export const AttachmentUploadProgressIndicator = ({ } return ( -
+
); diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index e771bf3176..101d6fe45e 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -10,7 +10,6 @@ import clsx from 'clsx'; import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; -import { FileSizeIndicator } from '../../Attachment'; import { IconExclamationMark, IconExclamationTriangleFill } from '../../Icons'; import { PlayButton } from '../../Button'; import { @@ -95,10 +94,7 @@ export const AudioAttachmentPreview = ({
{uploadState === 'uploading' && ( - + )} {showProgressControls ? ( <> diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index ee69d34545..08a6da2596 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -43,10 +43,7 @@ export const FileAttachmentPreview = ({
{uploadState === 'uploading' && ( - + )} {hasFatalError && ( diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index ee70aa2661..223a9f634f 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -100,7 +100,6 @@ export const MediaAttachmentPreview = ({ } uploadProgress={uploadProgress} - variant='overlay' /> )} diff --git a/src/components/MessageComposer/styling/AttachmentPreview.scss b/src/components/MessageComposer/styling/AttachmentPreview.scss index d1a119c3d8..b17a32c0c2 100644 --- a/src/components/MessageComposer/styling/AttachmentPreview.scss +++ b/src/components/MessageComposer/styling/AttachmentPreview.scss @@ -215,7 +215,7 @@ } .str-chat__loading-indicator, - .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--overlay, + .str-chat__attachment-upload-progress, .str-chat__icon--exclamation-circle { width: var(--icon-size-sm); height: var(--icon-size-sm); @@ -241,9 +241,10 @@ color: var(--badge-text-on-accent); } - .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--overlay { + .str-chat__attachment-upload-progress { border: none; border-radius: 0; + background: transparent; } } @@ -295,15 +296,11 @@ line-height: var(--typography-line-height-tight); .str-chat__loading-indicator, - .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--inline { + .str-chat__attachment-upload-progress { width: var(--icon-size-sm); height: var(--icon-size-sm); } - .str-chat__attachment-upload-progress.str-chat__attachment-upload-progress--inline { - flex-shrink: 0; - } - .str-chat__attachment-preview-file__upload-size-fraction { white-space: nowrap; } From b2c347a2ab26719032cf3a257bf802a80586dc19 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 7 Apr 2026 14:36:44 +0200 Subject: [PATCH 09/26] chore: small fixes --- .../AttachmentUploadProgressIndicator.tsx | 2 +- src/components/MessageComposer/styling/AttachmentPreview.scss | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx index b9e3d21e8a..2b4549ca34 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx @@ -20,7 +20,7 @@ export const AttachmentUploadProgressIndicator = ({ const { ProgressIndicator = DefaultProgressIndicator } = useComponentContext(); if (uploadProgress === undefined) { - return <>{fallback ?? }; + return <>{fallback ?? }; } return ( diff --git a/src/components/MessageComposer/styling/AttachmentPreview.scss b/src/components/MessageComposer/styling/AttachmentPreview.scss index b17a32c0c2..662d014025 100644 --- a/src/components/MessageComposer/styling/AttachmentPreview.scss +++ b/src/components/MessageComposer/styling/AttachmentPreview.scss @@ -215,8 +215,7 @@ } .str-chat__loading-indicator, - .str-chat__attachment-upload-progress, - .str-chat__icon--exclamation-circle { + .str-chat__attachment-upload-progress { width: var(--icon-size-sm); height: var(--icon-size-sm); position: absolute; From d4f2f6669e960ac5dc3801aaa78f3b1b4bb987b1 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 7 Apr 2026 14:44:18 +0200 Subject: [PATCH 10/26] chore: fix failing tests --- .../AttachmentPreviewList/MediaAttachmentPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index 223a9f634f..24e72dbc90 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -98,7 +98,7 @@ export const MediaAttachmentPreview = ({
{isUploading && ( } + fallback={} uploadProgress={uploadProgress} /> )} From b45192a766c9a7636fdeb05c2efee1a70887c1a7 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 7 Apr 2026 17:16:51 +0200 Subject: [PATCH 11/26] fix: css fixes --- .../MessageComposer/styling/AttachmentPreview.scss | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/MessageComposer/styling/AttachmentPreview.scss b/src/components/MessageComposer/styling/AttachmentPreview.scss index 662d014025..efa461e75a 100644 --- a/src/components/MessageComposer/styling/AttachmentPreview.scss +++ b/src/components/MessageComposer/styling/AttachmentPreview.scss @@ -76,10 +76,6 @@ --str-chat__attachment-preview-file-fatal-error-color: var(--color-accent-error); - .str-chat__attachment-upload-progress { - color: var(--str-chat__primary-color); - } - .str-chat__message-composer-voice-preview-slot { display: flex; align-items: center; @@ -222,7 +218,6 @@ inset-inline-start: var(--spacing-xxs); bottom: var(--spacing-xxs); border-radius: var(--radius-max); - border-radius: var(--radius-max); background: var(--background-core-elevation-0); color: var(--accent-primary); } @@ -239,12 +234,6 @@ background: var(--badge-bg-error); color: var(--badge-text-on-accent); } - - .str-chat__attachment-upload-progress { - border: none; - border-radius: 0; - background: transparent; - } } .str-chat__attachment-preview-media--upload-error { @@ -298,6 +287,7 @@ .str-chat__attachment-upload-progress { width: var(--icon-size-sm); height: var(--icon-size-sm); + color: var(--accent-primary); } .str-chat__attachment-preview-file__upload-size-fraction { From 77ba34ce17fca66fb67dd18e32269668d21069ff Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 7 Apr 2026 17:23:01 +0200 Subject: [PATCH 12/26] refactor: rename ProgressIndicator to CircularProgressIndicator --- ...ssIndicator.tsx => CircularProgressIndicator.tsx} | 12 +++++++----- src/components/Loading/index.ts | 2 +- ...Indicator.scss => CircularProgressIndicator.scss} | 2 +- src/components/Loading/styling/index.scss | 2 +- .../AttachmentUploadProgressIndicator.tsx | 7 ++++--- .../__tests__/AttachmentPreviewList.test.tsx | 4 ++-- src/context/ComponentContext.tsx | 6 +++--- 7 files changed, 19 insertions(+), 16 deletions(-) rename src/components/Loading/{ProgressIndicator.tsx => CircularProgressIndicator.tsx} (79%) rename src/components/Loading/styling/{ProgressIndicator.scss => CircularProgressIndicator.scss} (61%) diff --git a/src/components/Loading/ProgressIndicator.tsx b/src/components/Loading/CircularProgressIndicator.tsx similarity index 79% rename from src/components/Loading/ProgressIndicator.tsx rename to src/components/Loading/CircularProgressIndicator.tsx index cce1cd18ca..511d7d2fb7 100644 --- a/src/components/Loading/ProgressIndicator.tsx +++ b/src/components/Loading/CircularProgressIndicator.tsx @@ -5,24 +5,26 @@ import { useTranslationContext } from '../../context/TranslationContext'; const RING_RADIUS = 12; const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS; -export type ProgressIndicatorProps = { +export type CircularProgressIndicatorProps = { /** Clamped 0–100 completion. */ percent: number; }; /** Circular progress indicator with input from 0 to 100. */ -export const ProgressIndicator = ({ percent }: ProgressIndicatorProps) => { - const { t } = useTranslationContext('ProgressIndicator'); +export const CircularProgressIndicator = ({ + percent, +}: CircularProgressIndicatorProps) => { + const { t } = useTranslationContext('CircularProgressIndicator'); const dashOffset = RING_CIRCUMFERENCE * (1 - percent / 100); return ( -
+
{ - const { ProgressIndicator = DefaultProgressIndicator } = useComponentContext(); + const { CircularProgressIndicator = DefaultCircularProgressIndicator } = + useComponentContext(); if (uploadProgress === undefined) { return <>{fallback ?? }; @@ -25,7 +26,7 @@ export const AttachmentUploadProgressIndicator = ({ return (
- +
); }; diff --git a/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx b/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx index 37a9a8d1da..0acd77d470 100644 --- a/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx +++ b/src/components/MessageComposer/__tests__/AttachmentPreviewList.test.tsx @@ -353,7 +353,7 @@ describe('AttachmentPreviewList', () => { }); expect(screen.getByTestId(LOADING_INDICATOR_TEST_ID)).toBeInTheDocument(); - expect(screen.queryByTestId('progress-ring')).not.toBeInTheDocument(); + expect(screen.queryByTestId('circular-progress-ring')).not.toBeInTheDocument(); }); it('shows ring while uploading when uploadProgress is numeric', async () => { @@ -370,7 +370,7 @@ describe('AttachmentPreviewList', () => { ], }); - expect(screen.getByTestId('progress-ring')).toBeInTheDocument(); + expect(screen.getByTestId('circular-progress-ring')).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_INDICATOR_TEST_ID)).not.toBeInTheDocument(); }); diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index d5c7926c74..d1696fcc25 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -74,7 +74,7 @@ import type { VideoPlayerProps } from '../components/VideoPlayer'; import type { EditedMessagePreviewProps } from '../components/MessageComposer/EditedMessagePreview'; import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; -import type { ProgressIndicatorProps } from '../components/Loading/ProgressIndicator'; +import type { CircularProgressIndicatorProps } from '../components/Loading/CircularProgressIndicator'; export type ComponentContextValue = { /** Custom UI component to display additional message composer action buttons left to the textarea, defaults to and accepts same props as: [AdditionalMessageComposerActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/MessageComposerActions.tsx) */ @@ -149,8 +149,8 @@ export type ComponentContextValue = { LoadingErrorIndicator?: React.ComponentType; /** Custom UI component to render while the `MessageList` is loading new messages, defaults to and accepts same props as: [LoadingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/LoadingIndicator.tsx) */ LoadingIndicator?: React.ComponentType; - /** Custom UI component for determinate progress (0–100), defaults to and accepts same props as: [ProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/ProgressIndicator.tsx) */ - ProgressIndicator?: React.ComponentType; + /** Custom UI component for determinate progress (0–100), defaults to and accepts same props as: [CircularProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/CircularProgressIndicator.tsx) */ + CircularProgressIndicator?: React.ComponentType; /** Custom UI component to display a message in the standard `MessageList`, defaults to and accepts the same props as: [MessageUI](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageUI.tsx) */ Message?: React.ComponentType; /** Custom UI component for message actions popup, accepts no props, all the defaults are set within [MessageActions (unstable)](https://github.com/GetStream/stream-chat-react/blob/master/src/experimental/MessageActions/MessageActions.tsx) */ From 0a8d65dacf87ac6c90e464a7b26f8360c451ce2e Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 7 Apr 2026 17:44:19 +0200 Subject: [PATCH 13/26] feat: make AttachmentUploadProgressIndicator and AttachmentUploadProgressIndicator overridable --- .../AttachmentPreviewList/AudioAttachmentPreview.tsx | 10 +++++++--- .../AttachmentPreviewList/FileAttachmentPreview.tsx | 10 +++++++--- .../AttachmentPreviewList/MediaAttachmentPreview.tsx | 9 ++++++--- src/context/ComponentContext.tsx | 6 ++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index 101d6fe45e..1a18c07d75 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -4,10 +4,10 @@ import { type LocalAudioAttachment, type LocalVoiceRecordingAttachment, } from 'stream-chat'; -import { useTranslationContext } from '../../../context'; +import { useComponentContext, useTranslationContext } from '../../../context'; import React, { useEffect } from 'react'; import clsx from 'clsx'; -import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; +import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { IconExclamationMark, IconExclamationTriangleFill } from '../../Icons'; @@ -20,7 +20,7 @@ import { } from '../../AudioPlayback'; import { useAudioPlayer } from '../../AudioPlayback/WithAudioPlayback'; import { useStateStore } from '../../../store'; -import { AttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; +import { AttachmentUploadedSizeIndicator as DefaultAttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; export type AudioAttachmentPreviewProps> = UploadAttachmentPreviewProps< @@ -42,6 +42,10 @@ export const AudioAttachmentPreview = ({ removeAttachments, }: AudioAttachmentPreviewProps) => { const { t } = useTranslationContext(); + const { + AttachmentUploadedSizeIndicator = DefaultAttachmentUploadedSizeIndicator, + AttachmentUploadProgressIndicator = DefaultAttachmentUploadProgressIndicator, + } = useComponentContext(); const { id, previewUri, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; const url = attachment.asset_url || previewUri; diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index 08a6da2596..22444d2dd2 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useTranslationContext } from '../../../context'; +import { useComponentContext, useTranslationContext } from '../../../context'; import { FileIcon } from '../../FileIcon'; -import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; -import { AttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; +import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; +import { AttachmentUploadedSizeIndicator as DefaultAttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; import type { LocalAudioAttachment, LocalFileAttachment } from 'stream-chat'; import type { UploadAttachmentPreviewProps } from './types'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; @@ -20,6 +20,10 @@ export const FileAttachmentPreview = ({ removeAttachments, }: FileAttachmentPreviewProps) => { const { t } = useTranslationContext('FilePreview'); + const { + AttachmentUploadedSizeIndicator = DefaultAttachmentUploadedSizeIndicator, + AttachmentUploadProgressIndicator = DefaultAttachmentUploadProgressIndicator, + } = useComponentContext(); const { id, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index 24e72dbc90..66bc87e310 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -17,7 +17,7 @@ import clsx from 'clsx'; import { IconExclamationMark, IconRetry, IconVideoFill } from '../../Icons'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { Button } from '../../Button'; -import { AttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; +import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { LoadingIndicator as DefaultLoadingIndicator } from '../../Loading'; @@ -35,8 +35,11 @@ export const MediaAttachmentPreview = ({ removeAttachments, }: MediaAttachmentPreviewProps) => { const { t } = useTranslationContext(); - const { BaseImage = DefaultBaseImage, LoadingIndicator = DefaultLoadingIndicator } = - useComponentContext(); + const { + AttachmentUploadProgressIndicator = DefaultAttachmentUploadProgressIndicator, + BaseImage = DefaultBaseImage, + LoadingIndicator = DefaultLoadingIndicator, + } = useComponentContext(); const [thumbnailPreviewError, setThumbnailPreviewError] = useState(false); const { id, uploadPermissionCheck, uploadProgress, uploadState } = diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index d1696fcc25..8e36e50c14 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -75,6 +75,8 @@ import type { EditedMessagePreviewProps } from '../components/MessageComposer/Ed import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; import type { CircularProgressIndicatorProps } from '../components/Loading/CircularProgressIndicator'; +import type { AttachmentUploadProgressIndicatorProps } from '../components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator'; +import type { AttachmentUploadedSizeIndicatorProps } from '../components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator'; export type ComponentContextValue = { /** Custom UI component to display additional message composer action buttons left to the textarea, defaults to and accepts same props as: [AdditionalMessageComposerActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/MessageComposerActions.tsx) */ @@ -87,6 +89,10 @@ export type ComponentContextValue = { AttachmentPreviewList?: React.ComponentType; /** Custom UI component to control adding attachments to MessageComposer, defaults to and accepts same props as: [AttachmentSelector](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentSelector.tsx) */ AttachmentSelector?: React.ComponentType; + /** Custom UI component for upload progress on attachment previews in MessageComposer, defaults to and accepts same props as: [AttachmentUploadProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx) */ + AttachmentUploadProgressIndicator?: React.ComponentType; + /** Custom UI component for uploaded/total size on attachment previews in MessageComposer, defaults to and accepts same props as: [AttachmentUploadedSizeIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx) */ + AttachmentUploadedSizeIndicator?: React.ComponentType; /** Custom UI component for the dedicated voice recording preview slot above composer attachments (REACT-794), defaults to and accepts same props as: [VoiceRecordingPreviewSlot](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/VoiceRecordingPreviewSlot.tsx) */ VoiceRecordingPreviewSlot?: React.ComponentType; /** Custom UI component for contents of attachment selector initiation button */ From ff1e8792888554890c601ebd67b05251ee1f499b Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 09:32:09 +0200 Subject: [PATCH 14/26] fix: use LoadingIndicator instead of LoadingIndicatorIcon --- .../AudioRecorderRecordingControls.tsx | 10 ++++-- .../AttachmentUploadProgressIndicator.tsx | 19 +++++----- .../MediaAttachmentPreview.tsx | 7 +--- src/components/MessageComposer/icons.tsx | 35 ------------------- 4 files changed, 19 insertions(+), 52 deletions(-) diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx index 332ea66048..229b21a179 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx @@ -1,4 +1,4 @@ -import { CheckSignIcon, LoadingIndicatorIcon } from '../../MessageComposer/icons'; +import { CheckSignIcon } from '../../MessageComposer/icons'; import { IconDelete, IconPauseFill, IconVoice } from '../../Icons'; import React from 'react'; import { @@ -9,6 +9,7 @@ import { import { isRecording } from './recordingStateIdentity'; import { Button } from '../../Button'; import { addNotificationTargetTag, useNotificationTarget } from '../../Notifications'; +import { AttachmentUploadProgressIndicator } from '../../MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator'; const ToggleRecordingButton = () => { const { @@ -40,6 +41,7 @@ export const AudioRecorderRecordingControls = () => { const panel = useNotificationTarget(); const isUploadingFile = recording?.localMetadata?.uploadState === 'uploading'; + const uploadProgress = recording?.localMetadata?.uploadProgress; if (!recorder) return null; @@ -79,7 +81,11 @@ export const AudioRecorderRecordingControls = () => { size='sm' variant='primary' > - {isUploadingFile ? : } + {isUploadingFile ? ( + + ) : ( + + )}
); diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx index 5e436174a4..f7a1814f8b 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx @@ -1,27 +1,28 @@ import clsx from 'clsx'; -import React, { type ReactNode } from 'react'; +import React from 'react'; -import { CircularProgressIndicator as DefaultCircularProgressIndicator } from '../../Loading'; +import { + CircularProgressIndicator as DefaultCircularProgressIndicator, + LoadingIndicator as DefaultLoadingIndicator, +} from '../../Loading'; import { useComponentContext } from '../../../context'; -import { LoadingIndicatorIcon } from '../icons'; export type AttachmentUploadProgressIndicatorProps = { className?: string; - /** Shown when `uploadProgress` is `undefined` (e.g. progress tracking disabled). */ - fallback?: ReactNode; uploadProgress?: number; }; export const AttachmentUploadProgressIndicator = ({ className, - fallback, uploadProgress, }: AttachmentUploadProgressIndicatorProps) => { - const { CircularProgressIndicator = DefaultCircularProgressIndicator } = - useComponentContext(); + const { + CircularProgressIndicator = DefaultCircularProgressIndicator, + LoadingIndicator = DefaultLoadingIndicator, + } = useComponentContext(); if (uploadProgress === undefined) { - return <>{fallback ?? }; + return ; } return ( diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index 66bc87e310..b5edfdd91e 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -19,7 +19,6 @@ import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton' import { Button } from '../../Button'; import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; -import { LoadingIndicator as DefaultLoadingIndicator } from '../../Loading'; export type MediaAttachmentPreviewProps> = UploadAttachmentPreviewProps< @@ -38,7 +37,6 @@ export const MediaAttachmentPreview = ({ const { AttachmentUploadProgressIndicator = DefaultAttachmentUploadProgressIndicator, BaseImage = DefaultBaseImage, - LoadingIndicator = DefaultLoadingIndicator, } = useComponentContext(); const [thumbnailPreviewError, setThumbnailPreviewError] = useState(false); @@ -100,10 +98,7 @@ export const MediaAttachmentPreview = ({
{isUploading && ( - } - uploadProgress={uploadProgress} - /> + )} {isVideoAttachment(attachment) && diff --git a/src/components/MessageComposer/icons.tsx b/src/components/MessageComposer/icons.tsx index 94b8de7037..cdbdcdcd01 100644 --- a/src/components/MessageComposer/icons.tsx +++ b/src/components/MessageComposer/icons.tsx @@ -1,40 +1,5 @@ -import React, { useMemo } from 'react'; -import { nanoid } from 'nanoid'; - import { useTranslationContext } from '../../context/TranslationContext'; -export const LoadingIndicatorIcon = () => { - const id = useMemo(() => nanoid(), []); - - return ( -
- - - - - - - - - -
- ); -}; - export const UploadIcon = () => { const { t } = useTranslationContext('UploadIcon'); return ( From e686dcacd13213080a5c06443ffd02b78d8c879a Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 09:40:42 +0200 Subject: [PATCH 15/26] refactor: rename AttachmentUploadProgressIndicator to UploadProgressIndicator --- .../UploadProgressIndicator.tsx} | 14 ++++++-------- src/components/Loading/index.ts | 1 + .../AudioRecorderRecordingControls.tsx | 7 +++++-- .../AudioAttachmentPreview.tsx | 6 +++--- .../FileAttachmentPreview.tsx | 6 +++--- .../MediaAttachmentPreview.tsx | 8 +++----- src/context/ComponentContext.tsx | 6 +++--- 7 files changed, 24 insertions(+), 24 deletions(-) rename src/components/{MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx => Loading/UploadProgressIndicator.tsx} (60%) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx b/src/components/Loading/UploadProgressIndicator.tsx similarity index 60% rename from src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx rename to src/components/Loading/UploadProgressIndicator.tsx index f7a1814f8b..ffa12c203f 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx +++ b/src/components/Loading/UploadProgressIndicator.tsx @@ -1,21 +1,19 @@ import clsx from 'clsx'; import React from 'react'; -import { - CircularProgressIndicator as DefaultCircularProgressIndicator, - LoadingIndicator as DefaultLoadingIndicator, -} from '../../Loading'; -import { useComponentContext } from '../../../context'; +import { useComponentContext } from '../../context'; +import { CircularProgressIndicator as DefaultCircularProgressIndicator } from './CircularProgressIndicator'; +import { LoadingIndicator as DefaultLoadingIndicator } from './LoadingIndicator'; -export type AttachmentUploadProgressIndicatorProps = { +export type UploadProgressIndicatorProps = { className?: string; uploadProgress?: number; }; -export const AttachmentUploadProgressIndicator = ({ +export const UploadProgressIndicator = ({ className, uploadProgress, -}: AttachmentUploadProgressIndicatorProps) => { +}: UploadProgressIndicatorProps) => { const { CircularProgressIndicator = DefaultCircularProgressIndicator, LoadingIndicator = DefaultLoadingIndicator, diff --git a/src/components/Loading/index.ts b/src/components/Loading/index.ts index 8bf0565afe..944c54b5ae 100644 --- a/src/components/Loading/index.ts +++ b/src/components/Loading/index.ts @@ -3,3 +3,4 @@ export * from './LoadingChannels'; export * from './LoadingErrorIndicator'; export * from './LoadingIndicator'; export * from './CircularProgressIndicator'; +export * from './UploadProgressIndicator'; diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx index 229b21a179..461e06d4bd 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx @@ -3,13 +3,14 @@ import { IconDelete, IconPauseFill, IconVoice } from '../../Icons'; import React from 'react'; import { useChatContext, + useComponentContext, useMessageComposerContext, useTranslationContext, } from '../../../context'; import { isRecording } from './recordingStateIdentity'; import { Button } from '../../Button'; import { addNotificationTargetTag, useNotificationTarget } from '../../Notifications'; -import { AttachmentUploadProgressIndicator } from '../../MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator'; +import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; const ToggleRecordingButton = () => { const { @@ -35,6 +36,8 @@ const ToggleRecordingButton = () => { export const AudioRecorderRecordingControls = () => { const { client } = useChatContext(); const { t } = useTranslationContext(); + const { UploadProgressIndicator = DefaultUploadProgressIndicator } = + useComponentContext(); const { recordingController: { completeRecording, recorder, recording, recordingState }, } = useMessageComposerContext(); @@ -82,7 +85,7 @@ export const AudioRecorderRecordingControls = () => { variant='primary' > {isUploadingFile ? ( - + ) : ( )} diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index 1a18c07d75..16ae198ed7 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -7,7 +7,7 @@ import { import { useComponentContext, useTranslationContext } from '../../../context'; import React, { useEffect } from 'react'; import clsx from 'clsx'; -import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; +import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { IconExclamationMark, IconExclamationTriangleFill } from '../../Icons'; @@ -44,7 +44,7 @@ export const AudioAttachmentPreview = ({ const { t } = useTranslationContext(); const { AttachmentUploadedSizeIndicator = DefaultAttachmentUploadedSizeIndicator, - AttachmentUploadProgressIndicator = DefaultAttachmentUploadProgressIndicator, + UploadProgressIndicator = DefaultUploadProgressIndicator, } = useComponentContext(); const { id, previewUri, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; @@ -98,7 +98,7 @@ export const AudioAttachmentPreview = ({
{uploadState === 'uploading' && ( - + )} {showProgressControls ? ( <> diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index 22444d2dd2..f13e1e9d50 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useComponentContext, useTranslationContext } from '../../../context'; import { FileIcon } from '../../FileIcon'; -import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; +import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; import { AttachmentUploadedSizeIndicator as DefaultAttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; import type { LocalAudioAttachment, LocalFileAttachment } from 'stream-chat'; import type { UploadAttachmentPreviewProps } from './types'; @@ -22,7 +22,7 @@ export const FileAttachmentPreview = ({ const { t } = useTranslationContext('FilePreview'); const { AttachmentUploadedSizeIndicator = DefaultAttachmentUploadedSizeIndicator, - AttachmentUploadProgressIndicator = DefaultAttachmentUploadProgressIndicator, + UploadProgressIndicator = DefaultUploadProgressIndicator, } = useComponentContext(); const { id, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; @@ -47,7 +47,7 @@ export const FileAttachmentPreview = ({
{uploadState === 'uploading' && ( - + )} {hasFatalError && ( diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index b5edfdd91e..c48b945b7b 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -17,7 +17,7 @@ import clsx from 'clsx'; import { IconExclamationMark, IconRetry, IconVideoFill } from '../../Icons'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { Button } from '../../Button'; -import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './AttachmentUploadProgressIndicator'; +import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; export type MediaAttachmentPreviewProps> = @@ -35,8 +35,8 @@ export const MediaAttachmentPreview = ({ }: MediaAttachmentPreviewProps) => { const { t } = useTranslationContext(); const { - AttachmentUploadProgressIndicator = DefaultAttachmentUploadProgressIndicator, BaseImage = DefaultBaseImage, + UploadProgressIndicator = DefaultUploadProgressIndicator, } = useComponentContext(); const [thumbnailPreviewError, setThumbnailPreviewError] = useState(false); @@ -97,9 +97,7 @@ export const MediaAttachmentPreview = ({ )}
- {isUploading && ( - - )} + {isUploading && } {isVideoAttachment(attachment) && !hasUploadError && diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 8e36e50c14..e615f3545d 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -75,7 +75,7 @@ import type { EditedMessagePreviewProps } from '../components/MessageComposer/Ed import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; import type { CircularProgressIndicatorProps } from '../components/Loading/CircularProgressIndicator'; -import type { AttachmentUploadProgressIndicatorProps } from '../components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator'; +import type { UploadProgressIndicatorProps } from '../components/Loading/UploadProgressIndicator'; import type { AttachmentUploadedSizeIndicatorProps } from '../components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator'; export type ComponentContextValue = { @@ -89,8 +89,6 @@ export type ComponentContextValue = { AttachmentPreviewList?: React.ComponentType; /** Custom UI component to control adding attachments to MessageComposer, defaults to and accepts same props as: [AttachmentSelector](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentSelector.tsx) */ AttachmentSelector?: React.ComponentType; - /** Custom UI component for upload progress on attachment previews in MessageComposer, defaults to and accepts same props as: [AttachmentUploadProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadProgressIndicator.tsx) */ - AttachmentUploadProgressIndicator?: React.ComponentType; /** Custom UI component for uploaded/total size on attachment previews in MessageComposer, defaults to and accepts same props as: [AttachmentUploadedSizeIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx) */ AttachmentUploadedSizeIndicator?: React.ComponentType; /** Custom UI component for the dedicated voice recording preview slot above composer attachments (REACT-794), defaults to and accepts same props as: [VoiceRecordingPreviewSlot](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/VoiceRecordingPreviewSlot.tsx) */ @@ -272,6 +270,8 @@ export type ComponentContextValue = { UnreadMessagesNotification?: React.ComponentType; /** Custom UI component that separates read messages from unread, defaults to and accepts same props as: [UnreadMessagesSeparator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/UnreadMessagesSeparator.tsx) */ UnreadMessagesSeparator?: React.ComponentType; + /** Custom UI component for upload progress (e.g. attachment previews, audio recording), defaults to and accepts same props as: [UploadProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/UploadProgressIndicator.tsx) */ + UploadProgressIndicator?: React.ComponentType; /** Component used to play video. If not provided, ReactPlayer is used as a default video player. */ VideoPlayer?: React.ComponentType; /** Custom UI component to display a message in the `VirtualizedMessageList`, does not have a default implementation */ From 3a8d1fb9de8804d433ca040981e5c8f0706e3972 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 11:18:03 +0200 Subject: [PATCH 16/26] feat: update stream-chat-version --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 119953e4f3..72b6b4443e 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "emoji-mart": "^5.4.0", "react": "^19.0.0 || ^18.0.0 || ^17.0.0", "react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0", - "stream-chat": "^9.38.0" + "stream-chat": "^9.41.0" }, "peerDependenciesMeta": { "@breezystack/lamejs": { @@ -176,7 +176,7 @@ "react-dom": "^19.0.0", "sass": "^1.97.2", "semantic-release": "^25.0.2", - "stream-chat": "^9.38.0", + "stream-chat": "^9.41.0", "typescript": "^5.4.5", "typescript-eslint": "^8.17.0", "vite": "^7.3.1", diff --git a/yarn.lock b/yarn.lock index a511f1a7b4..6e124788a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7753,10 +7753,10 @@ stdin-discarder@^0.2.2: resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be" integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== -stream-chat@^9.38.0: - version "9.38.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.38.0.tgz#5c13eb8bbc2fa4adb774687b0c9c51f129d1b458" - integrity sha512-nyTFKHnhGfk1Op/xuZzPKzM9uNTy4TBma69+ApwGj/UtrK2pT6rSaU0Qy/oAqub+Bh7jR2/5vlV/8FWJ2BObFg== +stream-chat@^9.41.0: + version "9.41.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.41.0.tgz#ad88d7919aaf1d3c35b4a431a8cd464cb640f146" + integrity sha512-Rgp3vULGKYxHZ/aCeundly6ngdBGttTPz+YknmWhbqvNlEhPB/RM61CpQPHgPyfkSm+osJT3tEV9fKd+I/S77g== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" From 02e973f1a31336b886914d068f0f3572f440260b Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 12:59:16 +0200 Subject: [PATCH 17/26] refactor: remove unnecessary formatUploadByteFraction method --- .../AttachmentUploadedSizeIndicator.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx index b0e2b8e980..8acf81bd56 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx @@ -2,19 +2,13 @@ import React from 'react'; import { FileSizeIndicator } from '../../Attachment'; import { prettifyFileSize } from '../hooks/utils'; -function safePrettifyFileSize(bytes: number, maximumFractionDigits?: number): string { - if (!Number.isFinite(bytes) || bytes < 0) return ''; - if (bytes === 0) return '0 B'; - return prettifyFileSize(bytes, maximumFractionDigits); -} - function formatUploadByteFraction( uploadPercent: number, fullBytes: number, maximumFractionDigits?: number, ): string { const uploaded = Math.round((uploadPercent / 100) * fullBytes); - return `${safePrettifyFileSize(uploaded, maximumFractionDigits)} / ${safePrettifyFileSize(fullBytes, maximumFractionDigits)}`; + return `${prettifyFileSize(uploaded, maximumFractionDigits)} / ${prettifyFileSize(fullBytes, maximumFractionDigits)}`; } function resolveAttachmentFullByteSize(attachment: { From 388416d0c137d4cc3f249fda893d34d73b168bb0 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 13:14:58 +0200 Subject: [PATCH 18/26] refactor: simplify AttachmentUploadedSizeIndicator --- .../AttachmentUploadedSizeIndicator.tsx | 27 +++++++------------ .../styling/AttachmentPreview.scss | 4 --- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx index 8acf81bd56..c5332d0ef3 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx @@ -1,15 +1,5 @@ import React from 'react'; import { FileSizeIndicator } from '../../Attachment'; -import { prettifyFileSize } from '../hooks/utils'; - -function formatUploadByteFraction( - uploadPercent: number, - fullBytes: number, - maximumFractionDigits?: number, -): string { - const uploaded = Math.round((uploadPercent / 100) * fullBytes); - return `${prettifyFileSize(uploaded, maximumFractionDigits)} / ${prettifyFileSize(fullBytes, maximumFractionDigits)}`; -} function resolveAttachmentFullByteSize(attachment: { file_size?: number | string; @@ -44,19 +34,20 @@ export const AttachmentUploadedSizeIndicator = ({ }: AttachmentUploadedSizeIndicatorProps) => { const { uploadProgress, uploadState } = attachment.localMetadata ?? {}; const fullBytes = resolveAttachmentFullByteSize(attachment); + const uploaded = + uploadProgress !== undefined && fullBytes !== undefined + ? Math.round((uploadProgress / 100) * fullBytes) + : undefined; - if ( - uploadState === 'uploading' && - uploadProgress !== undefined && - fullBytes !== undefined - ) { + if (uploadState === 'uploading' && uploaded) { return ( - - {formatUploadByteFraction(uploadProgress, fullBytes)} - + {` / `} + +
); } diff --git a/src/components/MessageComposer/styling/AttachmentPreview.scss b/src/components/MessageComposer/styling/AttachmentPreview.scss index efa461e75a..cb9ee52531 100644 --- a/src/components/MessageComposer/styling/AttachmentPreview.scss +++ b/src/components/MessageComposer/styling/AttachmentPreview.scss @@ -290,10 +290,6 @@ color: var(--accent-primary); } - .str-chat__attachment-preview-file__upload-size-fraction { - white-space: nowrap; - } - .str-chat__attachment-preview-file__fatal-error { display: flex; align-items: center; From 52f063d3e3c4df49a41eb819e7fe26f946667bab Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 13:56:14 +0200 Subject: [PATCH 19/26] refactor: create UploadedSizeIndicator component --- .../Loading/UploadedSizeIndicator.tsx | 21 +++++++++++++++++++ src/components/Loading/index.ts | 1 + .../AttachmentUploadedSizeIndicator.tsx | 16 ++++++-------- src/context/ComponentContext.tsx | 3 +++ 4 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 src/components/Loading/UploadedSizeIndicator.tsx diff --git a/src/components/Loading/UploadedSizeIndicator.tsx b/src/components/Loading/UploadedSizeIndicator.tsx new file mode 100644 index 0000000000..1f78c7aba5 --- /dev/null +++ b/src/components/Loading/UploadedSizeIndicator.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { FileSizeIndicator } from '../Attachment'; + +export type UploadedSizeIndicatorProps = { + fullBytes: number; + uploadedBytes: number; +}; + +export const UploadedSizeIndicator = ({ + fullBytes, + uploadedBytes, +}: UploadedSizeIndicatorProps) => ( +
+ {` / `} + +
+); diff --git a/src/components/Loading/index.ts b/src/components/Loading/index.ts index 944c54b5ae..4a7604cde7 100644 --- a/src/components/Loading/index.ts +++ b/src/components/Loading/index.ts @@ -4,3 +4,4 @@ export * from './LoadingErrorIndicator'; export * from './LoadingIndicator'; export * from './CircularProgressIndicator'; export * from './UploadProgressIndicator'; +export * from './UploadedSizeIndicator'; diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx index c5332d0ef3..642a9f77f0 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx @@ -1,5 +1,8 @@ import React from 'react'; + +import { useComponentContext } from '../../../context'; import { FileSizeIndicator } from '../../Attachment'; +import { UploadedSizeIndicator as DefaultUploadedSizeIndicator } from '../../Loading/UploadedSizeIndicator'; function resolveAttachmentFullByteSize(attachment: { file_size?: number | string; @@ -32,6 +35,7 @@ export type AttachmentUploadedSizeIndicatorProps = { export const AttachmentUploadedSizeIndicator = ({ attachment, }: AttachmentUploadedSizeIndicatorProps) => { + const { UploadedSizeIndicator = DefaultUploadedSizeIndicator } = useComponentContext(); const { uploadProgress, uploadState } = attachment.localMetadata ?? {}; const fullBytes = resolveAttachmentFullByteSize(attachment); const uploaded = @@ -39,16 +43,8 @@ export const AttachmentUploadedSizeIndicator = ({ ? Math.round((uploadProgress / 100) * fullBytes) : undefined; - if (uploadState === 'uploading' && uploaded) { - return ( -
- {` / `} - -
- ); + if (uploadState === 'uploading' && uploaded !== undefined && fullBytes !== undefined) { + return ; } if (uploadState === 'finished') { diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index e615f3545d..93d69aa01c 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -75,6 +75,7 @@ import type { EditedMessagePreviewProps } from '../components/MessageComposer/Ed import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; import type { CircularProgressIndicatorProps } from '../components/Loading/CircularProgressIndicator'; +import type { UploadedSizeIndicatorProps } from '../components/Loading/UploadedSizeIndicator'; import type { UploadProgressIndicatorProps } from '../components/Loading/UploadProgressIndicator'; import type { AttachmentUploadedSizeIndicatorProps } from '../components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator'; @@ -272,6 +273,8 @@ export type ComponentContextValue = { UnreadMessagesSeparator?: React.ComponentType; /** Custom UI component for upload progress (e.g. attachment previews, audio recording), defaults to and accepts same props as: [UploadProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/UploadProgressIndicator.tsx) */ UploadProgressIndicator?: React.ComponentType; + /** Custom UI component for uploaded vs total byte size during attachment upload (MessageComposer previews), defaults to and accepts same props as: [UploadedSizeIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/UploadedSizeIndicator.tsx) */ + UploadedSizeIndicator?: React.ComponentType; /** Component used to play video. If not provided, ReactPlayer is used as a default video player. */ VideoPlayer?: React.ComponentType; /** Custom UI component to display a message in the `VirtualizedMessageList`, does not have a default implementation */ From d5a34c5cb5e89790eb184a71c87e9258b038c685 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 14:01:05 +0200 Subject: [PATCH 20/26] refactor: remove UploadProgressIndicator and AttachmentUploadedSizeIndicator from ComponentContext --- .../AudioRecorder/AudioRecorderRecordingControls.tsx | 5 +---- .../AttachmentPreviewList/AudioAttachmentPreview.tsx | 10 +++------- .../AttachmentPreviewList/FileAttachmentPreview.tsx | 10 +++------- .../AttachmentPreviewList/MediaAttachmentPreview.tsx | 7 ++----- src/context/ComponentContext.tsx | 6 ------ 5 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx index 461e06d4bd..51aa626290 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx @@ -3,14 +3,13 @@ import { IconDelete, IconPauseFill, IconVoice } from '../../Icons'; import React from 'react'; import { useChatContext, - useComponentContext, useMessageComposerContext, useTranslationContext, } from '../../../context'; import { isRecording } from './recordingStateIdentity'; import { Button } from '../../Button'; import { addNotificationTargetTag, useNotificationTarget } from '../../Notifications'; -import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; +import { UploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; const ToggleRecordingButton = () => { const { @@ -36,8 +35,6 @@ const ToggleRecordingButton = () => { export const AudioRecorderRecordingControls = () => { const { client } = useChatContext(); const { t } = useTranslationContext(); - const { UploadProgressIndicator = DefaultUploadProgressIndicator } = - useComponentContext(); const { recordingController: { completeRecording, recorder, recording, recordingState }, } = useMessageComposerContext(); diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index 16ae198ed7..071ecfeca7 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -4,10 +4,10 @@ import { type LocalAudioAttachment, type LocalVoiceRecordingAttachment, } from 'stream-chat'; -import { useComponentContext, useTranslationContext } from '../../../context'; +import { useTranslationContext } from '../../../context'; import React, { useEffect } from 'react'; import clsx from 'clsx'; -import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; +import { UploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; import { IconExclamationMark, IconExclamationTriangleFill } from '../../Icons'; @@ -20,7 +20,7 @@ import { } from '../../AudioPlayback'; import { useAudioPlayer } from '../../AudioPlayback/WithAudioPlayback'; import { useStateStore } from '../../../store'; -import { AttachmentUploadedSizeIndicator as DefaultAttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; +import { AttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; export type AudioAttachmentPreviewProps> = UploadAttachmentPreviewProps< @@ -42,10 +42,6 @@ export const AudioAttachmentPreview = ({ removeAttachments, }: AudioAttachmentPreviewProps) => { const { t } = useTranslationContext(); - const { - AttachmentUploadedSizeIndicator = DefaultAttachmentUploadedSizeIndicator, - UploadProgressIndicator = DefaultUploadProgressIndicator, - } = useComponentContext(); const { id, previewUri, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; const url = attachment.asset_url || previewUri; diff --git a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx index f13e1e9d50..225f12d5a2 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useComponentContext, useTranslationContext } from '../../../context'; +import { useTranslationContext } from '../../../context'; import { FileIcon } from '../../FileIcon'; -import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; -import { AttachmentUploadedSizeIndicator as DefaultAttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; +import { UploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; +import { AttachmentUploadedSizeIndicator } from './AttachmentUploadedSizeIndicator'; import type { LocalAudioAttachment, LocalFileAttachment } from 'stream-chat'; import type { UploadAttachmentPreviewProps } from './types'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; @@ -20,10 +20,6 @@ export const FileAttachmentPreview = ({ removeAttachments, }: FileAttachmentPreviewProps) => { const { t } = useTranslationContext('FilePreview'); - const { - AttachmentUploadedSizeIndicator = DefaultAttachmentUploadedSizeIndicator, - UploadProgressIndicator = DefaultUploadProgressIndicator, - } = useComponentContext(); const { id, uploadPermissionCheck, uploadProgress, uploadState } = attachment.localMetadata ?? {}; diff --git a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx index c48b945b7b..923a955a00 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -17,7 +17,7 @@ import clsx from 'clsx'; import { IconExclamationMark, IconRetry, IconVideoFill } from '../../Icons'; import { RemoveAttachmentPreviewButton } from '../RemoveAttachmentPreviewButton'; import { Button } from '../../Button'; -import { UploadProgressIndicator as DefaultUploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; +import { UploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; import { AttachmentPreviewRoot } from './utils/AttachmentPreviewRoot'; export type MediaAttachmentPreviewProps> = @@ -34,10 +34,7 @@ export const MediaAttachmentPreview = ({ removeAttachments, }: MediaAttachmentPreviewProps) => { const { t } = useTranslationContext(); - const { - BaseImage = DefaultBaseImage, - UploadProgressIndicator = DefaultUploadProgressIndicator, - } = useComponentContext(); + const { BaseImage = DefaultBaseImage } = useComponentContext(); const [thumbnailPreviewError, setThumbnailPreviewError] = useState(false); const { id, uploadPermissionCheck, uploadProgress, uploadState } = diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 93d69aa01c..301f4e0b6a 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -76,8 +76,6 @@ import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; import type { CircularProgressIndicatorProps } from '../components/Loading/CircularProgressIndicator'; import type { UploadedSizeIndicatorProps } from '../components/Loading/UploadedSizeIndicator'; -import type { UploadProgressIndicatorProps } from '../components/Loading/UploadProgressIndicator'; -import type { AttachmentUploadedSizeIndicatorProps } from '../components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator'; export type ComponentContextValue = { /** Custom UI component to display additional message composer action buttons left to the textarea, defaults to and accepts same props as: [AdditionalMessageComposerActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/MessageComposerActions.tsx) */ @@ -90,8 +88,6 @@ export type ComponentContextValue = { AttachmentPreviewList?: React.ComponentType; /** Custom UI component to control adding attachments to MessageComposer, defaults to and accepts same props as: [AttachmentSelector](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentSelector.tsx) */ AttachmentSelector?: React.ComponentType; - /** Custom UI component for uploaded/total size on attachment previews in MessageComposer, defaults to and accepts same props as: [AttachmentUploadedSizeIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx) */ - AttachmentUploadedSizeIndicator?: React.ComponentType; /** Custom UI component for the dedicated voice recording preview slot above composer attachments (REACT-794), defaults to and accepts same props as: [VoiceRecordingPreviewSlot](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/AttachmentPreviewList/VoiceRecordingPreviewSlot.tsx) */ VoiceRecordingPreviewSlot?: React.ComponentType; /** Custom UI component for contents of attachment selector initiation button */ @@ -271,8 +267,6 @@ export type ComponentContextValue = { UnreadMessagesNotification?: React.ComponentType; /** Custom UI component that separates read messages from unread, defaults to and accepts same props as: [UnreadMessagesSeparator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/UnreadMessagesSeparator.tsx) */ UnreadMessagesSeparator?: React.ComponentType; - /** Custom UI component for upload progress (e.g. attachment previews, audio recording), defaults to and accepts same props as: [UploadProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/UploadProgressIndicator.tsx) */ - UploadProgressIndicator?: React.ComponentType; /** Custom UI component for uploaded vs total byte size during attachment upload (MessageComposer previews), defaults to and accepts same props as: [UploadedSizeIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/UploadedSizeIndicator.tsx) */ UploadedSizeIndicator?: React.ComponentType; /** Component used to play video. If not provided, ReactPlayer is used as a default video player. */ From 028f6f62f85a77bf606feb63a22772358323345c Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 14:09:35 +0200 Subject: [PATCH 21/26] refactor: use ProgressIndicator in ComponentContext --- src/components/Loading/UploadProgressIndicator.tsx | 6 +++--- src/components/Loading/index.ts | 2 +- ...ircularProgressIndicator.tsx => progress-indicators.tsx} | 6 ++---- ...ircularProgressIndicator.scss => ProgressIndicator.scss} | 0 src/components/Loading/styling/index.scss | 2 +- src/context/ComponentContext.tsx | 6 +++--- 6 files changed, 10 insertions(+), 12 deletions(-) rename src/components/Loading/{CircularProgressIndicator.tsx => progress-indicators.tsx} (90%) rename src/components/Loading/styling/{CircularProgressIndicator.scss => ProgressIndicator.scss} (100%) diff --git a/src/components/Loading/UploadProgressIndicator.tsx b/src/components/Loading/UploadProgressIndicator.tsx index ffa12c203f..59a7fab4db 100644 --- a/src/components/Loading/UploadProgressIndicator.tsx +++ b/src/components/Loading/UploadProgressIndicator.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import React from 'react'; import { useComponentContext } from '../../context'; -import { CircularProgressIndicator as DefaultCircularProgressIndicator } from './CircularProgressIndicator'; +import { CircularProgressIndicator as DefaultProgressIndicator } from './progress-indicators'; import { LoadingIndicator as DefaultLoadingIndicator } from './LoadingIndicator'; export type UploadProgressIndicatorProps = { @@ -15,8 +15,8 @@ export const UploadProgressIndicator = ({ uploadProgress, }: UploadProgressIndicatorProps) => { const { - CircularProgressIndicator = DefaultCircularProgressIndicator, LoadingIndicator = DefaultLoadingIndicator, + ProgressIndicator = DefaultProgressIndicator, } = useComponentContext(); if (uploadProgress === undefined) { @@ -25,7 +25,7 @@ export const UploadProgressIndicator = ({ return (
- +
); }; diff --git a/src/components/Loading/index.ts b/src/components/Loading/index.ts index 4a7604cde7..e23c6a7c0d 100644 --- a/src/components/Loading/index.ts +++ b/src/components/Loading/index.ts @@ -2,6 +2,6 @@ export * from './LoadingChannel'; export * from './LoadingChannels'; export * from './LoadingErrorIndicator'; export * from './LoadingIndicator'; -export * from './CircularProgressIndicator'; +export * from './progress-indicators'; export * from './UploadProgressIndicator'; export * from './UploadedSizeIndicator'; diff --git a/src/components/Loading/CircularProgressIndicator.tsx b/src/components/Loading/progress-indicators.tsx similarity index 90% rename from src/components/Loading/CircularProgressIndicator.tsx rename to src/components/Loading/progress-indicators.tsx index 511d7d2fb7..51e5cc6f3a 100644 --- a/src/components/Loading/CircularProgressIndicator.tsx +++ b/src/components/Loading/progress-indicators.tsx @@ -5,15 +5,13 @@ import { useTranslationContext } from '../../context/TranslationContext'; const RING_RADIUS = 12; const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS; -export type CircularProgressIndicatorProps = { +export type ProgressIndicatorProps = { /** Clamped 0–100 completion. */ percent: number; }; /** Circular progress indicator with input from 0 to 100. */ -export const CircularProgressIndicator = ({ - percent, -}: CircularProgressIndicatorProps) => { +export const CircularProgressIndicator = ({ percent }: ProgressIndicatorProps) => { const { t } = useTranslationContext('CircularProgressIndicator'); const dashOffset = RING_CIRCUMFERENCE * (1 - percent / 100); diff --git a/src/components/Loading/styling/CircularProgressIndicator.scss b/src/components/Loading/styling/ProgressIndicator.scss similarity index 100% rename from src/components/Loading/styling/CircularProgressIndicator.scss rename to src/components/Loading/styling/ProgressIndicator.scss diff --git a/src/components/Loading/styling/index.scss b/src/components/Loading/styling/index.scss index 31084a7e00..e3b441c010 100644 --- a/src/components/Loading/styling/index.scss +++ b/src/components/Loading/styling/index.scss @@ -1,3 +1,3 @@ @use 'LoadingChannels'; @use 'LoadingIndicator'; -@use 'CircularProgressIndicator'; +@use 'ProgressIndicator'; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 301f4e0b6a..8f8fa0e0db 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -74,7 +74,7 @@ import type { VideoPlayerProps } from '../components/VideoPlayer'; import type { EditedMessagePreviewProps } from '../components/MessageComposer/EditedMessagePreview'; import type { FileIconProps } from '../components/FileIcon/FileIcon'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; -import type { CircularProgressIndicatorProps } from '../components/Loading/CircularProgressIndicator'; +import type { ProgressIndicatorProps } from '../components/Loading/progress-indicators'; import type { UploadedSizeIndicatorProps } from '../components/Loading/UploadedSizeIndicator'; export type ComponentContextValue = { @@ -150,8 +150,8 @@ export type ComponentContextValue = { LoadingErrorIndicator?: React.ComponentType; /** Custom UI component to render while the `MessageList` is loading new messages, defaults to and accepts same props as: [LoadingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/LoadingIndicator.tsx) */ LoadingIndicator?: React.ComponentType; - /** Custom UI component for determinate progress (0–100), defaults to and accepts same props as: [CircularProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/CircularProgressIndicator.tsx) */ - CircularProgressIndicator?: React.ComponentType; + /** Custom UI component for determinate progress (0–100), defaults to and accepts same props as: [ProgressIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/progress-indicators.tsx) */ + ProgressIndicator?: React.ComponentType; /** Custom UI component to display a message in the standard `MessageList`, defaults to and accepts the same props as: [MessageUI](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageUI.tsx) */ Message?: React.ComponentType; /** Custom UI component for message actions popup, accepts no props, all the defaults are set within [MessageActions (unstable)](https://github.com/GetStream/stream-chat-react/blob/master/src/experimental/MessageActions/MessageActions.tsx) */ From 225fe940e1201ec09935e3e9fac20f619492b020 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 14:12:09 +0200 Subject: [PATCH 22/26] refactor: remove unused className prop from UploadProgressIndicator --- src/components/Loading/UploadProgressIndicator.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Loading/UploadProgressIndicator.tsx b/src/components/Loading/UploadProgressIndicator.tsx index 59a7fab4db..8f9c92dd02 100644 --- a/src/components/Loading/UploadProgressIndicator.tsx +++ b/src/components/Loading/UploadProgressIndicator.tsx @@ -6,12 +6,10 @@ import { CircularProgressIndicator as DefaultProgressIndicator } from './progres import { LoadingIndicator as DefaultLoadingIndicator } from './LoadingIndicator'; export type UploadProgressIndicatorProps = { - className?: string; uploadProgress?: number; }; export const UploadProgressIndicator = ({ - className, uploadProgress, }: UploadProgressIndicatorProps) => { const { @@ -24,7 +22,7 @@ export const UploadProgressIndicator = ({ } return ( -
+
); From 4b51412d1d70b1edd948c4d499aac9f116fc6b9a Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 14:20:26 +0200 Subject: [PATCH 23/26] refactor: remover wrapper div from UploadProgressIndicator --- src/components/Loading/UploadProgressIndicator.tsx | 7 +------ src/components/Loading/progress-indicators.tsx | 2 +- .../MessageComposer/styling/AttachmentPreview.scss | 4 ++-- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/Loading/UploadProgressIndicator.tsx b/src/components/Loading/UploadProgressIndicator.tsx index 8f9c92dd02..5596518fe3 100644 --- a/src/components/Loading/UploadProgressIndicator.tsx +++ b/src/components/Loading/UploadProgressIndicator.tsx @@ -1,4 +1,3 @@ -import clsx from 'clsx'; import React from 'react'; import { useComponentContext } from '../../context'; @@ -21,9 +20,5 @@ export const UploadProgressIndicator = ({ return ; } - return ( -
- -
- ); + return ; }; diff --git a/src/components/Loading/progress-indicators.tsx b/src/components/Loading/progress-indicators.tsx index 51e5cc6f3a..b312bb7419 100644 --- a/src/components/Loading/progress-indicators.tsx +++ b/src/components/Loading/progress-indicators.tsx @@ -16,7 +16,7 @@ export const CircularProgressIndicator = ({ percent }: ProgressIndicatorProps) = const dashOffset = RING_CIRCUMFERENCE * (1 - percent / 100); return ( -
+
Date: Wed, 8 Apr 2026 14:32:22 +0200 Subject: [PATCH 24/26] feat: add FileSizeIndicator to ComponentContext --- src/components/Attachment/Audio.tsx | 5 +- src/components/Attachment/FileAttachment.tsx | 5 +- src/components/Attachment/VoiceRecording.tsx | 54 ++++++++++--------- .../components/FileSizeIndicator.tsx | 2 +- .../Loading/UploadedSizeIndicator.tsx | 24 +++++---- .../AttachmentUploadedSizeIndicator.tsx | 7 ++- src/context/ComponentContext.tsx | 3 ++ 7 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/components/Attachment/Audio.tsx b/src/components/Attachment/Audio.tsx index a8aca18af6..3f27e937d2 100644 --- a/src/components/Attachment/Audio.tsx +++ b/src/components/Attachment/Audio.tsx @@ -1,11 +1,11 @@ import React from 'react'; import type { Attachment } from 'stream-chat'; -import { FileSizeIndicator } from './components'; +import { FileSizeIndicator as DefaultFileSizeIndicator } from './components'; import type { AudioPlayerState } from '../AudioPlayback/AudioPlayer'; import { useAudioPlayer } from '../AudioPlayback/WithAudioPlayback'; import { useStateStore } from '../../store'; -import { useMessageContext } from '../../context'; +import { useComponentContext, useMessageContext } from '../../context'; import type { AudioPlayer } from '../AudioPlayback/AudioPlayer'; import { PlayButton } from '../Button/PlayButton'; import { FileIcon } from '../FileIcon'; @@ -17,6 +17,7 @@ type AudioAttachmentUIProps = { // todo: finish creating a BaseAudioPlayer derived from VoiceRecordingPlayerUI and AudioAttachmentUI const AudioAttachmentUI = ({ audioPlayer }: AudioAttachmentUIProps) => { + const { FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext(); const dataTestId = 'audio-widget'; const rootClassName = 'str-chat__message-attachment-audio-widget'; diff --git a/src/components/Attachment/FileAttachment.tsx b/src/components/Attachment/FileAttachment.tsx index fe2f8e19cf..7baed26cdc 100644 --- a/src/components/Attachment/FileAttachment.tsx +++ b/src/components/Attachment/FileAttachment.tsx @@ -3,14 +3,15 @@ import { useComponentContext } from '../../context/ComponentContext'; import { FileIcon } from '../FileIcon'; import type { Attachment } from 'stream-chat'; -import { FileSizeIndicator } from './components'; +import { FileSizeIndicator as DefaultFileSizeIndicator } from './components'; export type FileAttachmentProps = { attachment: Attachment; }; export const FileAttachment = ({ attachment }: FileAttachmentProps) => { - const { AttachmentFileIcon } = useComponentContext(); + const { AttachmentFileIcon, FileSizeIndicator = DefaultFileSizeIndicator } = + useComponentContext(); const FileIconComponent = AttachmentFileIcon ?? FileIcon; return (
{ + const { FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext(); const { canPlayRecord, durationSeconds, @@ -130,31 +135,32 @@ export const VoiceRecordingPlayer = ({ export type QuotedVoiceRecordingProps = Pick; -export const QuotedVoiceRecording = ({ attachment }: QuotedVoiceRecordingProps) => ( - // const { t } = useTranslationContext(); - // const title = attachment.title || t('Voice message'); -
-
-
-
- {attachment.duration ? ( - - ) : ( - - )} +export const QuotedVoiceRecording = ({ attachment }: QuotedVoiceRecordingProps) => { + const { FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext(); + return ( +
+
+
+
+ {attachment.duration ? ( + + ) : ( + + )} +
+
- -
-); + ); +}; export type VoiceRecordingProps = { /** The attachment object from the message's attachment list. */ diff --git a/src/components/Attachment/components/FileSizeIndicator.tsx b/src/components/Attachment/components/FileSizeIndicator.tsx index c95291db35..40926c8289 100644 --- a/src/components/Attachment/components/FileSizeIndicator.tsx +++ b/src/components/Attachment/components/FileSizeIndicator.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { prettifyFileSize } from '../../MessageComposer/hooks/utils'; -type FileSizeIndicatorProps = { +export type FileSizeIndicatorProps = { /** file size in byte */ fileSize?: number | string; /** diff --git a/src/components/Loading/UploadedSizeIndicator.tsx b/src/components/Loading/UploadedSizeIndicator.tsx index 1f78c7aba5..6a7b3c4b6d 100644 --- a/src/components/Loading/UploadedSizeIndicator.tsx +++ b/src/components/Loading/UploadedSizeIndicator.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { FileSizeIndicator } from '../Attachment'; +import { useComponentContext } from '../../context'; +import { FileSizeIndicator as DefaultFileSizeIndicator } from '../Attachment/components/FileSizeIndicator'; export type UploadedSizeIndicatorProps = { fullBytes: number; @@ -10,12 +11,15 @@ export type UploadedSizeIndicatorProps = { export const UploadedSizeIndicator = ({ fullBytes, uploadedBytes, -}: UploadedSizeIndicatorProps) => ( -
- {` / `} - -
-); +}: UploadedSizeIndicatorProps) => { + const { FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext(); + return ( +
+ {` / `} + +
+ ); +}; diff --git a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx index 642a9f77f0..323be3cb7e 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AttachmentUploadedSizeIndicator.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useComponentContext } from '../../../context'; -import { FileSizeIndicator } from '../../Attachment'; +import { FileSizeIndicator as DefaultFileSizeIndicator } from '../../Attachment/components/FileSizeIndicator'; import { UploadedSizeIndicator as DefaultUploadedSizeIndicator } from '../../Loading/UploadedSizeIndicator'; function resolveAttachmentFullByteSize(attachment: { @@ -35,7 +35,10 @@ export type AttachmentUploadedSizeIndicatorProps = { export const AttachmentUploadedSizeIndicator = ({ attachment, }: AttachmentUploadedSizeIndicatorProps) => { - const { UploadedSizeIndicator = DefaultUploadedSizeIndicator } = useComponentContext(); + const { + FileSizeIndicator = DefaultFileSizeIndicator, + UploadedSizeIndicator = DefaultUploadedSizeIndicator, + } = useComponentContext(); const { uploadProgress, uploadState } = attachment.localMetadata ?? {}; const fullBytes = resolveAttachmentFullByteSize(attachment); const uploaded = diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 8f8fa0e0db..a9d11e1b4b 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -73,6 +73,7 @@ import type { StopAIGenerationButtonProps } from '../components/MessageComposer/ import type { VideoPlayerProps } from '../components/VideoPlayer'; import type { EditedMessagePreviewProps } from '../components/MessageComposer/EditedMessagePreview'; import type { FileIconProps } from '../components/FileIcon/FileIcon'; +import type { FileSizeIndicatorProps } from '../components/Attachment/components/FileSizeIndicator'; import type { CommandChipProps } from '../components/MessageComposer/CommandChip'; import type { ProgressIndicatorProps } from '../components/Loading/progress-indicators'; import type { UploadedSizeIndicatorProps } from '../components/Loading/UploadedSizeIndicator'; @@ -126,6 +127,8 @@ export type ComponentContextValue = { DateSeparator?: React.ComponentType; /** Custom UI component to display the contents on file drag-and-drop overlay, defaults to and accepts same props as: [FileDragAndDropContent](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/WithDragAndDropUpload.tsx) */ FileDragAndDropContent?: React.ComponentType; + /** Custom UI component to display a formatted file byte size (message attachments, upload previews), defaults to and accepts same props as: [FileSizeIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/components/FileSizeIndicator.tsx) */ + FileSizeIndicator?: React.ComponentType; /** Custom UI component to override default preview of edited message, defaults to and accepts same props as: [EditedMessagePreview](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageComposer/EditedMessagePreview.tsx) */ EditedMessagePreview?: React.ComponentType; /** Custom UI component for rendering button with emoji picker in MessageComposer */ From 76a5a6c55ce5ad23a196547649502cb709b4cc3f Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 14:37:02 +0200 Subject: [PATCH 25/26] chore: move prop definition to the top of the file --- src/components/Loading/progress-indicators.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Loading/progress-indicators.tsx b/src/components/Loading/progress-indicators.tsx index b312bb7419..ccc53db25b 100644 --- a/src/components/Loading/progress-indicators.tsx +++ b/src/components/Loading/progress-indicators.tsx @@ -2,14 +2,14 @@ import React from 'react'; import { useTranslationContext } from '../../context/TranslationContext'; -const RING_RADIUS = 12; -const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS; - export type ProgressIndicatorProps = { /** Clamped 0–100 completion. */ percent: number; }; +const RING_RADIUS = 12; +const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS; + /** Circular progress indicator with input from 0 to 100. */ export const CircularProgressIndicator = ({ percent }: ProgressIndicatorProps) => { const { t } = useTranslationContext('CircularProgressIndicator'); From 5e3dbeede02814b2a0f7c8f5f1d0748df5596ab6 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 8 Apr 2026 15:02:49 +0200 Subject: [PATCH 26/26] fix: 0 byte results in NaN in FileSizeIndicator --- src/components/MessageComposer/hooks/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MessageComposer/hooks/utils.ts b/src/components/MessageComposer/hooks/utils.ts index 2fc762258f..95b210f36d 100644 --- a/src/components/MessageComposer/hooks/utils.ts +++ b/src/components/MessageComposer/hooks/utils.ts @@ -1,9 +1,9 @@ export function prettifyFileSize(bytes: number, precision = 3) { const units = ['B', 'kB', 'MB', 'GB']; - const exponent = Math.min( - Math.floor(Math.log(bytes) / Math.log(1024)), - units.length - 1, - ); + const exponent = + bytes === 0 + ? 0 + : Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const mantissa = bytes / 1024 ** exponent; const formattedMantissa = precision === 0 ? Math.round(mantissa).toString() : mantissa.toPrecision(precision);