diff --git a/src/components/Attachment/styling/LinkPreview.scss b/src/components/Attachment/styling/LinkPreview.scss index 9fab698d69..976bed466e 100644 --- a/src/components/Attachment/styling/LinkPreview.scss +++ b/src/components/Attachment/styling/LinkPreview.scss @@ -20,7 +20,7 @@ line-height: var(--typography-line-height-tight); * { - color: var(--chat-text-incoming, #1a1b25); + color: var(--chat-text-incoming); } .str-chat__message-attachment-card--header { @@ -32,8 +32,12 @@ flex-shrink: 0; overflow: hidden; - img { + img, + .str-chat__image-placeholder { border-radius: var(--message-bubble-radius-attachment-inline, 8px); + } + + img { object-fit: cover; width: 40px; height: 40px; @@ -96,6 +100,14 @@ border-radius: 0; } + .str-chat__message-attachment-card--header:has(.str-chat__image-placeholder) { + height: var(--str-chat__scraped-image-height); + + .str-chat__image-placeholder { + border-radius: 0; + } + } + .str-chat__message-attachment-card--content { padding: var(--spacing-sm); diff --git a/src/components/Gallery/BaseImage.tsx b/src/components/Gallery/BaseImage.tsx index b2e1e1b277..be3577465e 100644 --- a/src/components/Gallery/BaseImage.tsx +++ b/src/components/Gallery/BaseImage.tsx @@ -1,6 +1,8 @@ import React, { forwardRef, useEffect, useMemo, useState } from 'react'; import clsx from 'clsx'; +import { useComponentContext } from '../../context/ComponentContext'; import { DownloadButton } from '../Attachment'; +import { ImagePlaceholder as DefaultImagePlaceholder } from './ImagePlaceholder'; import { sanitizeUrl } from '@braintree/sanitize-url'; export type BaseImageProps = React.ComponentPropsWithRef<'img'> & { @@ -12,12 +14,15 @@ export const BaseImage = forwardRef(function B ref, ) { const { + alt: propsAlt, className: propsClassName, onError: propsOnError, - showDownloadButtonOnError = true, + showDownloadButtonOnError = false, ...imgProps } = props; const [error, setError] = useState(false); + const { ImagePlaceholder: ImagePlaceholderComponent = DefaultImagePlaceholder } = + useComponentContext(); const sanitizedUrl = useMemo(() => sanitizeUrl(src), [src]); useEffect( @@ -27,22 +32,29 @@ export const BaseImage = forwardRef(function B [sanitizedUrl], ); + if (error) { + return ( + <> + + {showDownloadButtonOnError && } + + ); + } + return ( - <> - { - setError(true); - propsOnError?.(e); - }} - ref={ref} - src={sanitizedUrl} - /> - {error && showDownloadButtonOnError && } - + {propsAlt} { + setError(true); + propsOnError?.(e); + }} + ref={ref} + src={sanitizedUrl} + /> ); }); diff --git a/src/components/Gallery/ImagePlaceholder.tsx b/src/components/Gallery/ImagePlaceholder.tsx new file mode 100644 index 0000000000..aa4b472c8e --- /dev/null +++ b/src/components/Gallery/ImagePlaceholder.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import clsx from 'clsx'; +import { useTranslationContext } from '../../context/TranslationContext'; +import { IconImages1Alt } from '../Icons'; + +export type ImagePlaceholderProps = { + className?: string; +}; + +export const ImagePlaceholder = ({ className }: ImagePlaceholderProps) => { + const { t } = useTranslationContext('ImagePlaceholder'); + return ( +
+ +
+ ); +}; diff --git a/src/components/Gallery/index.tsx b/src/components/Gallery/index.tsx index 6435074db3..fd4343b977 100644 --- a/src/components/Gallery/index.tsx +++ b/src/components/Gallery/index.tsx @@ -2,3 +2,4 @@ export * from './BaseImage'; export * from './Gallery'; export * from './GalleryContext'; export * from './GalleryUI'; +export * from './ImagePlaceholder'; diff --git a/src/components/Gallery/styling/BaseImage.scss b/src/components/Gallery/styling/BaseImage.scss new file mode 100644 index 0000000000..b1b5edf2de --- /dev/null +++ b/src/components/Gallery/styling/BaseImage.scss @@ -0,0 +1,8 @@ +.str-chat { + .str-chat__base-image { + display: block; + max-width: 100%; + max-height: 100%; + object-fit: contain; + } +} diff --git a/src/components/Gallery/styling/ImagePlaceholder.scss b/src/components/Gallery/styling/ImagePlaceholder.scss new file mode 100644 index 0000000000..1c4813ad00 --- /dev/null +++ b/src/components/Gallery/styling/ImagePlaceholder.scss @@ -0,0 +1,18 @@ +@use '../../../styling/utils'; + +.str-chat { + .str-chat__image-placeholder { + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + @include utils.flex-col-center; + background-color: var(--background-core-overlay-light); + + svg { + fill: var(--accent-neutral); + width: min(var(--icon-size-lg, 32px), 50%); + height: min(var(--icon-size-lg, 32px), 50%); + } + } +} diff --git a/src/components/Gallery/styling/index.scss b/src/components/Gallery/styling/index.scss index 96de974cfd..564f4632ff 100644 --- a/src/components/Gallery/styling/index.scss +++ b/src/components/Gallery/styling/index.scss @@ -1 +1,3 @@ +@use 'BaseImage'; @use 'Gallery'; +@use 'ImagePlaceholder'; diff --git a/src/components/Message/styling/QuotedMessage.scss b/src/components/Message/styling/QuotedMessage.scss index 33f31ae975..0b3de11b02 100644 --- a/src/components/Message/styling/QuotedMessage.scss +++ b/src/components/Message/styling/QuotedMessage.scss @@ -7,7 +7,10 @@ .str-chat__quoted-message-preview { background-color: var(--chat-bg-attachment-incoming); } - .str-chat__quoted-message-preview--own { + } + + .str-chat__message--me { + .str-chat__quoted-message-preview { background-color: var(--chat-bg-attachment-outgoing); } } diff --git a/src/components/MessageInput/styling/AttachmentPreviewThumbnail.scss b/src/components/MessageInput/styling/AttachmentPreviewThumbnail.scss index cdd2ed043e..d1ab23f6ec 100644 --- a/src/components/MessageInput/styling/AttachmentPreviewThumbnail.scss +++ b/src/components/MessageInput/styling/AttachmentPreviewThumbnail.scss @@ -1,4 +1,5 @@ // todo: should we have img dimensions determined by semantic variables? +.str-chat__link-preview-card .str-chat__image-placeholder, .str-chat__attachment-preview__thumbnail { border-radius: var(--radius-md); overflow: hidden; diff --git a/src/components/MessageInput/styling/QuotedMessagePreview.scss b/src/components/MessageInput/styling/QuotedMessagePreview.scss index 1c9cad4c6b..1c39df10b8 100644 --- a/src/components/MessageInput/styling/QuotedMessagePreview.scss +++ b/src/components/MessageInput/styling/QuotedMessagePreview.scss @@ -66,6 +66,11 @@ .str-chat__quoted-message-preview__image { display: flex; position: relative; + flex-shrink: 0; + width: 40px; + height: 40px; + min-width: 40px; + overflow: hidden; .str-chat__attachment-preview__thumbnail__play-indicator { display: flex; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index c40033034e..cee025e626 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -16,6 +16,7 @@ import { type FileDragAndDropContentProps, type GalleryProps, type GiphyPreviewMessageProps, + type ImagePlaceholderProps, type LoadingErrorIndicatorProps, type LoadingIndicatorProps, type MessageBouncePromptProps, @@ -96,6 +97,8 @@ export type ComponentContextValue = { BaseImage?: React.ComponentType; /** Custom UI component to display the contents of callout dialog, accepts same props as: [DefaultCalloutDialog](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Dialog/base/Callout.tsx) */ CalloutDialog?: React.ComponentType; + /** Custom UI component shown instead of the image when it fails to load, defaults to and accepts same props as: [ImagePlaceholder](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/ImagePlaceholder.tsx) */ + ImagePlaceholder?: React.ComponentType; /** Custom UI component to display set of action buttons within `ChannelPreviewMessenger` component, accepts same props as: [ChannelPreviewActionButtons](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/ChannelPreviewActionButtons.tsx) */ ChannelPreviewActionButtons?: React.ComponentType; /** Custom UI component to display command chip, defaults to and accepts same props as: [CommandChip](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/CommandChip.tsx) */ diff --git a/src/i18n/de.json b/src/i18n/de.json index 0afc0154f5..c47319d582 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -69,6 +69,7 @@ "aria/File input": "Dateieingabe", "aria/File upload": "Datei hochladen", "aria/Flag Message": "Nachricht melden", + "aria/Image failed to load": "Bild konnte nicht geladen werden", "aria/Image input": "Bildeingabe", "aria/Load More Channels": "Mehr Kanäle laden", "aria/Mark Message Unread": "Als ungelesen markieren", diff --git a/src/i18n/en.json b/src/i18n/en.json index d575a26bdd..74ad11c5f3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -69,6 +69,7 @@ "aria/File input": "File input", "aria/File upload": "File upload", "aria/Flag Message": "Flag Message", + "aria/Image failed to load": "Image failed to load", "aria/Image input": "Image input", "aria/Load More Channels": "Load More Channels", "aria/Mark Message Unread": "Mark Message Unread", diff --git a/src/i18n/es.json b/src/i18n/es.json index 6a930a1002..350bac084b 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -76,6 +76,7 @@ "aria/File input": "Entrada de archivo", "aria/File upload": "Carga de archivo", "aria/Flag Message": "Marcar mensaje", + "aria/Image failed to load": "Error al cargar la imagen", "aria/Image input": "Entrada de imagen", "aria/Load More Channels": "Cargar más canales", "aria/Mark Message Unread": "Marcar como no leído", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index de078634ed..49daa4883d 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -76,6 +76,7 @@ "aria/File input": "Entrée de fichier", "aria/File upload": "Téléchargement de fichier", "aria/Flag Message": "Signaler le message", + "aria/Image failed to load": "Échec du chargement de l'image", "aria/Image input": "Entrée d'image", "aria/Load More Channels": "Charger plus de canaux", "aria/Mark Message Unread": "Marquer comme non lu", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index f9b673e087..444c5fa108 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -69,6 +69,7 @@ "aria/File input": "फ़ाइल इनपुट", "aria/File upload": "फ़ाइल अपलोड", "aria/Flag Message": "संदेश फ्लैग करें", + "aria/Image failed to load": "छवि लोड होने में विफल", "aria/Image input": "छवि इनपुट", "aria/Load More Channels": "और चैनल लोड करें", "aria/Mark Message Unread": "अपठित चिह्नित करें", diff --git a/src/i18n/it.json b/src/i18n/it.json index 795b7d8c2c..dcda6f5fd9 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -76,6 +76,7 @@ "aria/File input": "Input di file", "aria/File upload": "Caricamento di file", "aria/Flag Message": "Segnala messaggio", + "aria/Image failed to load": "Caricamento immagine non riuscito", "aria/Image input": "Input di immagine", "aria/Load More Channels": "Carica altri canali", "aria/Mark Message Unread": "Contrassegna come non letto", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index cfbdac966b..4294b094db 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -68,6 +68,7 @@ "aria/File input": "ファイル入力", "aria/File upload": "ファイルアップロード", "aria/Flag Message": "メッセージをフラグ", + "aria/Image failed to load": "画像の読み込みに失敗しました", "aria/Image input": "画像入力", "aria/Load More Channels": "さらにチャンネルを読み込む", "aria/Mark Message Unread": "未読としてマーク", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 1a2df98069..9c8c56534d 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -68,6 +68,7 @@ "aria/File input": "파일 입력", "aria/File upload": "파일 업로드", "aria/Flag Message": "메시지 신고", + "aria/Image failed to load": "이미지를 불러오지 못했습니다", "aria/Image input": "이미지 입력", "aria/Load More Channels": "더 많은 채널 불러오기", "aria/Mark Message Unread": "읽지 않음으로 표시", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 7a9dbb12ea..d9e91f31f4 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -69,6 +69,7 @@ "aria/File input": "Bestandsinvoer", "aria/File upload": "Bestand uploaden", "aria/Flag Message": "Bericht markeren", + "aria/Image failed to load": "Afbeelding laden mislukt", "aria/Image input": "Afbeelding invoeren", "aria/Load More Channels": "Meer kanalen laden", "aria/Mark Message Unread": "Markeren als ongelezen", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 516af19f2f..0b96fdedd3 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -76,6 +76,7 @@ "aria/File input": "Entrada de arquivo", "aria/File upload": "Carregar arquivo", "aria/Flag Message": "Reportar mensagem", + "aria/Image failed to load": "Falha ao carregar a imagem", "aria/Image input": "Entrada de imagem", "aria/Load More Channels": "Carregar mais canais", "aria/Mark Message Unread": "Marcar como não lida", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index c94cf845c3..b7004ab2f7 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -84,6 +84,7 @@ "aria/File input": "Ввод файла", "aria/File upload": "Загрузка файла", "aria/Flag Message": "Пожаловаться на сообщение", + "aria/Image failed to load": "Не удалось загрузить изображение", "aria/Image input": "Ввод изображения", "aria/Load More Channels": "Загрузить больше каналов", "aria/Mark Message Unread": "Отметить как непрочитанное", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 8a68669ba8..2ee82a5d1b 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -69,6 +69,7 @@ "aria/File input": "Dosya girişi", "aria/File upload": "Dosya yükleme", "aria/Flag Message": "Mesajı bayrakla", + "aria/Image failed to load": "Görsel yüklenemedi", "aria/Image input": "Resim girişi", "aria/Load More Channels": "Daha Fazla Kanal Yükle", "aria/Mark Message Unread": "Okunmamış olarak işaretle",