From ad61c92993c8a778e1a83480ee43300a5837f037 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Fri, 6 Mar 2026 14:01:48 +0100 Subject: [PATCH 1/2] Initial commit --- .../ChannelPreviewMessenger.tsx | 91 +------ .../styling/ChannelPreview.scss | 60 +---- .../SummarizedMessagePreview.tsx | 87 +++++++ .../SummarizedMessagePreview/hooks/index.ts | 1 + .../hooks/useLatestMessagePreview.ts | 232 ++++++++++++++++++ .../SummarizedMessagePreview/index.ts | 2 + .../styling/SummarizedMessagePreview.scss | 57 +++++ .../styling/index.scss | 1 + .../Threads/ThreadList/ThreadListItemUI.tsx | 53 +--- ...eadListItem.scss => ThreadListItemUI.scss} | 59 +---- .../Threads/ThreadList/styling/index.scss | 2 +- src/i18n/de.json | 8 + src/i18n/en.json | 10 +- src/i18n/es.json | 12 + src/i18n/fr.json | 12 + src/i18n/hi.json | 8 + src/i18n/it.json | 12 + src/i18n/ja.json | 4 + src/i18n/ko.json | 4 + src/i18n/nl.json | 8 + src/i18n/pt.json | 12 + src/i18n/ru.json | 18 +- src/i18n/tr.json | 8 + src/styling/index.scss | 1 + 24 files changed, 520 insertions(+), 242 deletions(-) create mode 100644 src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx create mode 100644 src/components/SummarizedMessagePreview/hooks/index.ts create mode 100644 src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts create mode 100644 src/components/SummarizedMessagePreview/index.ts create mode 100644 src/components/SummarizedMessagePreview/styling/SummarizedMessagePreview.scss create mode 100644 src/components/SummarizedMessagePreview/styling/index.scss rename src/components/Threads/ThreadList/styling/{ThreadListItem.scss => ThreadListItemUI.scss} (72%) diff --git a/src/components/ChannelPreview/ChannelPreviewMessenger.tsx b/src/components/ChannelPreview/ChannelPreviewMessenger.tsx index 1a9ecaf14..bc50e80fa 100644 --- a/src/components/ChannelPreview/ChannelPreviewMessenger.tsx +++ b/src/components/ChannelPreview/ChannelPreviewMessenger.tsx @@ -3,49 +3,13 @@ import clsx from 'clsx'; import { ChannelPreviewActionButtons as DefaultChannelPreviewActionButtons } from './ChannelPreviewActionButtons'; import { ChannelPreviewTimestamp } from './ChannelPreviewTimestamp'; -import { - type ChannelPreviewDeliveryStatus, - type ChannelPreviewMessageType, - useLatestMessagePreview, -} from './hooks/useLatestMessagePreview'; import { ChannelAvatar as DefaultChannelAvatar } from '../Avatar'; import { Badge } from '../Badge'; -import { - IconCamera1, - IconChainLink, - IconCheckmark1Small, - IconCircleBanSign, - IconClock, - IconDoubleCheckmark1Small, - IconExclamationCircle1, - IconFileBend, - IconMapPin, - IconMicrophone, - IconMute, - IconVideo, -} from '../Icons'; +import { IconMute } from '../Icons'; import { useComponentContext } from '../../context'; import type { ChannelPreviewUIComponentProps } from './ChannelPreview'; - -const deliveryStatusIconMap: Record = { - delivered: IconDoubleCheckmark1Small, - read: IconDoubleCheckmark1Small, - sending: IconClock, - sent: IconCheckmark1Small, -}; - -const contentTypeIconMap: Partial< - Record -> = { - deleted: IconCircleBanSign, - file: IconFileBend, - image: IconCamera1, - link: IconChainLink, - location: IconMapPin, - video: IconVideo, - voice: IconMicrophone, -}; +import { SummarizedMessagePreview } from '../SummarizedMessagePreview'; const UnMemoizedChannelPreviewMessenger = (props: ChannelPreviewUIComponentProps) => { const { @@ -70,17 +34,6 @@ const UnMemoizedChannelPreviewMessenger = (props: ChannelPreviewUIComponentProps const channelPreviewButton = useRef(null); - const { deliveryStatus, senderName, text, type } = useLatestMessagePreview({ - latestMessage: lastMessage, - messageDeliveryStatus, - participantCount: channel.data?.member_count, - }); - - const DeliveryStatusIcon = deliveryStatus - ? deliveryStatusIconMap[deliveryStatus] - : undefined; - const ContentTypeIcon = contentTypeIconMap[type]; - const avatarName = displayTitle || channel.state.messages[channel.state.messages.length - 1]?.user?.id; @@ -133,41 +86,11 @@ const UnMemoizedChannelPreviewMessenger = (props: ChannelPreviewUIComponentProps )} -
- {type === 'error' ? ( - <> - - {text} - - ) : ( - <> - {DeliveryStatusIcon && ( - - - - )} - {senderName && ( - - {senderName}: - - )} - {ContentTypeIcon && } - {text} - - )} -
+ diff --git a/src/components/ChannelPreview/styling/ChannelPreview.scss b/src/components/ChannelPreview/styling/ChannelPreview.scss index 6bc9c0620..d8d23f1e3 100644 --- a/src/components/ChannelPreview/styling/ChannelPreview.scss +++ b/src/components/ChannelPreview/styling/ChannelPreview.scss @@ -45,10 +45,10 @@ cursor: pointer; text-align: start; font-family: var(--typography-font-family-sans); - background: var(--background-elevation-elevation-1); border-radius: var(--radius-lg); width: 100%; + background: var(--background-elevation-elevation-1); &:not(:disabled):hover { background: var(--background-core-hover); } @@ -118,62 +118,4 @@ } } } - - .str-chat__channel-preview-data__latest-message { - display: flex; - align-items: center; - gap: var(--spacing-xxs); - - flex-grow: 1; - flex-shrink: 1; - min-width: 0; - - color: var(--text-secondary); - font-size: var(--typography-font-size-sm); - font-style: normal; - font-weight: var(--typography-font-weight-regular); - line-height: var(--typography-line-height-normal); - - .str-chat__icon { - flex-shrink: 0; - width: var(--icon-size-sm); - height: var(--icon-size-sm); - } - - &--error { - color: var(--text-error); - } - - &--deleted { - color: var(--text-tertiary); - } - - .channel-preview-data__latest-message-text { - flex: 1; - min-width: 0; - - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .str-chat__channel-preview-data__latest-message-delivery-status { - display: flex; - flex-shrink: 0; - color: var(--text-tertiary); - - &--read { - color: var(--accent-primary); - } - } - - .str-chat__channel-preview-data__latest-message-sender { - color: var(--text-tertiary); - font-weight: var(--typography-font-weight-semi-bold); - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } } diff --git a/src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx b/src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx new file mode 100644 index 000000000..b09f6cf4e --- /dev/null +++ b/src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx @@ -0,0 +1,87 @@ +import clsx from 'clsx'; +import { + type ChannelPreviewDeliveryStatus, + type ChannelPreviewMessageType, + useLatestMessagePreview, + type UseLatestMessagePreviewParams, +} from './hooks/useLatestMessagePreview'; +import { + IconCamera1, + IconChainLink, + IconCheckmark1Small, + IconCircleBanSign, + IconClock, + IconDoubleCheckmark1Small, + IconExclamationCircle1, + IconFileBend, + IconMapPin, + IconMicrophone, + IconVideo, +} from '../Icons'; + +const deliveryStatusIconMap: Record = { + delivered: IconDoubleCheckmark1Small, + read: IconDoubleCheckmark1Small, + sending: IconClock, + sent: IconCheckmark1Small, +}; + +const contentTypeIconMap: Partial< + Record +> = { + deleted: IconCircleBanSign, + error: IconExclamationCircle1, + file: IconFileBend, + image: IconCamera1, + link: IconChainLink, + location: IconMapPin, + video: IconVideo, + voice: IconMicrophone, +}; + +export const SummarizedMessagePreview = ({ + latestMessage, + messageDeliveryStatus, + participantCount, +}: UseLatestMessagePreviewParams) => { + const { deliveryStatus, senderName, text, type } = useLatestMessagePreview({ + latestMessage, + messageDeliveryStatus, + participantCount, + }); + + const DeliveryStatusIcon = deliveryStatus + ? deliveryStatusIconMap[deliveryStatus] + : undefined; + const ContentTypeIcon = contentTypeIconMap[type]; + + return ( +
+ {type !== 'error' && ( + <> + {DeliveryStatusIcon && ( + + + + )} + {senderName && ( + + {senderName}: + + )} + + )} + {ContentTypeIcon && } + {text} +
+ ); +}; diff --git a/src/components/SummarizedMessagePreview/hooks/index.ts b/src/components/SummarizedMessagePreview/hooks/index.ts new file mode 100644 index 000000000..d62af71e0 --- /dev/null +++ b/src/components/SummarizedMessagePreview/hooks/index.ts @@ -0,0 +1 @@ +export * from './useLatestMessagePreview'; diff --git a/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts b/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts new file mode 100644 index 000000000..7478aff42 --- /dev/null +++ b/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts @@ -0,0 +1,232 @@ +import { useMemo } from 'react'; +import type { Attachment, LocalMessage } from 'stream-chat'; + +import { + getTranslatedMessageText, + type TranslationContextValue, + useChatContext, + useTranslationContext, +} from '../../../context'; + +import type { MessageDeliveryStatus } from '../../ChannelPreview'; + +export type ChannelPreviewMessageType = + | 'text' + | 'deleted' + | 'error' + | 'empty' + | 'image' + | 'video' + | 'voice' + | 'file' + | 'link' + | 'location' + | 'poll'; + +/** + * Delivery status of the last own message. + * Determines which delivery status icon to render in the preview. + * + * - `sending` — Clock icon + * - `sent` — Single checkmark icon + * - `delivered` — Double checkmark icon + * - `read` — Double checkmark icon (with distinct color) + */ +export type ChannelPreviewDeliveryStatus = 'sending' | 'sent' | 'delivered' | 'read'; + +export type LatestMessagePreviewData = { + /** + * The type of content being previewed. + * Use this to render the appropriate content-type icon. + */ + type: ChannelPreviewMessageType; + /** + * The preview text to display. + */ + text: string; + /** + * Delivery status of own message. + * Only present for own messages that are not in error state. + * Use this to render the delivery status icon (clock, checkmark, double checkmark). + */ + deliveryStatus?: ChannelPreviewDeliveryStatus; + /** + * Sender name prefix. + * - `"You"` (translated) for own messages + * - Sender's display name for incoming messages in group channels (>2 members) + * - `undefined` for incoming messages in direct conversations + */ + senderName?: string; +}; + +function getAttachmentContentType(attachment: Attachment): ChannelPreviewMessageType { + if (!attachment) return 'text'; + + // TODO: add audio (non-voice) content type when supported by the design + if (attachment.type === 'image' || attachment.type === 'giphy') return 'image'; + if (attachment.type === 'video') return 'video'; + if (attachment.type === 'voiceRecording') return 'voice'; + if (attachment.type === 'file') return 'file'; + if (attachment.og_scrape_url || attachment.title_link) return 'link'; + + return 'file'; +} + +function getAttachmentFallbackText( + type: ChannelPreviewMessageType, + count: number, + t: TranslationContextValue['t'], +): string { + switch (type) { + case 'image': + return t('imageCount', { count }); + case 'video': + return t('videoCount', { count }); + case 'voice': + return t('voiceMessageCount', { count }); + case 'link': + return t('linkCount', { count }); + case 'file': + default: + return t('fileCount', { count }); + } +} + +export type UseLatestMessagePreviewParams = { + /** The channel to generate preview for */ + participantCount?: number; + /** The latest message in the channel */ + latestMessage?: LocalMessage; + /** + * Delivery status from the `useMessageDeliveryStatus` hook. + * When provided, used to determine the delivery status icon. + * When omitted, delivery status icons are not shown. + */ + messageDeliveryStatus?: MessageDeliveryStatus; +}; + +export const useLatestMessagePreview = ({ + latestMessage, + messageDeliveryStatus, + participantCount = Infinity, +}: UseLatestMessagePreviewParams): LatestMessagePreviewData => { + const { client } = useChatContext('useLatestMessagePreview'); + const { t, userLanguage } = useTranslationContext('useLatestMessagePreview'); + + return useMemo(() => { + if (!latestMessage) { + return { text: t('Nothing yet...'), type: 'empty' as const }; + } + + if (latestMessage.status === 'failed' || latestMessage.type === 'error') { + return { text: t('Message failed to send'), type: 'error' as const }; + } + + const isOwnMessage = latestMessage.user?.id === client.user?.id; + + let deliveryStatus: ChannelPreviewDeliveryStatus | undefined; + if (isOwnMessage) { + deliveryStatus = messageDeliveryStatus ?? (latestMessage.status as 'sending'); + } + + let senderName: string | undefined; + if (isOwnMessage) { + senderName = t('You'); + } else if (!isOwnMessage && participantCount > 2) { + senderName = latestMessage.user?.name || latestMessage.user?.id; + } + + if (latestMessage.deleted_at) { + return { + deliveryStatus, + senderName, + text: t('Message deleted'), + type: 'deleted' as const, + }; + } + + if (latestMessage.poll) { + return { + deliveryStatus, + senderName, + text: t('Poll'), + type: 'poll' as const, + }; + } + + const textContent = + getTranslatedMessageText({ language: userLanguage, message: latestMessage }) || + latestMessage.text; + + if (latestMessage.shared_location) { + return { + deliveryStatus, + senderName, + text: textContent || t('Location'), + type: 'location' as const, + }; + } + + if (latestMessage.attachments && latestMessage.attachments.length) { + const attachments = latestMessage.attachments; + + let contentType: ChannelPreviewMessageType; + + const [firstAttachment] = attachments; + const firstAttachmentType = getAttachmentContentType(firstAttachment); + + if ( + attachments.every( + (attachment) => getAttachmentContentType(attachment) === firstAttachmentType, + ) + ) { + contentType = firstAttachmentType; + } else { + contentType = 'file'; + } + + let text = + // prioritize message text content if available + textContent || + // then fallback text of the single attachment if only one attachment is present and it's not a voice recording (fallback text is generic for voice recordings, so not useful in the preview) + (attachments.length === 1 && contentType !== 'voice' + ? firstAttachment.fallback || firstAttachment.title + : '') || + // then generic fallback text based on attachment type and count + getAttachmentFallbackText(contentType, attachments.length, t); + + // attach duration for audio/video attachments if available + if (attachments.length === 1 && typeof firstAttachment.duration === 'number') { + const minutes = Math.floor(firstAttachment.duration / 60); + const seconds = Math.ceil(firstAttachment.duration) % 60; + const durationString = `${minutes}:${seconds.toString().padStart(2, '0')}`; + text += ` (${durationString})`; + } + + return { + deliveryStatus, + senderName, + text, + type: contentType, + }; + } + + if (textContent) { + return { + deliveryStatus, + senderName, + text: textContent, + type: 'text' as const, + }; + } + + return { text: t('Empty message...'), type: 'empty' as const }; + }, [ + client.user?.id, + latestMessage, + messageDeliveryStatus, + participantCount, + t, + userLanguage, + ]); +}; diff --git a/src/components/SummarizedMessagePreview/index.ts b/src/components/SummarizedMessagePreview/index.ts new file mode 100644 index 000000000..3e9a0c9f9 --- /dev/null +++ b/src/components/SummarizedMessagePreview/index.ts @@ -0,0 +1,2 @@ +export * from './SummarizedMessagePreview'; +export * from './hooks'; diff --git a/src/components/SummarizedMessagePreview/styling/SummarizedMessagePreview.scss b/src/components/SummarizedMessagePreview/styling/SummarizedMessagePreview.scss new file mode 100644 index 000000000..ddd644f83 --- /dev/null +++ b/src/components/SummarizedMessagePreview/styling/SummarizedMessagePreview.scss @@ -0,0 +1,57 @@ +.str-chat__summarized-message-preview { + display: flex; + align-items: center; + gap: var(--spacing-xxs); + + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + + color: var(--text-secondary); + font-size: var(--typography-font-size-sm); + font-style: normal; + font-weight: var(--typography-font-weight-regular); + line-height: var(--typography-line-height-normal); + + .str-chat__icon { + flex-shrink: 0; + width: var(--icon-size-sm); + height: var(--icon-size-sm); + } + + &--error { + color: var(--text-error); + } + + &--deleted { + color: var(--text-tertiary); + } + + .str-chat__summarized-message-preview__text { + flex: 1; + min-width: 0; + + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .str-chat__summarized-message-preview__delivery-status { + display: flex; + flex-shrink: 0; + color: var(--text-tertiary); + + &--read { + color: var(--accent-primary); + } + } + + .str-chat__summarized-message-preview__sender { + color: var(--text-tertiary); + font-weight: var(--typography-font-weight-semi-bold); + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/src/components/SummarizedMessagePreview/styling/index.scss b/src/components/SummarizedMessagePreview/styling/index.scss new file mode 100644 index 000000000..6d0ca3c19 --- /dev/null +++ b/src/components/SummarizedMessagePreview/styling/index.scss @@ -0,0 +1 @@ +@forward './SummarizedMessagePreview'; diff --git a/src/components/Threads/ThreadList/ThreadListItemUI.tsx b/src/components/Threads/ThreadList/ThreadListItemUI.tsx index 72047ee40..75473b7f9 100644 --- a/src/components/Threads/ThreadList/ThreadListItemUI.tsx +++ b/src/components/Threads/ThreadList/ThreadListItemUI.tsx @@ -11,37 +11,10 @@ import { useThreadsViewContext } from '../../ChatView'; import { useThreadListItemContext } from './ThreadListItem'; import { useStateStore } from '../../../store'; import { Badge } from '../../Badge'; -import { - type ChannelPreviewMessageType, - useLatestMessagePreview, -} from '../../ChannelPreview/hooks/useLatestMessagePreview'; -import { - IconCamera1, - IconChainLink, - IconCircleBanSign, - IconExclamationCircle1, - IconFileBend, - IconMapPin, - IconMicrophone, - IconVideo, -} from '../../Icons'; -import clsx from 'clsx'; +import { SummarizedMessagePreview } from '../../SummarizedMessagePreview'; export type ThreadListItemUIProps = ComponentPropsWithoutRef<'button'>; -const contentTypeIconMap: Partial< - Record -> = { - deleted: IconCircleBanSign, - error: IconExclamationCircle1, - file: IconFileBend, - image: IconCamera1, - link: IconChainLink, - location: IconMapPin, - video: IconVideo, - voice: IconMicrophone, -}; - export const ThreadListItemUI = (props: ThreadListItemUIProps) => { const { client } = useChatContext(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -71,13 +44,6 @@ export const ThreadListItemUI = (props: ThreadListItemUIProps) => { replyCount, } = useStateStore(thread.state, selector); - const { senderName, text, type } = useLatestMessagePreview({ - latestMessage: parentMessage, - participantCount: participants?.length, - }); - - const ContentTypeIcon = contentTypeIconMap[type]; - const { displayTitle: channelDisplayTitle } = useChannelPreviewInfo({ channel }); const { t } = useTranslationContext('ThreadListItemUI'); @@ -117,19 +83,10 @@ export const ThreadListItemUI = (props: ThreadListItemUIProps) => { {channelDisplayTitle} -
- {type !== 'error' && !!senderName && ( - - {senderName}: - - )} - {ContentTypeIcon && } - {text} -
+
diff --git a/src/components/Threads/ThreadList/styling/ThreadListItem.scss b/src/components/Threads/ThreadList/styling/ThreadListItemUI.scss similarity index 72% rename from src/components/Threads/ThreadList/styling/ThreadListItem.scss rename to src/components/Threads/ThreadList/styling/ThreadListItemUI.scss index aab4de1ed..dc6b1f7e5 100644 --- a/src/components/Threads/ThreadList/styling/ThreadListItem.scss +++ b/src/components/Threads/ThreadList/styling/ThreadListItemUI.scss @@ -32,6 +32,15 @@ .str-chat__avatar { flex-shrink: 0; } + + .str-chat__summarized-message-preview { + .str-chat__summarized-message-preview__sender { + color: var(--text-secondary); + } + .str-chat__summarized-message-preview__text { + color: var(--text-primary); + } + } } .str-chat__thread-list-item__content { @@ -97,52 +106,6 @@ line-height: var(--typography-line-height-normal); color: var(--text-tertiary); white-space: nowrap; -} - -.str-chat__thread-list-item__message-preview { - display: flex; - align-items: center; - gap: var(--spacing-xxs); - - flex-grow: 1; - flex-shrink: 1; - min-width: 0; - - color: var(--text-secondary); - font-size: var(--typography-font-size-sm); - font-style: normal; - font-weight: var(--typography-font-weight-regular); - line-height: var(--typography-line-height-normal); - - .str-chat__icon { - flex-shrink: 0; - width: var(--icon-size-sm); - height: var(--icon-size-sm); - } - - &--error { - color: var(--text-error); - } - - &--deleted { - color: var(--text-tertiary); - } - - .str-chat__thread-list-item__message-preview-text { - flex: 1; - min-width: 0; - - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .str-chat__thread-list-item__message-preview-sender { - color: var(--text-tertiary); - font-weight: var(--typography-font-weight-semi-bold); - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + overflow: hidden; + text-overflow: ellipsis; } diff --git a/src/components/Threads/ThreadList/styling/index.scss b/src/components/Threads/ThreadList/styling/index.scss index ab312588f..17e7b4008 100644 --- a/src/components/Threads/ThreadList/styling/index.scss +++ b/src/components/Threads/ThreadList/styling/index.scss @@ -1,2 +1,2 @@ @use 'ThreadList'; -@use 'ThreadListItem'; +@use 'ThreadListItemUI'; diff --git a/src/i18n/de.json b/src/i18n/de.json index 34ecc4f9b..498a4aba1 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -172,6 +172,8 @@ "giphy-command-description": "Poste ein zufälliges Gif in den Kanal", "Hide who voted": "Verbergen, wer abgestimmt hat", "Image": "Bild", + "imageCount_one": "Bild", + "imageCount_other": "{{ count }} bilder", "Instant commands": "Instant commands", "language/af": "Afrikaans", "language/am": "Amharisch", @@ -233,6 +235,8 @@ "Let others add options": "Andere Optionen hinzufügen lassen", "Limit votes per person": "Stimmen pro Person begrenzen", "Link": "Link", + "linkCount_one": "Link", + "linkCount_other": "{{ count }} links", "live": "live", "Live for {{duration}}": "Live für {{duration}}", "Live location": "Live-Standort", @@ -393,6 +397,8 @@ "Upload type: \"{{ type }}\" is not allowed": "Upload-Typ: \"{{ type }}\" ist nicht erlaubt", "User uploaded content": "Vom Benutzer hochgeladener Inhalt", "Video": "Video", + "videoCount_one": "Video", + "videoCount_other": "{{ count }} videos", "View": "Ansehen", "View {{count}} comments_one": "{{count}} Kommentar anzeigen", "View {{count}} comments_other": "{{count}} Kommentare anzeigen", @@ -401,6 +407,8 @@ "View translation": "Übersetzung anzeigen", "Voice message": "Sprachnachricht", "Voice message {{ duration }}": "Sprachnachricht {{ duration }}", + "voiceMessageCount_one": "Sprachnachricht", + "voiceMessageCount_other": "{{ count }} sprachnachrichten", "Vote ended": "Abstimmung beendet", "Wait until all attachments have uploaded": "Bitte warten, bis alle Anhänge hochgeladen wurden", "You": "Du", diff --git a/src/i18n/en.json b/src/i18n/en.json index 6dba06dc7..925dab652 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -164,7 +164,7 @@ "File": "File", "File is too large: {{ size }}, maximum upload size is {{ limit }}": "File is too large: {{ size }}, maximum upload size is {{ limit }}", "File too large": "File too large", - "fileCount_one": "1 file", + "fileCount_one": "File", "fileCount_other": "{{ count }} files", "Flag": "Flag", "Generating...": "Generating...", @@ -172,6 +172,8 @@ "giphy-command-description": "Post a random gif to the channel", "Hide who voted": "Hide who voted", "Image": "Image", + "imageCount_one": "Image", + "imageCount_other": "{{ count }} images", "Instant commands": "Instant commands", "language/af": "Afrikaans", "language/am": "Amharic", @@ -233,6 +235,8 @@ "Let others add options": "Let others add options", "Limit votes per person": "Limit votes per person", "Link": "Link", + "linkCount_one": "Link", + "linkCount_other": "{{ count }} links", "live": "live", "Live for {{duration}}": "Live for {{duration}}", "Live location": "Live location", @@ -393,6 +397,8 @@ "Upload type: \"{{ type }}\" is not allowed": "Upload type: \"{{ type }}\" is not allowed", "User uploaded content": "User uploaded content", "Video": "Video", + "videoCount_one": "Video", + "videoCount_other": "{{ count }} videos", "View": "View", "View {{count}} comments_one": "View {{count}} comment", "View {{count}} comments_other": "View {{count}} comments", @@ -401,6 +407,8 @@ "View translation": "View translation", "Voice message": "Voice message", "Voice message {{ duration }}": "Voice message {{ duration }}", + "voiceMessageCount_one": "Voice message", + "voiceMessageCount_other": "{{ count }} voice messages", "Vote ended": "Vote ended", "Wait until all attachments have uploaded": "Wait until all attachments have uploaded", "You": "You", diff --git a/src/i18n/es.json b/src/i18n/es.json index a3375dcc3..01d8c9a9f 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -180,6 +180,9 @@ "giphy-command-description": "Publicar un gif aleatorio en el canal", "Hide who voted": "Ocultar quién votó", "Image": "Imagen", + "imageCount_one": "Imagen", + "imageCount_many": "{{ count }} imágenes", + "imageCount_other": "{{ count }} imágenes", "Instant commands": "Comandos instantáneos", "language/af": "Afrikáans", "language/am": "Amárico", @@ -241,6 +244,9 @@ "Let others add options": "Permitir que otros añadan opciones", "Limit votes per person": "Limitar votos por persona", "Link": "Enlace", + "linkCount_one": "Enlace", + "linkCount_many": "{{ count }} enlaces", + "linkCount_other": "{{ count }} enlaces", "live": "En vivo", "Live for {{duration}}": "En vivo durante {{duration}}", "Live location": "Ubicación en vivo", @@ -405,6 +411,9 @@ "Upload type: \"{{ type }}\" is not allowed": "Tipo de carga: \"{{ type }}\" no está permitido", "User uploaded content": "Contenido subido por el usuario", "Video": "Vídeo", + "videoCount_one": "Video", + "videoCount_many": "{{ count }} videos", + "videoCount_other": "{{ count }} videos", "View": "Ver", "View {{count}} comments_one": "Ver {{count}} comentario", "View {{count}} comments_many": "Ver {{count}} comentarios", @@ -414,6 +423,9 @@ "View translation": "Ver traducción", "Voice message": "Mensaje de voz", "Voice message {{ duration }}": "Mensaje de voz {{ duration }}", + "voiceMessageCount_one": "Mensaje de voz", + "voiceMessageCount_many": "{{ count }} mensajes de voz", + "voiceMessageCount_other": "{{ count }} mensajes de voz", "Vote ended": "Votación finalizada", "Wait until all attachments have uploaded": "Espere hasta que se hayan cargado todos los archivos adjuntos", "You": "Tú", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index e3c37dacc..5011207e5 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -180,6 +180,9 @@ "giphy-command-description": "Poster un GIF aléatoire dans le canal", "Hide who voted": "Masquer qui a voté", "Image": "Image", + "imageCount_one": "Photo", + "imageCount_many": "{{ count }} photos", + "imageCount_other": "{{ count }} photos", "Instant commands": "Commandes instantanées", "language/af": "Afrikaans", "language/am": "Amharique", @@ -241,6 +244,9 @@ "Let others add options": "Permettre à d'autres d'ajouter des options", "Limit votes per person": "Limiter les votes par personne", "Link": "Lien", + "linkCount_one": "Lien", + "linkCount_many": "{{ count }} liens", + "linkCount_other": "{{ count }} liens", "live": "en direct", "Live for {{duration}}": "En direct pendant {{duration}}", "Live location": "Emplacement en direct", @@ -405,6 +411,9 @@ "Upload type: \"{{ type }}\" is not allowed": "Le type de fichier : \"{{ type }}\" n'est pas autorisé", "User uploaded content": "Contenu téléchargé par l'utilisateur", "Video": "Vidéo", + "videoCount_one": "Vidéo", + "videoCount_many": "{{ count }} vidéos", + "videoCount_other": "{{ count }} vidéos", "View": "Voir", "View {{count}} comments_one": "Voir {{count}} commentaire", "View {{count}} comments_many": "Voir {{count}} commentaires", @@ -414,6 +423,9 @@ "View translation": "Voir la traduction", "Voice message": "Message vocal", "Voice message {{ duration }}": "Message vocal {{ duration }}", + "voiceMessageCount_one": "Mémo vocal", + "voiceMessageCount_many": "{{ count }} mémos vocaux", + "voiceMessageCount_other": "{{ count }} mémos vocaux", "Vote ended": "Vote terminé", "Wait until all attachments have uploaded": "Attendez que toutes les pièces jointes soient téléchargées", "You": "Vous", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index ebc66cb80..58228da14 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -173,6 +173,8 @@ "giphy-command-description": "चैनल पर एक क्रॉफिल जीआइएफ पोस्ट करें", "Hide who voted": "किसने वोट दिया छिपाएं", "Image": "छवि", + "imageCount_one": "1 छवि", + "imageCount_other": "{{ count }} छवियाँ", "Instant commands": "तत्काल कमांड", "language/af": "अफ्रीकी", "language/am": "अम्हारिक", @@ -234,6 +236,8 @@ "Let others add options": "दूसरों को विकल्प जोड़ने दें", "Limit votes per person": "प्रति व्यक्ति वोट सीमित करें", "Link": "लिंक", + "linkCount_one": "1 लिंक", + "linkCount_other": "{{ count }} लिंक", "live": "लाइव", "Live for {{duration}}": "{{duration}} के लिए लाइव", "Live location": "लाइव स्थान", @@ -394,6 +398,8 @@ "Upload type: \"{{ type }}\" is not allowed": "अपलोड प्रकार: \"{{ type }}\" की अनुमति नहीं है", "User uploaded content": "उपयोगकर्ता अपलोड की गई सामग्री", "Video": "वीडियो", + "videoCount_one": "1 वीडियो", + "videoCount_other": "{{ count }} वीडियो", "View": "देखें", "View {{count}} comments_one": "देखें {{count}} टिप्पणी", "View {{count}} comments_other": "देखें {{count}} टिप्पणियाँ", @@ -402,6 +408,8 @@ "View translation": "अनुवाद देखें", "Voice message": "आवाज संदेश", "Voice message {{ duration }}": "वॉइस संदेश {{ duration }}", + "voiceMessageCount_one": "1 ध्वनि संदेश", + "voiceMessageCount_other": "{{ count }} ध्वनि संदेश", "Vote ended": "मतदान समाप्त", "Wait until all attachments have uploaded": "सभी अटैचमेंट अपलोड होने तक प्रतीक्षा करें", "You": "आप", diff --git a/src/i18n/it.json b/src/i18n/it.json index 316daa4cf..ca1e9ada5 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -180,6 +180,9 @@ "giphy-command-description": "Pubblica un gif casuale sul canale", "Hide who voted": "Nascondi chi ha votato", "Image": "Immagine", + "imageCount_one": "Immagine", + "imageCount_many": "{{ count }} immagini", + "imageCount_other": "{{ count }} immagini", "Instant commands": "Comandi istantanei", "language/af": "Afrikaans", "language/am": "Amarico", @@ -241,6 +244,9 @@ "Let others add options": "Lascia che altri aggiungano opzioni", "Limit votes per person": "Limita i voti per persona", "Link": "Collegamento", + "linkCount_one": "Link", + "linkCount_many": "{{ count }} link", + "linkCount_other": "{{ count }} link", "live": "live", "Live for {{duration}}": "Live per {{duration}}", "Live location": "Posizione live", @@ -405,6 +411,9 @@ "Upload type: \"{{ type }}\" is not allowed": "Tipo di caricamento: \"{{ type }}\" non è consentito", "User uploaded content": "Contenuto caricato dall'utente", "Video": "Video", + "videoCount_one": "Video", + "videoCount_many": "{{ count }} video", + "videoCount_other": "{{ count }} video", "View": "Visualizza", "View {{count}} comments_one": "Visualizza {{count}} commento", "View {{count}} comments_many": "Visualizza {{count}} commenti", @@ -414,6 +423,9 @@ "View translation": "Visualizza traduzione", "Voice message": "Messaggio vocale", "Voice message {{ duration }}": "Messaggio vocale {{ duration }}", + "voiceMessageCount_one": "Messaggio vocale", + "voiceMessageCount_many": "{{ count }} messaggi vocali", + "voiceMessageCount_other": "{{ count }} messaggi vocali", "Vote ended": "Voto terminato", "Wait until all attachments have uploaded": "Attendi il caricamento di tutti gli allegati", "You": "Tu", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index d1f97cef6..965e352f5 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -170,6 +170,7 @@ "giphy-command-description": "チャンネルにランダムなGIFを投稿する", "Hide who voted": "誰が投票したかを非表示にする", "Image": "画像", + "imageCount_other": "{{ count }}件の画像", "Instant commands": "インスタントコマンド", "language/af": "アフリカース語", "language/am": "アムハラ語", @@ -231,6 +232,7 @@ "Let others add options": "他の人が選択肢を追加できるようにする", "Limit votes per person": "1人あたりの投票数を制限する", "Link": "リンク", + "linkCount_other": "{{ count }}件のリンク", "live": "ライブ", "Live for {{duration}}": "{{duration}}間ライブ", "Live location": "ライブ位置情報", @@ -391,6 +393,7 @@ "Upload type: \"{{ type }}\" is not allowed": "アップロードタイプ:\"{{ type }}\"は許可されていません", "User uploaded content": "ユーザーがアップロードしたコンテンツ", "Video": "動画", + "videoCount_other": "{{ count }}件の動画", "View": "表示", "View {{count}} comments_one": "{{count}} コメントを表示", "View {{count}} comments_other": "{{count}} コメントを表示", @@ -399,6 +402,7 @@ "View translation": "翻訳を表示", "Voice message": "ボイスメッセージ", "Voice message {{ duration }}": "ボイスメッセージ {{ duration }}", + "voiceMessageCount_other": "{{ count }}件のボイスメッセージ", "Vote ended": "投票が終了しました", "Wait until all attachments have uploaded": "すべての添付ファイルがアップロードされるまでお待ちください", "You": "あなた", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 9294c4ba2..aad1e6788 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -170,6 +170,7 @@ "giphy-command-description": "채널에 무작위 GIF 게시", "Hide who voted": "누가 투표했는지 숨기기", "Image": "이미지", + "imageCount_other": "이미지 {{ count }}개", "Instant commands": "즉시 명령어", "language/af": "아프리칸스어", "language/am": "암하라어", @@ -231,6 +232,7 @@ "Let others add options": "다른 사람이 선택지를 추가할 수 있도록 허용", "Limit votes per person": "1인당 투표 수 제한", "Link": "링크", + "linkCount_other": "링크 {{ count }}개", "live": "라이브", "Live for {{duration}}": "{{duration}} 동안 라이브", "Live location": "라이브 위치", @@ -391,6 +393,7 @@ "Upload type: \"{{ type }}\" is not allowed": "업로드 유형: \"{{ type }}\"은(는) 허용되지 않습니다.", "User uploaded content": "사용자 업로드 콘텐츠", "Video": "동영상", + "videoCount_other": "동영상 {{ count }}개", "View": "보기", "View {{count}} comments_one": "{{count}}개의 댓글 보기", "View {{count}} comments_other": "{{count}}개의 댓글 보기", @@ -399,6 +402,7 @@ "View translation": "번역 보기", "Voice message": "음성 메시지", "Voice message {{ duration }}": "음성 메시지 {{ duration }}", + "voiceMessageCount_other": "음성 메시지 {{ count }}개", "Vote ended": "투표 종료", "Wait until all attachments have uploaded": "모든 첨부 파일이 업로드될 때까지 기다립니다.", "You": "당신", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index e9ad1ac54..41f55471e 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -172,6 +172,8 @@ "giphy-command-description": "Plaats een willekeurige gif in het kanaal", "Hide who voted": "Verberg wie heeft gestemd", "Image": "Afbeelding", + "imageCount_one": "Afbeelding", + "imageCount_other": "{{ count }} afbeeldingen", "Instant commands": "Snelle opdrachten", "language/af": "Afrikaans", "language/am": "Amhaars", @@ -233,6 +235,8 @@ "Let others add options": "Laat anderen opties toevoegen", "Limit votes per person": "Stemmen per persoon beperken", "Link": "Link", + "linkCount_one": "Link", + "linkCount_other": "{{ count }} links", "live": "live", "Live for {{duration}}": "Live voor {{duration}}", "Live location": "Live locatie", @@ -395,6 +399,8 @@ "Upload type: \"{{ type }}\" is not allowed": "Uploadtype: \"{{ type }}\" is niet toegestaan", "User uploaded content": "Gebruikersgeüploade inhoud", "Video": "Video", + "videoCount_one": "Video", + "videoCount_other": "{{ count }} video's", "View": "Bekijken", "View {{count}} comments_one": "Bekijk {{count}} opmerkingen", "View {{count}} comments_other": "Bekijk {{count}} opmerkingen", @@ -403,6 +409,8 @@ "View translation": "Vertaling bekijken", "Voice message": "Spraakbericht", "Voice message {{ duration }}": "Spraakbericht {{ duration }}", + "voiceMessageCount_one": "Spraakbericht", + "voiceMessageCount_other": "{{ count }} spraakberichten", "Vote ended": "Stemmen beëindigd", "Wait until all attachments have uploaded": "Wacht tot alle bijlagen zijn geüpload", "You": "Jij", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 6e3cb2e0e..da9fa8e85 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -180,6 +180,9 @@ "giphy-command-description": "Postar um gif aleatório no canal", "Hide who voted": "Ocultar quem votou", "Image": "Imagem", + "imageCount_one": "Imagem", + "imageCount_many": "{{ count }} imagens", + "imageCount_other": "{{ count }} imagens", "Instant commands": "Comandos instantâneos", "language/af": "Africâner", "language/am": "Amárico", @@ -241,6 +244,9 @@ "Let others add options": "Permitir que outros adicionem opções", "Limit votes per person": "Limitar votos por pessoa", "Link": "Link", + "linkCount_one": "Link", + "linkCount_many": "{{ count }} links", + "linkCount_other": "{{ count }} links", "live": "ao vivo", "Live for {{duration}}": "Ao vivo por {{duration}}", "Live location": "Localização ao vivo", @@ -405,6 +411,9 @@ "Upload type: \"{{ type }}\" is not allowed": "Tipo de upload: \"{{ type }}\" não é permitido", "User uploaded content": "Conteúdo enviado pelo usuário", "Video": "Vídeo", + "videoCount_one": "Vídeo", + "videoCount_many": "{{ count }} vídeos", + "videoCount_other": "{{ count }} vídeos", "View": "Ver", "View {{count}} comments_one": "Ver {{count}} comentário", "View {{count}} comments_many": "Ver {{count}} comentários", @@ -414,6 +423,9 @@ "View translation": "Ver tradução", "Voice message": "Mensagem de voz", "Voice message {{ duration }}": "Mensagem de voz {{ duration }}", + "voiceMessageCount_one": "Mensagem de voz", + "voiceMessageCount_many": "{{ count }} mensagens de voz", + "voiceMessageCount_other": "{{ count }} mensagens de voz", "Vote ended": "Votação encerrada", "Wait until all attachments have uploaded": "Espere até que todos os anexos tenham sido carregados", "You": "Você", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 4a4de33dc..fd963839f 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -191,6 +191,10 @@ "giphy-command-description": "Опубликовать случайную GIF-анимацию в канале", "Hide who voted": "Скрыть, кто голосовал", "Image": "Изображение", + "imageCount_one": "{{ count }} изображение", + "imageCount_few": "{{ count }} изображения", + "imageCount_many": "{{ count }} изображений", + "imageCount_other": "{{ count }} изображений", "Instant commands": "Мгновенные команды", "language/af": "Африкаанс", "language/am": "Амхарский", @@ -251,7 +255,11 @@ "language/zh-TW": "Китайский (традиционный)", "Let others add options": "Разрешить другим добавлять варианты", "Limit votes per person": "Ограничить голоса на человека", - "Link": "Ссылка", + "Link": "Линк", + "linkCount_one": "{{ count }} линк", + "linkCount_few": "{{ count }} линка", + "linkCount_many": "{{ count }} линков", + "linkCount_other": "{{ count }} линков", "live": "В прямом эфире", "Live for {{duration}}": "В прямом эфире {{duration}}", "Live location": "Местоположение в прямом эфире", @@ -420,6 +428,10 @@ "Upload type: \"{{ type }}\" is not allowed": "Тип загрузки: \"{{ type }}\" не разрешен", "User uploaded content": "Пользователь загрузил контент", "Video": "Видео", + "videoCount_one": "{{ count }} видео", + "videoCount_few": "{{ count }} видео", + "videoCount_many": "{{ count }} видео", + "videoCount_other": "{{ count }} видео", "View": "Просмотр", "View {{count}} comments_one": "Просмотреть {{count}} комментарий", "View {{count}} comments_few": "Просмотреть {{count}} комментариев", @@ -430,6 +442,10 @@ "View translation": "Показать перевод", "Voice message": "Голосовое сообщение", "Voice message {{ duration }}": "Голосовое сообщение {{ duration }}", + "voiceMessageCount_one": "{{ count }} голосовое сообщение", + "voiceMessageCount_few": "{{ count }} голосовых сообщения", + "voiceMessageCount_many": "{{ count }} голосовых сообщений", + "voiceMessageCount_other": "{{ count }} голосовых сообщений", "Vote ended": "Голосование завершено", "Wait until all attachments have uploaded": "Подождите, пока все вложения загрузятся", "You": "Вы", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 95907391a..aed0805ae 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -172,6 +172,8 @@ "giphy-command-description": "Rastgele bir gif'i kanala gönder", "Hide who voted": "Kimin oy verdiğini gizle", "Image": "Görsel", + "imageCount_one": "Görsel", + "imageCount_other": "{{ count }} görsel", "Instant commands": "Anlık komutlar", "language/af": "Afrikanca", "language/am": "Amharca", @@ -233,6 +235,8 @@ "Let others add options": "Başkalarının seçenek eklemesine izin ver", "Limit votes per person": "Kişi başına oy sınırı", "Link": "Bağlantı", + "linkCount_one": "Bağlantı", + "linkCount_other": "{{ count }} bağlantı", "live": "canlı", "Live for {{duration}}": "{{duration}} boyunca canlı", "Live location": "Canlı konum", @@ -393,6 +397,8 @@ "Upload type: \"{{ type }}\" is not allowed": "Yükleme türü: \"{{ type }}\" izin verilmez", "User uploaded content": "Kullanıcı tarafından yüklenen içerik", "Video": "Video", + "videoCount_one": "Video", + "videoCount_other": "{{ count }} video", "View": "Görüntüle", "View {{count}} comments_one": "{{count}} yorumu görüntüle", "View {{count}} comments_other": "{{count}} yorumu görüntüle", @@ -401,6 +407,8 @@ "View translation": "Çeviriyi görüntüle", "Voice message": "Sesli mesaj", "Voice message {{ duration }}": "Sesli mesaj {{ duration }}", + "voiceMessageCount_one": "Sesli mesaj", + "voiceMessageCount_other": "{{ count }} sesli mesaj", "Vote ended": "Oylama sona erdi", "Wait until all attachments have uploaded": "Tüm ekler yüklenene kadar bekleyin", "You": "Sen", diff --git a/src/styling/index.scss b/src/styling/index.scss index 0139e6474..70aaa8a98 100644 --- a/src/styling/index.scss +++ b/src/styling/index.scss @@ -47,6 +47,7 @@ @use '../components/Thread/styling' as Thread; @use '../components/Threads/ThreadList/styling' as ThreadList; @use '../components/VideoPlayer/styling' as VideoPlayer; +@use '../components/SummarizedMessagePreview/styling' as SummarizedMessagePreview; // Layers have to be kept the last @import 'modern-normalize' layer(css-reset); From aeb0798e482370c12f2ba20a509915ff29a8c478 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Fri, 6 Mar 2026 17:04:52 +0100 Subject: [PATCH 2/2] Remove unused file --- src/components/ChannelPreview/hooks/index.ts | 7 - .../hooks/useLatestMessagePreview.ts | 226 ------------------ 2 files changed, 233 deletions(-) delete mode 100644 src/components/ChannelPreview/hooks/useLatestMessagePreview.ts diff --git a/src/components/ChannelPreview/hooks/index.ts b/src/components/ChannelPreview/hooks/index.ts index 9056c76f0..16eec9ba2 100644 --- a/src/components/ChannelPreview/hooks/index.ts +++ b/src/components/ChannelPreview/hooks/index.ts @@ -1,9 +1,2 @@ export { useChannelPreviewInfo } from './useChannelPreviewInfo'; -export { useLatestMessagePreview } from './useLatestMessagePreview'; -export type { - ChannelPreviewDeliveryStatus, - ChannelPreviewMessageType, - LatestMessagePreviewData, - UseLatestMessagePreviewParams, -} from './useLatestMessagePreview'; export { MessageDeliveryStatus } from './useMessageDeliveryStatus'; diff --git a/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts b/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts deleted file mode 100644 index 090556932..000000000 --- a/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { useMemo } from 'react'; -import type { Attachment, LocalMessage } from 'stream-chat'; - -import { useChatContext } from '../../../context/ChatContext'; -import { useTranslationContext } from '../../../context/TranslationContext'; -import { getTranslatedMessageText } from '../../../context/MessageTranslationViewContext'; -import type { TranslationContextValue } from '../../../context/TranslationContext'; -import type { MessageDeliveryStatus } from './useMessageDeliveryStatus'; - -/** - * The type of content being previewed in the channel list item. - * Determines which icon to render alongside the preview text. - * - * - `text` — Regular text message, no content-type icon - * - `deleted` — Deleted message, renders ban/circle icon - * - `error` — Failed message, renders exclamation circle icon - * - `empty` — No messages yet, no icon - * - `image` — Image attachment, renders camera icon - * - `video` — Video attachment, renders video icon - * - `voice` — Voice message, renders microphone icon - * - `file` — File attachment, renders file icon - * - `link` — Link preview, renders chain link icon - * - `location` — Shared location, renders map pin icon - * - `poll` — Poll message, emoji prefix in text - */ -export type ChannelPreviewMessageType = - | 'text' - | 'deleted' - | 'error' - | 'empty' - | 'image' - | 'video' - | 'voice' - | 'file' - | 'link' - | 'location' - | 'poll'; - -/** - * Delivery status of the last own message. - * Determines which delivery status icon to render in the preview. - * - * - `sending` — Clock icon - * - `sent` — Single checkmark icon - * - `delivered` — Double checkmark icon - * - `read` — Double checkmark icon (with distinct color) - */ -export type ChannelPreviewDeliveryStatus = 'sending' | 'sent' | 'delivered' | 'read'; - -/** - * Structured data for rendering the channel preview message row. - * Produced by the `useLatestMessagePreview` hook. - */ -export type LatestMessagePreviewData = { - /** - * The type of content being previewed. - * Use this to render the appropriate content-type icon. - */ - type: ChannelPreviewMessageType; - /** - * The preview text to display. - */ - text: string; - /** - * Delivery status of own message. - * Only present for own messages that are not in error state. - * Use this to render the delivery status icon (clock, checkmark, double checkmark). - */ - deliveryStatus?: ChannelPreviewDeliveryStatus; - /** - * Sender name prefix. - * - `"You"` (translated) for own messages - * - Sender's display name for incoming messages in group channels (>2 members) - * - `undefined` for incoming messages in direct conversations - */ - senderName?: string; -}; - -function getAttachmentContentType(attachments: Attachment[]): ChannelPreviewMessageType { - const [first] = attachments; - if (!first) return 'text'; - - if (first.type === 'image' || first.type === 'giphy') return 'image'; - if (first.type === 'video') return 'video'; - if (first.type === 'voiceRecording') return 'voice'; - if (first.type === 'file') return 'file'; - if (first.og_scrape_url || first.title_link) return 'link'; - - return 'file'; -} - -function getAttachmentFallbackText( - type: ChannelPreviewMessageType, - t: TranslationContextValue['t'], -): string { - switch (type) { - case 'image': - return t('Image'); - case 'video': - return t('Video'); - case 'voice': - return t('Voice message'); - case 'link': - return t('Link'); - case 'file': - default: - return t('File'); - } -} - -export type UseLatestMessagePreviewParams = { - /** The channel to generate preview for */ - participantCount?: number; - /** The latest message in the channel */ - latestMessage?: LocalMessage; - /** - * Delivery status from the `useMessageDeliveryStatus` hook. - * When provided, used to determine the delivery status icon. - * When omitted, delivery status icons are not shown. - */ - messageDeliveryStatus?: MessageDeliveryStatus; -}; - -export const useLatestMessagePreview = ({ - latestMessage, - messageDeliveryStatus, - participantCount = Infinity, -}: UseLatestMessagePreviewParams): LatestMessagePreviewData => { - const { client } = useChatContext('useLatestMessagePreview'); - const { t, userLanguage } = useTranslationContext('useLatestMessagePreview'); - - return useMemo(() => { - if (!latestMessage) { - return { text: t('Nothing yet...'), type: 'empty' as const }; - } - - if (latestMessage.status === 'failed' || latestMessage.type === 'error') { - return { text: t('Message failed to send'), type: 'error' as const }; - } - - const isOwnMessage = latestMessage.user?.id === client.user?.id; - - let deliveryStatus: ChannelPreviewDeliveryStatus | undefined; - if (isOwnMessage) { - deliveryStatus = messageDeliveryStatus ?? (latestMessage.status as 'sending'); - } - - let senderName: string | undefined; - if (isOwnMessage) { - senderName = t('You'); - } else if (!isOwnMessage && participantCount > 2) { - senderName = latestMessage.user?.name || latestMessage.user?.id; - } - - if (latestMessage.deleted_at) { - return { - deliveryStatus, - senderName, - text: t('Message deleted'), - type: 'deleted' as const, - }; - } - - if (latestMessage.poll) { - return { - deliveryStatus, - senderName, - text: t('Poll'), - type: 'poll' as const, - }; - } - - const textContent = - getTranslatedMessageText({ language: userLanguage, message: latestMessage }) || - latestMessage.text; - - if (latestMessage.shared_location) { - return { - deliveryStatus, - senderName, - text: textContent || t('Location'), - type: 'location' as const, - }; - } - - if (latestMessage.attachments && latestMessage.attachments.length) { - const attachments = latestMessage.attachments; - - let contentType: ChannelPreviewMessageType; - let text: string; - - if (attachments.length === 1) { - contentType = getAttachmentContentType(attachments); - text = textContent || getAttachmentFallbackText(contentType, t); - } else { - contentType = 'file'; - text = textContent || t('fileCount', { count: attachments.length }); - } - - return { - deliveryStatus, - senderName, - text, - type: contentType, - }; - } - - if (textContent) { - return { - deliveryStatus, - senderName, - text: textContent, - type: 'text' as const, - }; - } - - return { text: t('Empty message...'), type: 'empty' as const }; - }, [ - client.user?.id, - latestMessage, - messageDeliveryStatus, - participantCount, - t, - userLanguage, - ]); -};