Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/vite/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" />
<link rel="preload" as="font" type="font/woff2" crossorigin href="https://fonts.gstatic.com/s/geist/v4/gyByhwUxId8gMEwcGFU.woff2" />
</head>
<body>
<div id="root"></div>
Expand Down
12 changes: 6 additions & 6 deletions src/components/Attachment/ModalGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -161,7 +164,7 @@ const ThumbnailButton = ({
if (showRetryIndicator) {
setIsLoadFailed(false);
setIsImageLoading(true);
setRetryCount((currentRetryCount) => currentRetryCount + 1);
setRetrySuffix(`&retry=${Date.now()}`);
return;
}

Expand All @@ -186,9 +189,6 @@ const ThumbnailButton = ({
<VideoThumbnail alt={t('User uploaded content')} src={item.videoThumbnailUrl} />
) : (
<BaseImage
// Remount the image on retry so the browser gets a fresh load attempt and
// BaseImage clears its local load-failed state.
key={retryCount}
{...baseImageProps}
alt={item.alt ?? t('User uploaded content')}
onError={(event) => {
Expand All @@ -201,7 +201,7 @@ const ThumbnailButton = ({
setIsLoadFailed(false);
itemOnLoad?.(event);
}}
src={imageUrl}
src={imageUrl ? `${imageUrl}${retrySuffix}` : imageUrl}
{...(baseImageUsesDefaultBehavior ? { showDownloadButtonOnError: false } : {})}
/>
)}
Expand Down
8 changes: 6 additions & 2 deletions src/components/Attachment/styling/LinkPreview.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
82 changes: 68 additions & 14 deletions src/components/Attachment/styling/ModalGallery.scss
Original file line number Diff line number Diff line change
@@ -1,53 +1,94 @@
@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: transparent;
background-color: var(--chat-bg);
color: var(--str-chat__text-primary);
border-radius: calc(
var(--str-chat__message-bubble-radius-group-bottom) - var(
--str-chat__attachment-margin
)
);
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__space-2);
gap: var(--str-chat__spacing-xxs);
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;

$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;

&.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;
}
&: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;
}
}

Expand Down Expand Up @@ -97,25 +138,38 @@
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;
}

&.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 {
Expand All @@ -139,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%,
Expand Down
12 changes: 9 additions & 3 deletions src/components/BaseImage/BaseImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@ export const BaseImage = forwardRef<HTMLImageElement, BaseImageProps>(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<string | null>(null);
const { ImagePlaceholder: ImagePlaceholderComponent = DefaultImagePlaceholder } =
useComponentContext();

const sanitizedUrl = useMemo(() => sanitizeUrl(src), [src]);
const error = failedSrc === sanitizedUrl;

useEffect(
() => () => {
setError(false);
setFailedSrc(null);
},
[sanitizedUrl],
);
Expand All @@ -50,7 +56,7 @@ export const BaseImage = forwardRef<HTMLImageElement, BaseImageProps>(function B
alt={propsAlt ?? ''}
className={clsx(propsClassName, 'str-chat__base-image')}
onError={(e) => {
setError(true);
setFailedSrc(sanitizedUrl);
propsOnError?.(e);
}}
ref={ref}
Expand Down
2 changes: 2 additions & 0 deletions src/components/BaseImage/styling/ImagePlaceholder.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
4 changes: 3 additions & 1 deletion src/components/Gallery/__tests__/ModalGallery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
34 changes: 8 additions & 26 deletions src/components/Message/styling/Message.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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: fit-content(var(--str-chat__message-max-width)) auto;
column-gap: var(--str-chat__space-8);
position: relative;
width: fit-content;

.str-chat__message-reactions-host {
display: flex;
Expand Down Expand Up @@ -302,7 +280,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;
Expand Down Expand Up @@ -465,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);

Expand Down Expand Up @@ -540,6 +518,10 @@ $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-bubble {
width: 100%;
}
}

&.str-chat__message--has-single-attachment.str-chat__message--has-giphy-attachment {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Poll/styling/Poll.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
)
);
max-width: 100%;
min-width: 260px;
min-width: min(260px, 100%);
font: var(--str-chat__font-caption-default);

button {
Expand Down
1 change: 0 additions & 1 deletion src/components/Search/styling/Search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down