Media Gallery improvements

This commit is contained in:
Fedor Indutny
2025-11-20 10:52:17 -08:00
committed by GitHub
parent 879d5946fa
commit 60bb04a4fc
26 changed files with 235 additions and 60 deletions

View File

@@ -58,6 +58,10 @@ function createMediaItem(
// Unused for now
source: undefined,
sourceServiceId: undefined,
isErased: false,
readStatus: undefined,
sendStateByConversationId: undefined,
errors: undefined,
},
...overrideProps,
};
@@ -110,6 +114,10 @@ export function Multimedia(): JSX.Element {
// Unused for now
source: undefined,
sourceServiceId: undefined,
isErased: false,
readStatus: undefined,
sendStateByConversationId: undefined,
errors: undefined,
},
},
{
@@ -130,6 +138,10 @@ export function Multimedia(): JSX.Element {
// Unused for now
source: undefined,
sourceServiceId: undefined,
isErased: false,
readStatus: undefined,
sendStateByConversationId: undefined,
errors: undefined,
},
},
createMediaItem({
@@ -170,6 +182,10 @@ export function MissingMedia(): JSX.Element {
// Unused for now
source: undefined,
sourceServiceId: undefined,
isErased: false,
readStatus: undefined,
sendStateByConversationId: undefined,
errors: undefined,
},
},
],

View File

