From a613190468e369a34a8874a2d8563923f9a2daf5 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 29 Apr 2026 13:25:49 +0200 Subject: [PATCH 01/13] refactor: make message bubbles responsive --- src/components/Message/styling/Message.scss | 36 +++++++-------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index 508601180..06fe3bd1b 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -182,17 +182,10 @@ $message-bubble-padding: var(--str-chat__spacing-xs); @media (max-width: 767px) { --str-chat-message-options-size: var(--str-chat__message-options-button-size); - & .str-chat__message-bubble { - width: fit-content(var(--str-chat__message-max-width)); - max-width: min(100%, var(--str-chat__message-max-width)); - } - &.str-chat__message--other, &.str-chat__message--me { .str-chat__message-inner { margin-inline: 0; - width: fit-content; - max-width: min(100%, var(--str-chat__message-max-width)); .str-chat__message-reactions-host { justify-self: flex-start; @@ -204,22 +197,6 @@ $message-bubble-padding: var(--str-chat__spacing-xs); } } } - - &.str-chat__message--other { - .str-chat__message-inner { - grid-template-columns: auto var(--str-chat-message-options-size); - } - } - - &.str-chat__message--me { - .str-chat__message-inner { - grid-template-columns: var(--str-chat-message-options-size) auto; - } - - .str-chat__message-bubble { - justify-self: flex-end; - } - } } a { @@ -272,9 +249,10 @@ $message-bubble-padding: var(--str-chat__spacing-xs); 'reactions .' 'message-bubble options' 'replies replies'; - grid-template-columns: auto 1fr; + grid-template-columns: minmax(0, auto) 1fr; column-gap: var(--str-chat__space-8); position: relative; + max-width: 100%; .str-chat__message-reactions-host { display: flex; @@ -298,7 +276,7 @@ $message-bubble-padding: var(--str-chat__spacing-xs); .str-chat__message-bubble { width: fit-content(var(--str-chat__message-max-width)); - max-width: var(--str-chat__message-max-width); + max-width: min(var(--str-chat__message-max-width), 100%); min-width: 0; display: flex; flex-direction: column; @@ -536,6 +514,14 @@ $message-bubble-padding: var(--str-chat__spacing-xs); &.str-chat__message--has-attachment { --str-chat__message-max-width: var(--str-chat__message-with-attachment-max-width); + + .str-chat__message-inner { + width: fit-content(var(--str-chat__message-max-width)); + } + + .str-chat__message-bubble { + width: 100%; + } } &.str-chat__message--has-single-attachment.str-chat__message--has-giphy-attachment { From 98ceb7b0d89054149f3ef5ed1ce3c109bb90a14c Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 29 Apr 2026 13:59:09 +0200 Subject: [PATCH 02/13] fix: prevent images being squashed in narrow message lists --- src/components/Attachment/styling/LinkPreview.scss | 8 ++++++-- src/components/Attachment/styling/ModalGallery.scss | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/Attachment/styling/LinkPreview.scss b/src/components/Attachment/styling/LinkPreview.scss index e455e8405..7d756e2f7 100644 --- a/src/components/Attachment/styling/LinkPreview.scss +++ b/src/components/Attachment/styling/LinkPreview.scss @@ -90,13 +90,17 @@ padding: 0; img { - height: var(--str-chat__scraped-image-height); + aspect-ratio: 1.91 / 1; width: 100%; + height: auto; + // CDN resize requires max-height to be present on this element + max-height: var(--str-chat__scraped-image-height); border-radius: 0; } .str-chat__message-attachment-card--header:has(.str-chat__image-placeholder) { - height: var(--str-chat__scraped-image-height); + aspect-ratio: 1.91 / 1; + height: auto; .str-chat__image-placeholder { border-radius: 0; diff --git a/src/components/Attachment/styling/ModalGallery.scss b/src/components/Attachment/styling/ModalGallery.scss index fa37a432d..5fcd4dce8 100644 --- a/src/components/Attachment/styling/ModalGallery.scss +++ b/src/components/Attachment/styling/ModalGallery.scss @@ -19,9 +19,8 @@ border-radius: var(--str-chat__radius-lg); gap: var(--str-chat__space-2); width: $max-width; - max-width: $max-width; - // CDN resize requires height/max-height to be present on the img element, this rule ensures that - height: var(--str-chat__attachment-max-width); + max-width: 100%; + aspect-ratio: 4 / 3; .str-chat__modal-gallery__image { width: 100%; @@ -97,8 +96,9 @@ height: 100%; object-fit: cover; cursor: zoom-in; - // CDN resize requires max-width to be present on this element + // CDN resize requires max-width and max-height to be present on this element max-width: $max-width; + max-height: $max-width; transition: opacity 150ms ease-in-out; } From 4f08dadbfd5372ede6b51dc38a5f1168f44fa70a Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 29 Apr 2026 14:16:09 +0200 Subject: [PATCH 03/13] fix(ModalGallery): add missing border radii and correct gap btw images --- .../Attachment/styling/ModalGallery.scss | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/Attachment/styling/ModalGallery.scss b/src/components/Attachment/styling/ModalGallery.scss index 5fcd4dce8..0d10dc85c 100644 --- a/src/components/Attachment/styling/ModalGallery.scss +++ b/src/components/Attachment/styling/ModalGallery.scss @@ -5,7 +5,7 @@ $max-width: var(--str-chat__attachment-max-width); .str-chat__modal-gallery { - background: transparent; + background-color: var(--chat-bg); color: var(--str-chat__text-primary); border-radius: calc( var(--str-chat__message-bubble-radius-group-bottom) - var( @@ -17,36 +17,64 @@ grid-template-rows: 50% 50%; overflow: hidden; border-radius: var(--str-chat__radius-lg); - gap: var(--str-chat__space-2); + gap: var(--str-chat__spacing-xxs); width: $max-width; max-width: 100%; aspect-ratio: 4 / 3; + $outer-radius: var(--str-chat__radius-lg); + $inner-radius: var(--str-chat__radius-md); + .str-chat__modal-gallery__image { width: 100%; height: 100%; min-width: 0; min-height: 0; + border-radius: $inner-radius; + + &:nth-child(1) { + border-start-start-radius: $outer-radius; + } + &:nth-child(2) { + border-start-end-radius: $outer-radius; + } + &:nth-child(3) { + border-end-start-radius: $outer-radius; + } + &:nth-child(4) { + border-end-end-radius: $outer-radius; + } } &.str-chat__modal-gallery--two-images { grid-template-rows: 1fr; + + .str-chat__modal-gallery__image:nth-child(1) { + border-end-start-radius: $outer-radius; + } + + .str-chat__modal-gallery__image:nth-child(2) { + border-end-end-radius: $outer-radius; + } } &.str-chat__modal-gallery--three-images { .str-chat__modal-gallery__image:nth-child(1) { grid-column: 1; - grid-row: 1 / span 2; /* Span two rows */ + grid-row: 1 / span 2; + border-end-start-radius: $outer-radius; } .str-chat__modal-gallery__image:nth-child(2) { grid-column: 2; grid-row: 1; + border-start-end-radius: $outer-radius; } .str-chat__modal-gallery__image:nth-child(3) { grid-column: 2; grid-row: 2; + border-end-end-radius: $outer-radius; } } From d9ae9f3cf82474186ad3c5902210bdb973cd601c Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 12:10:43 +0200 Subject: [PATCH 04/13] fix: show the image retry load and failed load overlays correctly --- src/components/Attachment/ModalGallery.tsx | 12 +++--- .../Attachment/styling/ModalGallery.scss | 40 +++++++++++++++---- src/components/BaseImage/BaseImage.tsx | 12 ++++-- .../BaseImage/styling/ImagePlaceholder.scss | 2 + 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/components/Attachment/ModalGallery.tsx b/src/components/Attachment/ModalGallery.tsx index 89fb98d61..3557c68be 100644 --- a/src/components/Attachment/ModalGallery.tsx +++ b/src/components/Attachment/ModalGallery.tsx @@ -147,7 +147,10 @@ const ThumbnailButton = ({ const imageUrl = item.imageUrl; const [isLoadFailed, setIsLoadFailed] = useState(false); const [isImageLoading, setIsImageLoading] = useState(Boolean(imageUrl)); - const [retryCount, setRetryCount] = useState(0); + // Cache-busting suffix appended to image src on retry. Using a suffix instead of + // a React key remount keeps the component (and its placeholder) mounted, preventing + // layout shifts and height collapse during the reload attempt. + const [retrySuffix, setRetrySuffix] = useState(''); const { onError: itemOnError, @@ -161,7 +164,7 @@ const ThumbnailButton = ({ if (showRetryIndicator) { setIsLoadFailed(false); setIsImageLoading(true); - setRetryCount((currentRetryCount) => currentRetryCount + 1); + setRetrySuffix(`&retry=${Date.now()}`); return; } @@ -186,9 +189,6 @@ const ThumbnailButton = ({ ) : ( { @@ -201,7 +201,7 @@ const ThumbnailButton = ({ setIsLoadFailed(false); itemOnLoad?.(event); }} - src={imageUrl} + src={imageUrl ? `${imageUrl}${retrySuffix}` : imageUrl} {...(baseImageUsesDefaultBehavior ? { showDownloadButtonOnError: false } : {})} /> )} diff --git a/src/components/Attachment/styling/ModalGallery.scss b/src/components/Attachment/styling/ModalGallery.scss index 0d10dc85c..50547c9a0 100644 --- a/src/components/Attachment/styling/ModalGallery.scss +++ b/src/components/Attachment/styling/ModalGallery.scss @@ -1,8 +1,11 @@ @use '../../../styling/utils'; .str-chat__attachment-list { - .str-chat__message-attachment--gallery { + .str-chat__message-attachment--gallery, + .str-chat__message-attachment--image { $max-width: var(--str-chat__attachment-max-width); + min-width: 0; + max-width: 100%; .str-chat__modal-gallery { background-color: var(--chat-bg); @@ -13,8 +16,8 @@ ) ); display: grid; - grid-template-columns: 50% 50%; - grid-template-rows: 50% 50%; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; overflow: hidden; border-radius: var(--str-chat__radius-lg); gap: var(--str-chat__spacing-xxs); @@ -32,6 +35,17 @@ min-height: 0; border-radius: $inner-radius; + &.str-chat__modal-gallery__image--loading, + &.str-chat__modal-gallery__image--load-failed { + min-height: 0; + } + + &:only-child { + grid-column: 1 / -1; + grid-row: 1 / -1; + border-radius: $outer-radius; + } + &:nth-child(1) { border-start-start-radius: $outer-radius; } @@ -131,19 +145,31 @@ } &.str-chat__modal-gallery__image--loading { + min-height: 200px; + align-items: stretch; + img { + position: absolute; opacity: 0; } + + .str-chat__modal-gallery__image-loading-overlay { + position: static; + flex: 1; + min-width: 0; + height: auto; + } } &.str-chat__modal-gallery__image--load-failed { cursor: pointer; min-height: 200px; + align-items: stretch; .str-chat__image-placeholder.str-chat__base-image--load-failed { - width: 100%; - min-height: 200px; - align-self: stretch; + flex: 1; + min-width: 0; + height: auto; } img { @@ -167,7 +193,7 @@ display: flex; align-items: center; justify-content: center; - background-color: var(--chat-bg); + background-color: var(--str-chat__background-core-overlay-light); background-image: linear-gradient( 90deg, var(--str-chat__skeleton-loading-base) 0%, diff --git a/src/components/BaseImage/BaseImage.tsx b/src/components/BaseImage/BaseImage.tsx index 77ea3e9a1..70ddea3ce 100644 --- a/src/components/BaseImage/BaseImage.tsx +++ b/src/components/BaseImage/BaseImage.tsx @@ -20,14 +20,20 @@ export const BaseImage = forwardRef(function B showDownloadButtonOnError = false, ...imgProps } = props; - const [error, setError] = useState(false); + // Store the failed URL rather than a boolean so that when src changes (e.g. retry + // with a cache-busting param), the error state clears synchronously via the derived + // `error` check below. A boolean would require a useEffect to reset, causing a + // 1-frame flash of the error placeholder before the loading state kicks in. + const [failedSrc, setFailedSrc] = useState(null); const { ImagePlaceholder: ImagePlaceholderComponent = DefaultImagePlaceholder } = useComponentContext(); const sanitizedUrl = useMemo(() => sanitizeUrl(src), [src]); + const error = failedSrc === sanitizedUrl; + useEffect( () => () => { - setError(false); + setFailedSrc(null); }, [sanitizedUrl], ); @@ -50,7 +56,7 @@ export const BaseImage = forwardRef(function B alt={propsAlt ?? ''} className={clsx(propsClassName, 'str-chat__base-image')} onError={(e) => { - setError(true); + setFailedSrc(sanitizedUrl); propsOnError?.(e); }} ref={ref} diff --git a/src/components/BaseImage/styling/ImagePlaceholder.scss b/src/components/BaseImage/styling/ImagePlaceholder.scss index 1d1397364..10170d545 100644 --- a/src/components/BaseImage/styling/ImagePlaceholder.scss +++ b/src/components/BaseImage/styling/ImagePlaceholder.scss @@ -7,12 +7,14 @@ min-width: 0; min-height: 0; @include utils.flex-col-center; + overflow: hidden; background-color: var(--str-chat__background-core-overlay-light); svg { fill: var(--str-chat__accent-neutral); width: min(var(--str-chat__icon-size-lg, 32px), 50%); height: min(var(--str-chat__icon-size-lg, 32px), 50%); + flex-shrink: 0; } } } From 782d192967960d7ab30713a008388de8e0352eb7 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 12:11:37 +0200 Subject: [PATCH 05/13] fix: make poll responsive --- src/components/Poll/styling/Poll.scss | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/Poll/styling/Poll.scss b/src/components/Poll/styling/Poll.scss index 9fc0669c1..02bc9e5cd 100644 --- a/src/components/Poll/styling/Poll.scss +++ b/src/components/Poll/styling/Poll.scss @@ -6,13 +6,8 @@ flex-direction: column; gap: var(--str-chat__spacing-xl); padding: var(--str-chat__spacing-xs); - width: calc( - var(--str-chat__message-with-attachment-max-width) - calc( - 2 * var(--str-chat__spacing-xs) - ) - ); + min-width: min(260px, 100%); max-width: 100%; - min-width: 260px; font: var(--str-chat__font-caption-default); button { From da2ad4ac2bad82a40462825825a3941b86d2b4f8 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 12:12:22 +0200 Subject: [PATCH 06/13] fix: prevent cutting off the outline of search results header buttons --- src/components/Search/styling/Search.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/styling/Search.scss b/src/components/Search/styling/Search.scss index e84876600..f19ad4f3a 100644 --- a/src/components/Search/styling/Search.scss +++ b/src/components/Search/styling/Search.scss @@ -67,7 +67,6 @@ min-height: 0; .str-chat__search-results-header { - overflow-x: auto; scrollbar-width: none; .str-chat__search-results-header__filter-source-buttons { From 29bca3280ec3906b40347bd7786c0b441624c187 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 12:12:42 +0200 Subject: [PATCH 07/13] chore(demo): fix the filters --- examples/vite/src/App.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index b511bf8c0..5be3e02ae 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -291,11 +291,18 @@ const App = () => { { type: 'public' }, // public example channels { - cid: { - $in: ['random', 'general', 'music', 'jokes'].map( - (channelId) => `messaging:${channelId}`, - ), - }, + $and: [ + { + cid: { + $in: ['random', 'general', 'music', 'jokes'].map( + (channelId) => `messaging:${channelId}`, + ), + }, + }, + { + members: { $in: [userId] }, + }, + ], }, ], }), From f09d967ca503fa68f7018eba44697b1a7797a695 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 12:13:11 +0200 Subject: [PATCH 08/13] chore(demo): remove unnecessary preload link for woff2 fonts --- examples/vite/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/vite/index.html b/examples/vite/index.html index 0c2e031ff..4fbb263d7 100644 --- a/examples/vite/index.html +++ b/examples/vite/index.html @@ -9,7 +9,6 @@ -
From 36ac685737c1a1038b62bf51860c29beea3f55b8 Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 4 May 2026 15:35:03 +0200 Subject: [PATCH 09/13] fix: prevent Poll from shrinking --- src/components/Poll/styling/Poll.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Poll/styling/Poll.scss b/src/components/Poll/styling/Poll.scss index 02bc9e5cd..e7a309c60 100644 --- a/src/components/Poll/styling/Poll.scss +++ b/src/components/Poll/styling/Poll.scss @@ -6,8 +6,13 @@ flex-direction: column; gap: var(--str-chat__spacing-xl); padding: var(--str-chat__spacing-xs); - min-width: min(260px, 100%); + width: calc( + var(--str-chat__message-with-attachment-max-width) - calc( + 2 * var(--str-chat__spacing-xs) + ) + ); max-width: 100%; + min-width: min(260px, 100%); font: var(--str-chat__font-caption-default); button { From d514f745acc5bd6796372b85d541ecab3cf12dde Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 4 May 2026 15:49:02 +0200 Subject: [PATCH 10/13] fix: prevent message options to be pushed to the edge of the message row --- src/components/Message/styling/Message.scss | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index 4b38f4a40..bf2d0a94f 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -252,7 +252,7 @@ $message-bubble-padding: var(--str-chat__spacing-xs); grid-template-columns: minmax(0, auto) 1fr; column-gap: var(--str-chat__space-8); position: relative; - max-width: 100%; + width: fit-content; .str-chat__message-reactions-host { display: flex; @@ -519,10 +519,6 @@ $message-bubble-padding: var(--str-chat__spacing-xs); &.str-chat__message--has-attachment { --str-chat__message-max-width: var(--str-chat__message-with-attachment-max-width); - .str-chat__message-inner { - width: fit-content(var(--str-chat__message-max-width)); - } - .str-chat__message-bubble { width: 100%; } From f4ce13ac46a7b5cdb4733c262a1fb28a39880d0a Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 4 May 2026 15:51:40 +0200 Subject: [PATCH 11/13] style: fix linter issue --- src/components/Poll/styling/Poll.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Poll/styling/Poll.scss b/src/components/Poll/styling/Poll.scss index e7a309c60..91bd22a9d 100644 --- a/src/components/Poll/styling/Poll.scss +++ b/src/components/Poll/styling/Poll.scss @@ -7,8 +7,8 @@ gap: var(--str-chat__spacing-xl); padding: var(--str-chat__spacing-xs); width: calc( - var(--str-chat__message-with-attachment-max-width) - calc( - 2 * var(--str-chat__spacing-xs) + var(--str-chat__message-with-attachment-max-width) - calc( + 2 * var(--str-chat__spacing-xs) ) ); max-width: 100%; From 0eea2d16a2b323abaa5632afa6aa1bd5515a51c3 Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 4 May 2026 15:58:56 +0200 Subject: [PATCH 12/13] test: reflect load retry timestamp in image src --- src/components/Gallery/__tests__/ModalGallery.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Gallery/__tests__/ModalGallery.test.tsx b/src/components/Gallery/__tests__/ModalGallery.test.tsx index f2ef4541f..625a4ea81 100644 --- a/src/components/Gallery/__tests__/ModalGallery.test.tsx +++ b/src/components/Gallery/__tests__/ModalGallery.test.tsx @@ -365,7 +365,9 @@ describe('ModalGallery', () => { screen.getByTestId('str-chat__modal-gallery__image-loading-overlay'), ).toBeInTheDocument(); expect(retriedImage).not.toBe(image); - expect(retriedImage).toHaveAttribute('src', 'http://test-image.jpg'); + expect(retriedImage.getAttribute('src')).toMatch( + /^http:\/\/test-image\.jpg&retry=\d+$/, + ); fireEvent.load(retriedImage); fireEvent.click(container.querySelector('.str-chat__modal-gallery__image')); From baaad3d6b1a76193b91acd086d7387ad0450777f Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 4 May 2026 16:43:32 +0200 Subject: [PATCH 13/13] fix: prevent message-bubble area from growing --- src/components/Message/styling/Message.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index bf2d0a94f..271e674fa 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -249,7 +249,7 @@ $message-bubble-padding: var(--str-chat__spacing-xs); 'reactions .' 'message-bubble options' 'replies replies'; - grid-template-columns: minmax(0, auto) 1fr; + grid-template-columns: fit-content(var(--str-chat__message-max-width)) auto; column-gap: var(--str-chat__space-8); position: relative; width: fit-content; @@ -443,7 +443,7 @@ $message-bubble-padding: var(--str-chat__spacing-xs); '. reactions' 'options message-bubble' 'replies replies'; - grid-template-columns: 1fr auto; + grid-template-columns: auto fit-content(var(--str-chat__message-max-width)); margin-inline-start: var(--str-chat-message-options-size);