Clicking media gallery date should show message

This commit is contained in:
Fedor Indutny
2025-11-19 14:39:23 -08:00
committed by GitHub
parent c016a6591b
commit 6f77be57e0
11 changed files with 84 additions and 17 deletions

View File

@@ -6756,6 +6756,10 @@
"messageformat": "Open the link in a browser", "messageformat": "Open the link in a browser",
"description": "Alt text for the link preview item button" "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": { "icu:MediaGallery__tab__audio": {
"messageformat": "Audio", "messageformat": "Audio",
"description": "Header of the links pane in the media gallery, showing audio" "description": "Header of the links pane in the media gallery, showing audio"

View File

@@ -29,6 +29,7 @@ export function Multiple(): JSX.Element {
mediaItem={mediaItem} mediaItem={mediaItem}
authorTitle="Alice" authorTitle="Alice"
onClick={action('onClick')} onClick={action('onClick')}
onShowMessage={action('onShowMessage')}
/> />
))} ))}
</> </>

View File

@@ -20,6 +20,7 @@ const MIN_PEAK_HEIGHT = 2;
export type DataProps = Readonly<{ export type DataProps = Readonly<{
mediaItem: MediaItemType; mediaItem: MediaItemType;
onClick: (status: AttachmentStatusType['state']) => void; onClick: (status: AttachmentStatusType['state']) => void;
onShowMessage: () => void;
}>; }>;
// Provided by smart layer // Provided by smart layer
@@ -35,6 +36,7 @@ export function AudioListItem({
mediaItem, mediaItem,
authorTitle, authorTitle,
onClick, onClick,
onShowMessage,
}: Props): JSX.Element { }: Props): JSX.Element {
const { attachment } = mediaItem; const { attachment } = mediaItem;
@@ -102,6 +104,7 @@ export function AudioListItem({
subtitle={subtitle.join(' · ')} subtitle={subtitle.join(' · ')}
readyLabel={i18n('icu:startDownload')} readyLabel={i18n('icu:startDownload')}
onClick={onClick} onClick={onClick}
onShowMessage={onShowMessage}
/> />
); );
} }

View File

@@ -28,6 +28,7 @@ export function Multiple(): JSX.Element {
key={mediaItem.attachment.fileName} key={mediaItem.attachment.fileName}
mediaItem={mediaItem} mediaItem={mediaItem}
onClick={action('onClick')} onClick={action('onClick')}
onShowMessage={action('onShowMessage')}
/> />
))} ))}
</> </>

View File

@@ -18,12 +18,14 @@ export type Props = {
i18n: LocalizerType; i18n: LocalizerType;
mediaItem: MediaItemType; mediaItem: MediaItemType;
onClick: (status: AttachmentStatusType['state']) => void; onClick: (status: AttachmentStatusType['state']) => void;
onShowMessage: () => void;
}; };
export function DocumentListItem({ export function DocumentListItem({
i18n, i18n,
mediaItem, mediaItem,
onClick, onClick,
onShowMessage,
}: Props): JSX.Element { }: Props): JSX.Element {
const { attachment } = mediaItem; const { attachment } = mediaItem;
@@ -57,6 +59,7 @@ export function DocumentListItem({
subtitle={subtitle} subtitle={subtitle}
readyLabel={i18n('icu:startDownload')} readyLabel={i18n('icu:startDownload')}
onClick={onClick} onClick={onClick}
onShowMessage={onShowMessage}
/> />
); );
} }

View File

@@ -29,6 +29,7 @@ export function Multiple(): JSX.Element {
authorTitle="Alice" authorTitle="Alice"
mediaItem={mediaItem} mediaItem={mediaItem}
onClick={action('onClick')} onClick={action('onClick')}
onShowMessage={action('onShowMessage')}
/> />
))} ))}
</> </>

View File

@@ -19,6 +19,7 @@ import { ListItem } from './ListItem.dom.js';
export type DataProps = Readonly<{ export type DataProps = Readonly<{
mediaItem: LinkPreviewMediaItemType; mediaItem: LinkPreviewMediaItemType;
onClick: (status: AttachmentStatusType['state']) => void; onClick: (status: AttachmentStatusType['state']) => void;
onShowMessage: () => void;
}>; }>;
// Provided by smart layer // Provided by smart layer
@@ -35,6 +36,7 @@ export function LinkPreviewItem({
mediaItem, mediaItem,
authorTitle, authorTitle,
onClick, onClick,
onShowMessage,
}: Props): JSX.Element { }: Props): JSX.Element {
const { preview } = mediaItem; const { preview } = mediaItem;
@@ -94,6 +96,7 @@ export function LinkPreviewItem({
subtitle={subtitle} subtitle={subtitle}
readyLabel={i18n('icu:LinkPreviewItem__alt')} readyLabel={i18n('icu:LinkPreviewItem__alt')}
onClick={onClick} onClick={onClick}
onShowMessage={onShowMessage}
/> />
); );
} }

