diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 38dd851735..05bc17c122 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -6756,6 +6756,10 @@ "messageformat": "Open the link in a browser", "description": "Alt text for the link preview item button" }, + "icu:ListItem__show-message": { + "messageformat": "Show message in conversation", + "description": "Alt text for the button in media gallery list view" + }, "icu:MediaGallery__tab__audio": { "messageformat": "Audio", "description": "Header of the links pane in the media gallery, showing audio" diff --git a/ts/components/conversation/media-gallery/AudioListItem.dom.stories.tsx b/ts/components/conversation/media-gallery/AudioListItem.dom.stories.tsx index e824838d75..23b1f7b0f6 100644 --- a/ts/components/conversation/media-gallery/AudioListItem.dom.stories.tsx +++ b/ts/components/conversation/media-gallery/AudioListItem.dom.stories.tsx @@ -29,6 +29,7 @@ export function Multiple(): JSX.Element { mediaItem={mediaItem} authorTitle="Alice" onClick={action('onClick')} + onShowMessage={action('onShowMessage')} /> ))} diff --git a/ts/components/conversation/media-gallery/AudioListItem.dom.tsx b/ts/components/conversation/media-gallery/AudioListItem.dom.tsx index 5d6b5db62c..748768c7c7 100644 --- a/ts/components/conversation/media-gallery/AudioListItem.dom.tsx +++ b/ts/components/conversation/media-gallery/AudioListItem.dom.tsx @@ -20,6 +20,7 @@ const MIN_PEAK_HEIGHT = 2; export type DataProps = Readonly<{ mediaItem: MediaItemType; onClick: (status: AttachmentStatusType['state']) => void; + onShowMessage: () => void; }>; // Provided by smart layer @@ -35,6 +36,7 @@ export function AudioListItem({ mediaItem, authorTitle, onClick, + onShowMessage, }: Props): JSX.Element { const { attachment } = mediaItem; @@ -102,6 +104,7 @@ export function AudioListItem({ subtitle={subtitle.join(' ยท ')} readyLabel={i18n('icu:startDownload')} onClick={onClick} + onShowMessage={onShowMessage} /> ); } diff --git a/ts/components/conversation/media-gallery/DocumentListItem.dom.stories.tsx b/ts/components/conversation/media-gallery/DocumentListItem.dom.stories.tsx index 04d28cf65c..71aa71d903 100644 --- a/ts/components/conversation/media-gallery/DocumentListItem.dom.stories.tsx +++ b/ts/components/conversation/media-gallery/DocumentListItem.dom.stories.tsx @@ -28,6 +28,7 @@ export function Multiple(): JSX.Element { key={mediaItem.attachment.fileName} mediaItem={mediaItem} onClick={action('onClick')} + onShowMessage={action('onShowMessage')} /> ))} diff --git a/ts/components/conversation/media-gallery/DocumentListItem.dom.tsx b/ts/components/conversation/media-gallery/DocumentListItem.dom.tsx index 33a1cac769..6d51d110a7 100644 --- a/ts/components/conversation/media-gallery/DocumentListItem.dom.tsx +++ b/ts/components/conversation/media-gallery/DocumentListItem.dom.tsx @@ -18,12 +18,14 @@ export type Props = { i18n: LocalizerType; mediaItem: MediaItemType; onClick: (status: AttachmentStatusType['state']) => void; + onShowMessage: () => void; }; export function DocumentListItem({ i18n, mediaItem, onClick, + onShowMessage, }: Props): JSX.Element { const { attachment } = mediaItem; @@ -57,6 +59,7 @@ export function DocumentListItem({ subtitle={subtitle} readyLabel={i18n('icu:startDownload')} onClick={onClick} + onShowMessage={onShowMessage} /> ); } diff --git a/ts/components/conversation/media-gallery/LinkPreviewItem.dom.stories.tsx b/ts/components/conversation/media-gallery/LinkPreviewItem.dom.stories.tsx index 88d4a6b18d..2b1058da46 100644 --- a/ts/components/conversation/media-gallery/LinkPreviewItem.dom.stories.tsx +++ b/ts/components/conversation/media-gallery/LinkPreviewItem.dom.stories.tsx @@ -29,6 +29,7 @@ export function Multiple(): JSX.Element { authorTitle="Alice" mediaItem={mediaItem} onClick={action('onClick')} + onShowMessage={action('onShowMessage')} /> ))} diff --git a/ts/components/conversation/media-gallery/LinkPreviewItem.dom.tsx b/ts/components/conversation/media-gallery/LinkPreviewItem.dom.tsx index dc472ab3b5..550b2d01c1 100644 --- a/ts/components/conversation/media-gallery/LinkPreviewItem.dom.tsx +++ b/ts/components/conversation/media-gallery/LinkPreviewItem.dom.tsx @@ -19,6 +19,7 @@ import { ListItem } from './ListItem.dom.js'; export type DataProps = Readonly<{ mediaItem: LinkPreviewMediaItemType; onClick: (status: AttachmentStatusType['state']) => void; + onShowMessage: () => void; }>; // Provided by smart layer @@ -35,6 +36,7 @@ export function LinkPreviewItem({ mediaItem, authorTitle, onClick, + onShowMessage, }: Props): JSX.Element { const { preview } = mediaItem; @@ -94,6 +96,7 @@ export function LinkPreviewItem({ subtitle={subtitle} readyLabel={i18n('icu:LinkPreviewItem__alt')} onClick={onClick} + onShowMessage={onShowMessage} /> ); } diff --git a/ts/components/conversation/media-gallery/ListItem.dom.tsx b/ts/components/conversation/media-gallery/ListItem.dom.tsx index 7e62125862..9826de28f9 100644 --- a/ts/components/conversation/media-gallery/ListItem.dom.tsx +++ b/ts/components/conversation/media-gallery/ListItem.dom.tsx @@ -10,6 +10,7 @@ import type { AttachmentForUIType } from '../../../types/Attachment.std.js'; import type { LocalizerType } from '../../../types/Util.std.js'; import { SpinnerV2 } from '../../SpinnerV2.dom.js'; import { tw } from '../../../axo/tw.dom.js'; +import { AriaClickable } from '../../../axo/AriaClickable.dom.js'; import { AxoSymbol } from '../../../axo/AxoSymbol.dom.js'; import { useAttachmentStatus, @@ -24,6 +25,7 @@ export type Props = { subtitle: React.ReactNode; readyLabel: string; onClick: (status: AttachmentStatusType['state']) => void; + onShowMessage: () => void; }; export function ListItem({ @@ -34,6 +36,7 @@ export function ListItem({ subtitle, readyLabel, onClick, + onShowMessage, }: Props): JSX.Element { const { message } = mediaItem; let attachment: AttachmentForUIType | undefined; @@ -53,11 +56,21 @@ export function ListItem({ const handleClick = useCallback( (ev: React.MouseEvent) => { ev.preventDefault(); - onClick?.(status?.state || 'ReadyToShow'); + ev.stopPropagation(); + onClick(status?.state || 'ReadyToShow'); }, [onClick, status?.state] ); + const handleDateClick = useCallback( + (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + onShowMessage(); + }, + [onShowMessage] + ); + if (status == null || status.state === 'ReadyToShow') { label = readyLabel; } else if (status.state === 'NeedsDownload') { @@ -108,14 +121,11 @@ export function ListItem({ } return ( - + {button} - + ); } diff --git a/ts/components/conversation/media-gallery/utils/storybook.dom.tsx b/ts/components/conversation/media-gallery/utils/storybook.dom.tsx index 28514972ba..841958c222 100644 --- a/ts/components/conversation/media-gallery/utils/storybook.dom.tsx +++ b/ts/components/conversation/media-gallery/utils/storybook.dom.tsx @@ -1,7 +1,11 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback } from 'react'; -import type { PropsType } from '../../../../state/smart/MediaItem.dom.js'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { action } from '@storybook/addon-actions'; + +import type { PropsType } from '../../../../state/smart/MediaItem.preload.js'; import { getSafeDomain } from '../../../../types/LinkPreview.std.js'; import type { AttachmentStatusType } from '../../../../hooks/useAttachmentStatus.std.js'; import { missingCaseError } from '../../../../util/missingCaseError.std.js'; @@ -19,6 +23,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element { }, [mediaItem, onItemClick] ); + const onShowMessage = action('onShowMessage'); switch (mediaItem.type) { case 'audio': @@ -28,6 +33,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element { authorTitle="Alice" mediaItem={mediaItem} onClick={onClick} + onShowMessage={onShowMessage} /> ); case 'media': @@ -36,7 +42,12 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element { ); case 'document': return ( - + ); case 'link': { const hydratedMediaItem = { @@ -53,6 +64,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element { authorTitle="Alice" mediaItem={hydratedMediaItem} onClick={onClick} + onShowMessage={onShowMessage} /> ); } diff --git a/ts/state/smart/AllMedia.preload.tsx b/ts/state/smart/AllMedia.preload.tsx index 58ca72a66a..8d4e2fb73e 100644 --- a/ts/state/smart/AllMedia.preload.tsx +++ b/ts/state/smart/AllMedia.preload.tsx @@ -16,7 +16,7 @@ import { useAudioPlayerActions } from '../ducks/audioPlayer.preload.js'; import { MediaItem, type PropsType as MediaItemPropsType, -} from './MediaItem.dom.js'; +} from './MediaItem.preload.js'; import { SmartMiniPlayer } from './MiniPlayer.preload.js'; const log = createLogger('AllMedia'); diff --git a/ts/state/smart/MediaItem.dom.tsx b/ts/state/smart/MediaItem.preload.tsx similarity index 78% rename from ts/state/smart/MediaItem.dom.tsx rename to ts/state/smart/MediaItem.preload.tsx index b9fa89c84d..ed13121154 100644 --- a/ts/state/smart/MediaItem.dom.tsx +++ b/ts/state/smart/MediaItem.preload.tsx @@ -13,6 +13,7 @@ import type { AttachmentStatusType } from '../../hooks/useAttachmentStatus.std.j import { missingCaseError } from '../../util/missingCaseError.std.js'; import { getIntl, getTheme } from '../selectors/user.std.js'; import { getConversationSelector } from '../selectors/conversations.dom.js'; +import { useConversationsActions } from '../ducks/conversations.preload.js'; export type PropsType = Readonly<{ onItemClick: (event: ItemClickEvent) => unknown; @@ -27,12 +28,14 @@ export const MediaItem = memo(function MediaItem({ const theme = useSelector(getTheme); const getConversation = useSelector(getConversationSelector); + const { showConversation } = useConversationsActions(); + + const { message } = mediaItem; + const authorTitle = - mediaItem.message.type === 'outgoing' + message.type === 'outgoing' ? i18n('icu:you') - : getConversation( - mediaItem.message.sourceServiceId ?? mediaItem.message.source - ).title; + : getConversation(message.sourceServiceId ?? message.source).title; const onClick = useCallback( (state: AttachmentStatusType['state']) => { @@ -41,6 +44,13 @@ export const MediaItem = memo(function MediaItem({ [mediaItem, onItemClick] ); + const onShowMessage = useCallback(() => { + showConversation({ + conversationId: message.conversationId, + messageId: message.id, + }); + }, [message.conversationId, message.id, showConversation]); + switch (mediaItem.type) { case 'audio': return ( @@ -49,6 +59,7 @@ export const MediaItem = memo(function MediaItem({ authorTitle={authorTitle} mediaItem={mediaItem} onClick={onClick} + onShowMessage={onShowMessage} /> ); case 'media': @@ -62,7 +73,12 @@ export const MediaItem = memo(function MediaItem({ ); case 'document': return ( - + ); case 'link': { const hydratedMediaItem = { @@ -80,6 +96,7 @@ export const MediaItem = memo(function MediaItem({ authorTitle={authorTitle} mediaItem={hydratedMediaItem} onClick={onClick} + onShowMessage={onShowMessage} /> ); }