mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 18:29:06 +00:00
Media Gallery improvements
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -675,6 +675,7 @@ function ReplyOrReactionMessage({
|
||||
id={reply.id}
|
||||
interactionMode="mouse"
|
||||
isSpoilerExpanded={isSpoilerExpanded}
|
||||
isVoiceMessagePlayed={false}
|
||||
messageExpanded={messageExpanded}
|
||||
readStatus={reply.readStatus}
|
||||
renderingContext="StoryViewsNRepliesModal"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -36,6 +36,7 @@ const defaultMessage: MessageDataPropsType = {
|
||||
isSelectMode: false,
|
||||
isSMS: false,
|
||||
isSpoilerExpanded: {},
|
||||
isVoiceMessagePlayed: false,
|
||||
previews: [],
|
||||
readStatus: ReadStatus.Read,
|
||||
status: 'sent',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -71,6 +71,7 @@ function mockMessageTimelineItem(
|
||||
isSelectMode: false,
|
||||
isSMS: false,
|
||||
isSpoilerExpanded: {},
|
||||
isVoiceMessagePlayed: false,
|
||||
previews: [],
|
||||
readStatus: ReadStatus.Read,
|
||||
canRetryDeleteForEveryone: true,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -65,6 +65,10 @@ const createMediaItem = (
|
||||
// Unused for now
|
||||
source: undefined,
|
||||
sourceServiceId: undefined,
|
||||
readStatus: undefined,
|
||||
isErased: false,
|
||||
errors: undefined,
|
||||
sendStateByConversationId: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -88,6 +88,10 @@ function createRandomMessage(
|
||||
// Unused for now
|
||||
source: undefined,
|
||||
sourceServiceId: undefined,
|
||||
isErased: false,
|
||||
readStatus: undefined,
|
||||
sendStateByConversationId: undefined,
|
||||
errors: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5368,6 +5368,10 @@ function getSortedMedia(
|
||||
const createQuery = (timeFilter: QueryFragment): QueryFragment => sqlFragment`
|
||||
SELECT
|
||||
message_attachments.*,
|
||||
messages.json -> '$.sendStateByConversationId' AS messageSendState,
|
||||
messages.json -> '$.errors' AS messageErrors,
|
||||
messages.isErased AS messageIsErased,
|
||||
messages.readStatus AS messageReadStatus,
|
||||
messages.source AS messageSource,
|
||||
messages.sourceServiceId AS messageSourceServiceId
|
||||
FROM message_attachments
|
||||
@@ -5399,6 +5403,10 @@ function getSortedMedia(
|
||||
|
||||
const results: Array<
|
||||
MessageAttachmentDBType & {
|
||||
messageSendState: string | null;
|
||||
messageErrors: string | null;
|
||||
messageIsErased: number | null;
|
||||
messageReadStatus: ReadStatus | null;
|
||||
messageSource: string | null;
|
||||
messageSourceServiceId: ServiceIdString | null;
|
||||
}
|
||||
@@ -5410,6 +5418,10 @@ function getSortedMedia(
|
||||
messageType,
|
||||
messageSource,
|
||||
messageSourceServiceId,
|
||||
messageSendState,
|
||||
messageErrors,
|
||||
messageIsErased,
|
||||
messageReadStatus,
|
||||
sentAt,
|
||||
receivedAt,
|
||||
receivedAtMs,
|
||||
@@ -5425,6 +5437,11 @@ function getSortedMedia(
|
||||
receivedAt,
|
||||
receivedAtMs: receivedAtMs ?? undefined,
|
||||
sentAt,
|
||||
sendStateByConversationId:
|
||||
messageSendState == null ? undefined : JSON.parse(messageSendState),
|
||||
errors: messageErrors == null ? undefined : JSON.parse(messageErrors),
|
||||
isErased: messageIsErased === 1,
|
||||
readStatus: messageReadStatus ?? undefined,
|
||||
},
|
||||
index: orderInMessage,
|
||||
attachment: convertAttachmentDBFieldsToAttachmentType(attachment),
|
||||
@@ -5487,6 +5504,10 @@ function getOlderLinkPreviews(
|
||||
receivedAt: message.received_at,
|
||||
receivedAtMs: message.received_at_ms ?? undefined,
|
||||
sentAt: message.sent_at,
|
||||
errors: message.errors,
|
||||
sendStateByConversationId: message.sendStateByConversationId,
|
||||
readStatus: message.readStatus,
|
||||
isErased: !!message.isErased,
|
||||
},
|
||||
preview: message.preview[0],
|
||||
};
|
||||
|
||||
@@ -28,7 +28,6 @@ import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl.std.js';
|
||||
import { assertDev } from '../../util/assert.std.js';
|
||||
import { drop } from '../../util/drop.std.js';
|
||||
import { Sound, SoundType } from '../../util/Sound.std.js';
|
||||
import { getMessageById } from '../../messages/getMessageById.preload.js';
|
||||
import { DataReader } from '../../sql/Client.preload.js';
|
||||
|
||||
const stateChangeConfirmUpSound = new Sound({
|
||||
@@ -110,12 +109,16 @@ async function getNextVoiceNote({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const next = await getMessageById(results[0].message.id);
|
||||
if (next == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return extractVoiceNoteForPlayback(next.attributes, ourConversationId);
|
||||
const { message, attachment } = results[0];
|
||||
return extractVoiceNoteForPlayback(
|
||||
{
|
||||
...message,
|
||||
attachments: [attachment],
|
||||
sent_at: message.sentAt,
|
||||
received_at: message.receivedAt,
|
||||
},
|
||||
ourConversationId
|
||||
);
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
@@ -233,6 +233,10 @@ function showLightboxForViewOnceMedia(
|
||||
sentAt: message.get('sent_at'),
|
||||
source: message.get('source'),
|
||||
sourceServiceId: message.get('sourceServiceId'),
|
||||
isErased: !!message.get('isErased'),
|
||||
readStatus: message.get('readStatus'),
|
||||
sendStateByConversationId: message.get('sendStateByConversationId'),
|
||||
errors: message.get('errors'),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -338,6 +342,10 @@ function showLightbox(opts: {
|
||||
source: message.get('source'),
|
||||
sourceServiceId: message.get('sourceServiceId'),
|
||||
sentAt,
|
||||
isErased: !!message.get('isErased'),
|
||||
errors: message.get('errors'),
|
||||
readStatus: message.get('readStatus'),
|
||||
sendStateByConversationId: message.get('sendStateByConversationId'),
|
||||
},
|
||||
type: 'media' as const,
|
||||
attachment: getPropsForAttachment(
|
||||
|
||||
@@ -5,6 +5,7 @@ import lodash from 'lodash';
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import type { ReadonlyMessageAttributesType } from '../../model-types.d.ts';
|
||||
import { createLogger } from '../../logging/log.std.js';
|
||||
import { DataReader } from '../../sql/Client.preload.js';
|
||||
import type {
|
||||
@@ -111,6 +112,25 @@ function _sortItems<
|
||||
]);
|
||||
}
|
||||
|
||||
function _cleanMessage(
|
||||
message: ReadonlyMessageAttributesType
|
||||
): MediaItemMessageType {
|
||||
return {
|
||||
id: message.id,
|
||||
type: message.type,
|
||||
source: message.source,
|
||||
sourceServiceId: message.sourceServiceId,
|
||||
conversationId: message.conversationId,
|
||||
receivedAt: message.received_at,
|
||||
receivedAtMs: message.received_at_ms,
|
||||
sentAt: message.sent_at,
|
||||
isErased: !!message.isErased,
|
||||
errors: message.errors,
|
||||
readStatus: message.readStatus,
|
||||
sendStateByConversationId: message.sendStateByConversationId,
|
||||
};
|
||||
}
|
||||
|
||||
function _cleanAttachments(
|
||||
type: 'media' | 'audio' | 'documents',
|
||||
rawMedia: ReadonlyArray<MediaItemDBType>
|
||||
@@ -428,16 +448,7 @@ export function reducer(
|
||||
return {
|
||||
index,
|
||||
attachment,
|
||||
message: {
|
||||
id: message.id,
|
||||
type: message.type,
|
||||
source: message.source,
|
||||
sourceServiceId: message.sourceServiceId,
|
||||
conversationId: message.conversationId,
|
||||
receivedAt: message.received_at,
|
||||
receivedAtMs: message.received_at_ms,
|
||||
sentAt: message.sent_at,
|
||||
},
|
||||
message: _cleanMessage(message),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -460,16 +471,7 @@ export function reducer(
|
||||
? [
|
||||
{
|
||||
preview: message.preview[0],
|
||||
message: {
|
||||
id: message.id,
|
||||
type: message.type,
|
||||
source: message.source,
|
||||
sourceServiceId: message.sourceServiceId,
|
||||
conversationId: message.conversationId,
|
||||
receivedAt: message.received_at,
|
||||
receivedAtMs: message.received_at_ms,
|
||||
sentAt: message.sent_at,
|
||||
},
|
||||
message: _cleanMessage(message),
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
@@ -8,11 +8,7 @@ import {
|
||||
getUserConversationId,
|
||||
getUserNumber,
|
||||
} from './user.std.js';
|
||||
import {
|
||||
getMessagePropStatus,
|
||||
getSource,
|
||||
getSourceServiceId,
|
||||
} from './message.preload.js';
|
||||
import { getSource, getSourceServiceId } from './message.preload.js';
|
||||
import {
|
||||
getConversationByIdSelector,
|
||||
getConversations,
|
||||
@@ -26,7 +22,7 @@ import type { ReadonlyMessageAttributesType } from '../../model-types.d.ts';
|
||||
import { getMessageIdForLogging } from '../../util/idForLogging.preload.js';
|
||||
import * as Attachment from '../../util/Attachment.std.js';
|
||||
import type { ActiveAudioPlayerStateType } from '../ducks/audioPlayer.preload.js';
|
||||
import { isPlayed } from '../../util/Attachment.std.js';
|
||||
import { isVoiceMessagePlayed } from '../../util/isVoiceMessagePlayed.std.js';
|
||||
import type { ServiceIdString } from '../../types/ServiceId.std.js';
|
||||
|
||||
const log = createLogger('audioPlayer');
|
||||
@@ -81,7 +77,20 @@ export const selectVoiceNoteTitle = createSelector(
|
||||
);
|
||||
|
||||
export function extractVoiceNoteForPlayback(
|
||||
message: ReadonlyMessageAttributesType,
|
||||
message: Pick<
|
||||
ReadonlyMessageAttributesType,
|
||||
| 'id'
|
||||
| 'type'
|
||||
| 'attachments'
|
||||
| 'isErased'
|
||||
| 'errors'
|
||||
| 'readStatus'
|
||||
| 'sendStateByConversationId'
|
||||
| 'sent_at'
|
||||
| 'received_at'
|
||||
| 'source'
|
||||
| 'sourceServiceId'
|
||||
>,
|
||||
ourConversationId: string | undefined
|
||||
): VoiceNoteForPlayback | undefined {
|
||||
const { type } = message;
|
||||
@@ -98,13 +107,12 @@ export function extractVoiceNoteForPlayback(
|
||||
const voiceNoteUrl = attachment.path
|
||||
? getLocalAttachmentUrl(attachment)
|
||||
: undefined;
|
||||
const status = getMessagePropStatus(message, ourConversationId);
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
url: voiceNoteUrl,
|
||||
type,
|
||||
isPlayed: isPlayed(type, status, message.readStatus),
|
||||
isPlayed: isVoiceMessagePlayed(message, ourConversationId),
|
||||
messageIdForLogging: getMessageIdForLogging(message),
|
||||
sentAt: message.sent_at,
|
||||
receivedAt: message.received_at,
|
||||
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
getLocalAttachmentUrl,
|
||||
AttachmentDisposition,
|
||||
} from '../../util/getLocalAttachmentUrl.std.js';
|
||||
import { isVoiceMessagePlayed } from '../../util/isVoiceMessagePlayed.std.js';
|
||||
import { isPermanentlyUndownloadable } from '../../jobs/AttachmentDownloadManager.preload.js';
|
||||
|
||||
import { getAccountSelector } from './accounts.std.js';
|
||||
@@ -977,6 +978,7 @@ export const getPropsForMessage = (
|
||||
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
||||
isTapToViewExpired:
|
||||
isMessageTapToView && isIncoming(message) && message.isErased,
|
||||
isVoiceMessagePlayed: isVoiceMessagePlayed(message, ourConversationId),
|
||||
readStatus: message.readStatus ?? ReadStatus.Read,
|
||||
selectedReaction,
|
||||
status: getMessagePropStatus(message, ourConversationId),
|
||||
|
||||
@@ -11,7 +11,12 @@ import { getSafeDomain } from '../../types/LinkPreview.std.js';
|
||||
import type { GenericMediaItemType } from '../../types/MediaItem.std.js';
|
||||
import type { AttachmentStatusType } from '../../hooks/useAttachmentStatus.std.js';
|
||||
import { missingCaseError } from '../../util/missingCaseError.std.js';
|
||||
import { getIntl, getTheme } from '../selectors/user.std.js';
|
||||
import { isVoiceMessagePlayed } from '../../util/isVoiceMessagePlayed.std.js';
|
||||
import {
|
||||
getIntl,
|
||||
getTheme,
|
||||
getUserConversationId,
|
||||
} from '../selectors/user.std.js';
|
||||
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||
|
||||
@@ -26,6 +31,7 @@ export const MediaItem = memo(function MediaItem({
|
||||
}: PropsType) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
const ourConversationId = useSelector(getUserConversationId);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
|
||||
const { showConversation } = useConversationsActions();
|
||||
@@ -57,6 +63,7 @@ export const MediaItem = memo(function MediaItem({
|
||||
<AudioListItem
|
||||
i18n={i18n}
|
||||
authorTitle={authorTitle}
|
||||
isPlayed={isVoiceMessagePlayed(mediaItem.message, ourConversationId)}
|
||||
mediaItem={mediaItem}
|
||||
onClick={onClick}
|
||||
onShowMessage={onShowMessage}
|
||||
@@ -75,6 +82,7 @@ export const MediaItem = memo(function MediaItem({
|
||||
return (
|
||||
<DocumentListItem
|
||||
i18n={i18n}
|
||||
authorTitle={authorTitle}
|
||||
mediaItem={mediaItem}
|
||||
onClick={onClick}
|
||||
onShowMessage={onShowMessage}
|
||||
|
||||
@@ -31,8 +31,14 @@ const toMediaItem = (id: string, date: Date): MediaItemType => {
|
||||
receivedAt: date.getTime(),
|
||||
receivedAtMs: date.getTime(),
|
||||
sentAt: date.getTime(),
|
||||
|
||||
// Unused for now
|
||||
source: undefined,
|
||||
sourceServiceId: undefined,
|
||||
isErased: false,
|
||||
readStatus: undefined,
|
||||
sendStateByConversationId: undefined,
|
||||
errors: undefined,
|
||||
},
|
||||
attachment: fakeAttachment({
|
||||
fileName: 'fileName',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { MessageAttributesType } from '../model-types.d.ts';
|
||||
import type { MessageAttributesType, CustomError } from '../model-types.d.ts';
|
||||
import type { SendStateByConversationId } from '../messages/MessageSendState.std.js';
|
||||
import type { ReadStatus } from '../messages/MessageReadStatus.std.js';
|
||||
import type { AttachmentForUIType } from './Attachment.std.js';
|
||||
import type { LinkPreviewForUIType } from './message/LinkPreviews.std.js';
|
||||
import type { ServiceIdString } from './ServiceId.std.js';
|
||||
@@ -15,6 +17,10 @@ export type MediaItemMessageType = Readonly<{
|
||||
sentAt: number;
|
||||
source: string | undefined;
|
||||
sourceServiceId: ServiceIdString | undefined;
|
||||
isErased: boolean;
|
||||
sendStateByConversationId: SendStateByConversationId | undefined;
|
||||
readStatus: ReadStatus | undefined;
|
||||
errors: ReadonlyArray<CustomError> | undefined;
|
||||
}>;
|
||||
|
||||
export type MediaItemType = {
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
} from './GoogleChrome.std.js';
|
||||
import type { LocalizerType } from '../types/Util.std.js';
|
||||
import { ThemeType } from '../types/Util.std.js';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus.std.js';
|
||||
import type { MessageStatusType } from '../types/message/MessageStatus.std.js';
|
||||
import { isMoreRecentThan } from './timestamp.std.js';
|
||||
import { DAY } from './durations/index.std.js';
|
||||
import {
|
||||
@@ -270,17 +268,6 @@ export function isAudio(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isPlayed(
|
||||
direction: 'outgoing' | 'incoming',
|
||||
status: MessageStatusType | undefined,
|
||||
readStatus: ReadStatus | undefined
|
||||
): boolean {
|
||||
if (direction === 'outgoing') {
|
||||
return status === 'viewed';
|
||||
}
|
||||
return readStatus === ReadStatus.Viewed;
|
||||
}
|
||||
|
||||
export function canRenderAudio(
|
||||
attachments?: ReadonlyArray<AttachmentType>
|
||||
): boolean {
|
||||
|
||||
54
ts/util/isVoiceMessagePlayed.std.ts
Normal file
54
ts/util/isVoiceMessagePlayed.std.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReadonlyMessageAttributesType } from '../model-types.d.ts';
|
||||
import { isIncoming, isOutgoing } from '../messages/helpers.std.js';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus.std.js';
|
||||
import {
|
||||
isSent,
|
||||
isViewed,
|
||||
isMessageJustForMe,
|
||||
getHighestSuccessfulRecipientStatus,
|
||||
} from '../messages/MessageSendState.std.js';
|
||||
|
||||
export function isVoiceMessagePlayed(
|
||||
message: Pick<
|
||||
ReadonlyMessageAttributesType,
|
||||
'type' | 'isErased' | 'errors' | 'readStatus' | 'sendStateByConversationId'
|
||||
>,
|
||||
ourConversationId: string | undefined
|
||||
): boolean {
|
||||
if (message.isErased) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.errors != null && message.errors.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isIncoming(message)) {
|
||||
return message.readStatus === ReadStatus.Viewed;
|
||||
}
|
||||
|
||||
if (isOutgoing(message)) {
|
||||
const { sendStateByConversationId = {} } = message;
|
||||
|
||||
if (isMessageJustForMe(sendStateByConversationId, ourConversationId)) {
|
||||
return isSent(
|
||||
getHighestSuccessfulRecipientStatus(
|
||||
sendStateByConversationId,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return isViewed(
|
||||
getHighestSuccessfulRecipientStatus(
|
||||
sendStateByConversationId,
|
||||
ourConversationId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user