View File

@@ -10,6 +10,7 @@ import type { AttachmentForUIType } from '../../../types/Attachment.std.js';
import type { LocalizerType } from '../../../types/Util.std.js'; import type { LocalizerType } from '../../../types/Util.std.js';
import { SpinnerV2 } from '../../SpinnerV2.dom.js'; import { SpinnerV2 } from '../../SpinnerV2.dom.js';
import { tw } from '../../../axo/tw.dom.js'; import { tw } from '../../../axo/tw.dom.js';
import { AriaClickable } from '../../../axo/AriaClickable.dom.js';
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.js'; import { AxoSymbol } from '../../../axo/AxoSymbol.dom.js';
import { import {
useAttachmentStatus, useAttachmentStatus,
@@ -24,6 +25,7 @@ export type Props = {
subtitle: React.ReactNode; subtitle: React.ReactNode;
readyLabel: string; readyLabel: string;
onClick: (status: AttachmentStatusType['state']) => void; onClick: (status: AttachmentStatusType['state']) => void;
onShowMessage: () => void;
}; };
export function ListItem({ export function ListItem({
@@ -34,6 +36,7 @@ export function ListItem({
subtitle, subtitle,
readyLabel, readyLabel,
onClick, onClick,
onShowMessage,
}: Props): JSX.Element { }: Props): JSX.Element {
const { message } = mediaItem; const { message } = mediaItem;
let attachment: AttachmentForUIType | undefined; let attachment: AttachmentForUIType | undefined;
@@ -53,11 +56,21 @@ export function ListItem({
const handleClick = useCallback( const handleClick = useCallback(
(ev: React.MouseEvent) => { (ev: React.MouseEvent) => {
ev.preventDefault(); ev.preventDefault();
onClick?.(status?.state || 'ReadyToShow'); ev.stopPropagation();
onClick(status?.state || 'ReadyToShow');
}, },
[onClick, status?.state] [onClick, status?.state]
); );
const handleDateClick = useCallback(
(ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
onShowMessage();
},
[onShowMessage]
);
if (status == null || status.state === 'ReadyToShow') { if (status == null || status.state === 'ReadyToShow') {
label = readyLabel; label = readyLabel;
} else if (status.state === 'NeedsDownload') { } else if (status.state === 'NeedsDownload') {
@@ -108,14 +121,11 @@ export function ListItem({
} }
return ( return (
<button <AriaClickable.Root
className={tw( className={tw(
'flex w-full flex-row gap-3 py-2', 'flex w-full flex-row gap-3 py-2',
mediaItem.type === 'link' ? undefined : 'items-center' mediaItem.type === 'link' ? undefined : 'items-center'
)} )}
type="button"
onClick={handleClick}
aria-label={label}
> >
<div className={tw('shrink-0')}>{thumbnail}</div> <div className={tw('shrink-0')}>{thumbnail}</div>
<div className={tw('grow overflow-hidden text-start')}> <div className={tw('grow overflow-hidden text-start')}>
@@ -124,10 +134,22 @@ export function ListItem({
{subtitle} {subtitle}
</div> </div>
</div> </div>
<div className={tw('shrink-0 type-body-small text-label-secondary')}> <AriaClickable.HiddenTrigger aria-label={label} onClick={handleClick} />
<AriaClickable.SubWidget>
<button
type="button"
className={tw(
'shrink-0 self-stretch',
mediaItem.type === 'link' ? undefined : 'flex items-center',
'type-body-small text-label-secondary'
)}
aria-label={i18n('icu:ListItem__show-message')}
onClick={handleDateClick}
>
{moment(timestamp).format('MMM D')} {moment(timestamp).format('MMM D')}
</div>
{button}
</button> </button>
</AriaClickable.SubWidget>
{button}
</AriaClickable.Root>
); );
} }

View File

@@ -1,7 +1,11 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react'; 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 { getSafeDomain } from '../../../../types/LinkPreview.std.js';
import type { AttachmentStatusType } from '../../../../hooks/useAttachmentStatus.std.js'; import type { AttachmentStatusType } from '../../../../hooks/useAttachmentStatus.std.js';
import { missingCaseError } from '../../../../util/missingCaseError.std.js'; import { missingCaseError } from '../../../../util/missingCaseError.std.js';
@@ -19,6 +23,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element {
}, },
[mediaItem, onItemClick] [mediaItem, onItemClick]
); );
const onShowMessage = action('onShowMessage');
switch (mediaItem.type) { switch (mediaItem.type) {
case 'audio': case 'audio':
@@ -28,6 +33,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element {
authorTitle="Alice" authorTitle="Alice"
mediaItem={mediaItem} mediaItem={mediaItem}
onClick={onClick} onClick={onClick}
onShowMessage={onShowMessage}
/> />
); );
case 'media': case 'media':
@@ -36,7 +42,12 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element {
); );
case 'document': case 'document':
return ( return (
<DocumentListItem i18n={i18n} mediaItem={mediaItem} onClick={onClick} /> <DocumentListItem
i18n={i18n}
mediaItem={mediaItem}
onClick={onClick}
onShowMessage={onShowMessage}
/>
); );
case 'link': { case 'link': {
const hydratedMediaItem = { const hydratedMediaItem = {
@@ -53,6 +64,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element {
authorTitle="Alice" authorTitle="Alice"
mediaItem={hydratedMediaItem} mediaItem={hydratedMediaItem}
onClick={onClick} onClick={onClick}
onShowMessage={onShowMessage}
/> />
); );
} }

View File

@@ -16,7 +16,7 @@ import { useAudioPlayerActions } from '../ducks/audioPlayer.preload.js';
import { import {
MediaItem, MediaItem,
type PropsType as MediaItemPropsType, type PropsType as MediaItemPropsType,
} from './MediaItem.dom.js'; } from './MediaItem.preload.js';
import { SmartMiniPlayer } from './MiniPlayer.preload.js'; import { SmartMiniPlayer } from './MiniPlayer.preload.js';
const log = createLogger('AllMedia'); const log = createLogger('AllMedia');

View File

@@ -13,6 +13,7 @@ import type { AttachmentStatusType } from '../../hooks/useAttachmentStatus.std.j
import { missingCaseError } from '../../util/missingCaseError.std.js'; import { missingCaseError } from '../../util/missingCaseError.std.js';
import { getIntl, getTheme } from '../selectors/user.std.js'; import { getIntl, getTheme } from '../selectors/user.std.js';
import { getConversationSelector } from '../selectors/conversations.dom.js'; import { getConversationSelector } from '../selectors/conversations.dom.js';
import { useConversationsActions } from '../ducks/conversations.preload.js';
export type PropsType = Readonly<{ export type PropsType = Readonly<{
onItemClick: (event: ItemClickEvent) => unknown; onItemClick: (event: ItemClickEvent) => unknown;
@@ -27,12 +28,14 @@ export const MediaItem = memo(function MediaItem({
const theme = useSelector(getTheme); const theme = useSelector(getTheme);
const getConversation = useSelector(getConversationSelector); const getConversation = useSelector(getConversationSelector);
const { showConversation } = useConversationsActions();
const { message } = mediaItem;
const authorTitle = const authorTitle =
mediaItem.message.type === 'outgoing' message.type === 'outgoing'
? i18n('icu:you') ? i18n('icu:you')
: getConversation( : getConversation(message.sourceServiceId ?? message.source).title;
mediaItem.message.sourceServiceId ?? mediaItem.message.source
).title;
const onClick = useCallback( const onClick = useCallback(
(state: AttachmentStatusType['state']) => { (state: AttachmentStatusType['state']) => {
@@ -41,6 +44,13 @@ export const MediaItem = memo(function MediaItem({
[mediaItem, onItemClick] [mediaItem, onItemClick]
); );
const onShowMessage = useCallback(() => {
showConversation({
conversationId: message.conversationId,
messageId: message.id,
});
}, [message.conversationId, message.id, showConversation]);
switch (mediaItem.type) { switch (mediaItem.type) {
case 'audio': case 'audio':
return ( return (
@@ -49,6 +59,7 @@ export const MediaItem = memo(function MediaItem({
authorTitle={authorTitle} authorTitle={authorTitle}
mediaItem={mediaItem} mediaItem={mediaItem}
onClick={onClick} onClick={onClick}
onShowMessage={onShowMessage}
/> />
); );
case 'media': case 'media':
@@ -62,7 +73,12 @@ export const MediaItem = memo(function MediaItem({
); );
case 'document': case 'document':
return ( return (
<DocumentListItem i18n={i18n} mediaItem={mediaItem} onClick={onClick} /> <DocumentListItem
i18n={i18n}
mediaItem={mediaItem}
onClick={onClick}
onShowMessage={onShowMessage}
/>
); );
case 'link': { case 'link': {
const hydratedMediaItem = { const hydratedMediaItem = {
@@ -80,6 +96,7 @@ export const MediaItem = memo(function MediaItem({
authorTitle={authorTitle} authorTitle={authorTitle}
mediaItem={hydratedMediaItem} mediaItem={hydratedMediaItem}
onClick={onClick} onClick={onClick}
onShowMessage={onShowMessage}
/> />
); );
} }