@@ -675,6 +675,7 @@ function ReplyOrReactionMessage({
id={reply.id}
interactionMode="mouse"
isSpoilerExpanded={isSpoilerExpanded}
isVoiceMessagePlayed={false}
messageExpanded={messageExpanded}
readStatus={reply.readStatus}
renderingContext="StoryViewsNRepliesModal"

View File

@@ -70,7 +70,6 @@ import {
isGIF,
isImage,
isImageAttachment,
isPlayed,
isVideo,
} from '../../util/Attachment.std.js';
import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact.std.js';
@@ -250,6 +249,7 @@ export type PropsData = {
isSelectMode: boolean;
isSMS: boolean;
isSpoilerExpanded?: Record<number, boolean>;
isVoiceMessagePlayed: boolean;
canEndPoll?: boolean;
direction: DirectionType;
timestamp: number;
@@ -1145,11 +1145,11 @@ export class Message extends React.PureComponent<Props, State> {
i18n,
id,
isSticker,
isVoiceMessagePlayed,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
pushPanelForConversation,
quote,
readStatus,
renderAudioAttachment,
renderingContext,
retryMessageSend,
@@ -1282,8 +1282,6 @@ export class Message extends React.PureComponent<Props, State> {
}
if (isAttachmentAudio) {
const played = isPlayed(direction, status, readStatus);
return renderAudioAttachment({
i18n,
buttonRef: this.audioButtonRef,
@@ -1299,7 +1297,7 @@ export class Message extends React.PureComponent<Props, State> {
expirationTimestamp,
id,
conversationId,
played,
played: isVoiceMessagePlayed,
pushPanelForConversation,
status,
textPending: textAttachment?.pending,

View File

@@ -36,6 +36,7 @@ const defaultMessage: MessageDataPropsType = {
isSelectMode: false,
isSMS: false,
isSpoilerExpanded: {},
isVoiceMessagePlayed: false,
previews: [],
readStatus: ReadStatus.Read,
status: 'sent',

View File

@@ -108,6 +108,7 @@ const defaultMessageProps: TimelineMessagesProps = {
isSelectMode: false,
isSMS: false,
isSpoilerExpanded: {},
isVoiceMessagePlayed: false,
toggleSelectMessage: action('toggleSelectMessage'),
cancelAttachmentDownload: action('default--cancelAttachmentDownload'),
kickOffAttachmentDownload: action('default--kickOffAttachmentDownload'),

View File

@@ -71,6 +71,7 @@ function mockMessageTimelineItem(
isSelectMode: false,
isSMS: false,
isSpoilerExpanded: {},
isVoiceMessagePlayed: false,
previews: [],
readStatus: ReadStatus.Read,
canRetryDeleteForEveryone: true,

View File

@@ -286,6 +286,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
isTapToView: overrideProps.isTapToView,
isTapToViewError: overrideProps.isTapToViewError,
isTapToViewExpired: overrideProps.isTapToViewExpired,
isVoiceMessagePlayed: false,
cancelAttachmentDownload: action('cancelAttachmentDownload'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),

View File

@@ -27,6 +27,7 @@ export function Multiple(): JSX.Element {
i18n={i18n}
key={index}
mediaItem={mediaItem}
isPlayed={Math.random() > 0.5}
authorTitle="Alice"
onClick={action('onClick')}
onShowMessage={action('onShowMessage')}

View File

@@ -3,6 +3,8 @@
import React from 'react';
import { noop } from 'lodash';
import type { Transition } from 'framer-motion';
import { motion } from 'framer-motion';
import { tw } from '../../../axo/tw.dom.js';
import { formatFileSize } from '../../../util/formatFileSize.std.js';
@@ -17,6 +19,13 @@ const BAR_COUNT = 7;
const MAX_PEAK_HEIGHT = 22;
const MIN_PEAK_HEIGHT = 2;
const DOT_TRANSITION: Transition = {
type: 'spring',
mass: 0.5,
stiffness: 350,
damping: 20,
};
export type DataProps = Readonly<{
mediaItem: MediaItemType;
onClick: (status: AttachmentStatusType['state']) => void;
@@ -29,12 +38,14 @@ export type Props = DataProps &
i18n: LocalizerType;
theme?: ThemeType;
authorTitle: string;
isPlayed: boolean;
}>;
export function AudioListItem({
i18n,
mediaItem,
authorTitle,
isPlayed,
onClick,
onShowMessage,
}: Props): JSX.Element {
@@ -95,13 +106,29 @@ export function AudioListItem({
</div>
);
const dot = (
<motion.div
className={tw('size-1.5 shrink-0 rounded bg-label-secondary')}
initial={false}
animate={{ scale: isPlayed ? 0 : 1 }}
transition={DOT_TRANSITION}
/>
);
return (
<ListItem
i18n={i18n}
mediaItem={mediaItem}
thumbnail={thumbnail}
title={fileName == null ? authorTitle : `${fileName} · ${authorTitle}`}
subtitle={subtitle.join(' · ')}
subtitle={
<div className={tw('flex items-center gap-1')}>
<div className={tw('truncate overflow-hidden')}>
{subtitle.join(' · ')}
</div>
{dot}
</div>
}
readyLabel={i18n('icu:startDownload')}
onClick={onClick}
onShowMessage={onShowMessage}

View File

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

View File

@@ -17,6 +17,7 @@ import { ListItem } from './ListItem.dom.js';
export type Props = {
i18n: LocalizerType;
mediaItem: MediaItemType;
authorTitle: string;
onClick: (status: AttachmentStatusType['state']) => void;
onShowMessage: () => void;
};
@@ -24,6 +25,7 @@ export type Props = {
export function DocumentListItem({
i18n,
mediaItem,
authorTitle,
onClick,
onShowMessage,
}: Props): JSX.Element {
@@ -50,12 +52,18 @@ export function DocumentListItem({
</>
);
const title = new Array<string>();
if (fileName) {
title.push(fileName);
}
title.push(authorTitle);
return (
<ListItem
i18n={i18n}
mediaItem={mediaItem}
thumbnail={<FileThumbnail {...attachment} />}
title={fileName}
title={title.join(' · ')}
subtitle={subtitle}
readyLabel={i18n('icu:startDownload')}
onClick={onClick}

View File

@@ -12,6 +12,7 @@ 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 { UserText } from '../../UserText.dom.js';
import {
useAttachmentStatus,
type AttachmentStatusType,
@@ -21,7 +22,7 @@ export type Props = {
i18n: LocalizerType;
mediaItem: GenericMediaItemType;
thumbnail: React.ReactNode;
title: React.ReactNode;
title: string;
subtitle: React.ReactNode;
readyLabel: string;
onClick: (status: AttachmentStatusType['state']) => void;
@@ -129,7 +130,9 @@ export function ListItem({
>
<div className={tw('shrink-0')}>{thumbnail}</div>
<div className={tw('grow overflow-hidden text-start')}>
<h3 className={tw('truncate')}>{title}</h3>
<h3 className={tw('truncate')}>
<UserText text={title} />
</h3>
<div className={tw('type-body-small leading-4 text-label-secondary')}>
{subtitle}
</div>

View File

@@ -65,6 +65,10 @@ const createMediaItem = (
// Unused for now
source: undefined,
sourceServiceId: undefined,
readStatus: undefined,
isErased: false,
errors: undefined,
sendStateByConversationId: undefined,
},
});

View File

@@ -88,6 +88,10 @@ function createRandomMessage(
// Unused for now
source: undefined,
sourceServiceId: undefined,
isErased: false,
readStatus: undefined,
sendStateByConversationId: undefined,
errors: undefined,
};
}

View File

@@ -9,6 +9,7 @@ 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';
import { isVoiceMessagePlayed } from '../../../../util/isVoiceMessagePlayed.std.js';
import { LinkPreviewItem } from '../LinkPreviewItem.dom.js';
import { MediaGridItem } from '../MediaGridItem.dom.js';
import { DocumentListItem } from '../DocumentListItem.dom.js';
@@ -31,6 +32,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element {
<AudioListItem
i18n={i18n}
authorTitle="Alice"
isPlayed={isVoiceMessagePlayed(mediaItem.message, undefined)}
mediaItem={mediaItem}
onClick={onClick}
onShowMessage={onShowMessage}
@@ -44,6 +46,7 @@ export function MediaItem({ mediaItem, onItemClick }: PropsType): JSX.Element {
return (
<DocumentListItem
i18n={i18n}
authorTitle="Alice"
mediaItem={mediaItem}
onClick={onClick}
onShowMessage={onShowMessage}