Allow manually retrying attachment downloads

This commit is contained in:
Fedor Indutny 2022-05-23 16:07:41 -07:00 committed by GitHub
parent 59b45399e4
commit dfc310805a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 265 additions and 58 deletions

View file

@ -919,6 +919,10 @@
"message": "Downloading", "message": "Downloading",
"description": "Shown in the message bubble while a long message attachment is being downloaded" "description": "Shown in the message bubble while a long message attachment is being downloaded"
}, },
"downloadFullMessage": {
"message": "Download Full Message",
"description": "Shown in the message bubble while a long message attachment is not downloaded"
},
"downloadAttachment": { "downloadAttachment": {
"message": "Download Attachment", "message": "Download Attachment",
"description": "Shown in a message's triple-dot menu if there isn't room for a dedicated download button" "description": "Shown in a message's triple-dot menu if there isn't room for a dedicated download button"

View file

@ -6,6 +6,7 @@
font-weight: bold; font-weight: bold;
} }
&__download-body,
&__read-more { &__read-more {
@include button-reset; @include button-reset;
font-weight: bold; font-weight: bold;

View file

@ -19,6 +19,7 @@ import {
IMAGE_PNG, IMAGE_PNG,
IMAGE_WEBP, IMAGE_WEBP,
VIDEO_MP4, VIDEO_MP4,
LONG_MESSAGE,
stringToMIMEType, stringToMIMEType,
IMAGE_GIF, IMAGE_GIF,
} from '../../types/MIME'; } from '../../types/MIME';
@ -205,7 +206,11 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
status: overrideProps.status || 'sent', status: overrideProps.status || 'sent',
text: overrideProps.text || text('text', ''), text: overrideProps.text || text('text', ''),
textDirection: overrideProps.textDirection || TextDirection.Default, textDirection: overrideProps.textDirection || TextDirection.Default,
textPending: boolean('textPending', overrideProps.textPending || false), textAttachment: overrideProps.textAttachment || {
contentType: LONG_MESSAGE,
size: 123,
pending: boolean('textPending', false),
},
theme: ThemeType.light, theme: ThemeType.light,
timestamp: number('timestamp', overrideProps.timestamp || Date.now()), timestamp: number('timestamp', overrideProps.timestamp || Date.now()),
}); });
@ -420,7 +425,27 @@ story.add('Will expire but still sending', () => {
story.add('Pending', () => { story.add('Pending', () => {
const props = createProps({ const props = createProps({
text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.', text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
textPending: true, textAttachment: {
contentType: LONG_MESSAGE,
size: 123,
pending: true,
},
});
return renderBothDirections(props);
});
story.add('Long body can be downloaded', () => {
const props = createProps({
text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
textAttachment: {
contentType: LONG_MESSAGE,
size: 123,
pending: false,
error: true,
digest: 'abc',
key: 'def',
},
}); });
return renderBothDirections(props); return renderBothDirections(props);

View file

@ -201,7 +201,7 @@ export type PropsData = {
displayLimit?: number; displayLimit?: number;
text?: string; text?: string;
textDirection: TextDirection; textDirection: TextDirection;
textPending?: boolean; textAttachment?: AttachmentType;
isSticker?: boolean; isSticker?: boolean;
isSelected?: boolean; isSelected?: boolean;
isSelectedCounter?: number; isSelectedCounter?: number;
@ -818,7 +818,7 @@ export class Message extends React.PureComponent<Props, State> {
status, status,
i18n, i18n,
text, text,
textPending, textAttachment,
timestamp, timestamp,
id, id,
showMessageDetail, showMessageDetail,
@ -842,7 +842,7 @@ export class Message extends React.PureComponent<Props, State> {
onWidthMeasured={isInline ? this.updateMetadataWidth : undefined} onWidthMeasured={isInline ? this.updateMetadataWidth : undefined}
showMessageDetail={showMessageDetail} showMessageDetail={showMessageDetail}
status={status} status={status}
textPending={textPending} textPending={textAttachment?.pending}
timestamp={timestamp} timestamp={timestamp}
/> />
); );
@ -903,7 +903,7 @@ export class Message extends React.PureComponent<Props, State> {
shouldCollapseBelow, shouldCollapseBelow,
status, status,
text, text,
textPending, textAttachment,
theme, theme,
timestamp, timestamp,
} = this.props; } = this.props;
@ -1031,7 +1031,7 @@ export class Message extends React.PureComponent<Props, State> {
played, played,
showMessageDetail, showMessageDetail,
status, status,
textPending, textPending: textAttachment?.pending,
timestamp, timestamp,
kickOffAttachmentDownload() { kickOffAttachmentDownload() {
@ -1206,6 +1206,7 @@ export class Message extends React.PureComponent<Props, State> {
width={72} width={72}
url={first.image.url} url={first.image.url}
attachment={first.image} attachment={first.image}
blurHash={first.image.blurHash}
onError={this.handleImageError} onError={this.handleImageError}
i18n={i18n} i18n={i18n}
onClick={onPreviewImageClick} onClick={onPreviewImageClick}
@ -1699,10 +1700,11 @@ export class Message extends React.PureComponent<Props, State> {
id, id,
messageExpanded, messageExpanded,
openConversation, openConversation,
kickOffAttachmentDownload,
status, status,
text, text,
textDirection, textDirection,
textPending, textAttachment,
} = this.props; } = this.props;
const { metadataWidth } = this.state; const { metadataWidth } = this.state;
const isRTL = textDirection === TextDirection.RightToLeft; const isRTL = textDirection === TextDirection.RightToLeft;
@ -1741,8 +1743,17 @@ export class Message extends React.PureComponent<Props, State> {
id={id} id={id}
messageExpanded={messageExpanded} messageExpanded={messageExpanded}
openConversation={openConversation} openConversation={openConversation}
kickOffBodyDownload={() => {
if (!textAttachment) {
return;
}
kickOffAttachmentDownload({
attachment: textAttachment,
messageId: id,
});
}}
text={contents || ''} text={contents || ''}
textPending={textPending} textAttachment={textAttachment}
/> />
{!isRTL && {!isRTL &&
this.getMetadataPlacement() === MetadataPlacement.InlineWithText && ( this.getMetadataPlacement() === MetadataPlacement.InlineWithText && (

View file

@ -25,7 +25,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
direction: 'incoming', direction: 'incoming',
i18n, i18n,
text: text('text', overrideProps.text || ''), text: text('text', overrideProps.text || ''),
textPending: boolean('textPending', overrideProps.textPending || false), textAttachment: overrideProps.textAttachment || {
pending: boolean('textPending', false),
},
}); });
story.add('Links Enabled', () => { story.add('Links Enabled', () => {
@ -91,7 +93,9 @@ story.add('Jumbomoji Disabled by Text', () => {
story.add('Text Pending', () => { story.add('Text Pending', () => {
const props = createProps({ const props = createProps({
text: 'Check out https://www.signal.org', text: 'Check out https://www.signal.org',
textPending: true, textAttachment: {
pending: true,
},
}); });
return <MessageBody {...props} />; return <MessageBody {...props} />;

View file

@ -4,6 +4,8 @@
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import React from 'react'; import React from 'react';
import type { AttachmentType } from '../../types/Attachment';
import { canBeDownloaded } from '../../types/Attachment';
import type { SizeClassType } from '../emoji/lib'; import type { SizeClassType } from '../emoji/lib';
import { getSizeClass } from '../emoji/lib'; import { getSizeClass } from '../emoji/lib';
import { AtMentionify } from './AtMentionify'; import { AtMentionify } from './AtMentionify';
@ -25,7 +27,7 @@ type OpenConversationActionType = (
export type Props = { export type Props = {
direction?: 'incoming' | 'outgoing'; direction?: 'incoming' | 'outgoing';
text: string; text: string;
textPending?: boolean; textAttachment?: Pick<AttachmentType, 'pending' | 'digest' | 'key'>;
/** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */ /** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */
disableJumbomoji?: boolean; disableJumbomoji?: boolean;
/** If set, links will be left alone instead of turned into clickable `<a>` tags. */ /** If set, links will be left alone instead of turned into clickable `<a>` tags. */
@ -34,6 +36,7 @@ export type Props = {
bodyRanges?: BodyRangesType; bodyRanges?: BodyRangesType;
onIncreaseTextLength?: () => unknown; onIncreaseTextLength?: () => unknown;
openConversation?: OpenConversationActionType; openConversation?: OpenConversationActionType;
kickOffBodyDownload?: () => void;
}; };
const renderEmoji = ({ const renderEmoji = ({
@ -71,10 +74,12 @@ export function MessageBody({
onIncreaseTextLength, onIncreaseTextLength,
openConversation, openConversation,
text, text,
textPending, textAttachment,
kickOffBodyDownload,
}: Props): JSX.Element { }: Props): JSX.Element {
const hasReadMore = Boolean(onIncreaseTextLength); const hasReadMore = Boolean(onIncreaseTextLength);
const textWithSuffix = textPending || hasReadMore ? `${text}...` : text; const textWithSuffix =
textAttachment?.pending || hasReadMore ? `${text}...` : text;
const sizeClass = disableJumbomoji ? undefined : getSizeClass(text); const sizeClass = disableJumbomoji ? undefined : getSizeClass(text);
const processedText = AtMentionify.preprocessMentions( const processedText = AtMentionify.preprocessMentions(
@ -103,6 +108,40 @@ export function MessageBody({
); );
}; };
let pendingContent: React.ReactNode;
if (hasReadMore) {
pendingContent = null;
} else if (textAttachment?.pending) {
pendingContent = (
<span className="MessageBody__highlight"> {i18n('downloading')}</span>
);
} else if (
textAttachment &&
canBeDownloaded(textAttachment) &&
kickOffBodyDownload
) {
pendingContent = (
<span>
{' '}
<button
className="MessageBody__download-body"
onClick={() => {
kickOffBodyDownload();
}}
onKeyDown={(ev: KeyboardEvent) => {
if (ev.key === 'Space' || ev.key === 'Enter') {
kickOffBodyDownload();
}
}}
tabIndex={0}
type="button"
>
{i18n('downloadFullMessage')}
</button>
</span>
);
}
return ( return (
<span> <span>
{disableLinks ? ( {disableLinks ? (
@ -127,9 +166,7 @@ export function MessageBody({
}} }}
/> />
)} )}
{textPending ? ( {pendingContent}
<span className="MessageBody__highlight"> {i18n('downloading')}</span>
) : null}
{onIncreaseTextLength ? ( {onIncreaseTextLength ? (
<button <button
className="MessageBody__read-more" className="MessageBody__read-more"

View file

@ -11,11 +11,12 @@ export type Props = Pick<
MessageBodyPropsType, MessageBodyPropsType,
| 'direction' | 'direction'
| 'text' | 'text'
| 'textPending' | 'textAttachment'
| 'disableLinks' | 'disableLinks'
| 'i18n' | 'i18n'
| 'bodyRanges' | 'bodyRanges'
| 'openConversation' | 'openConversation'
| 'kickOffBodyDownload'
> & { > & {
id: string; id: string;
displayLimit?: number; displayLimit?: number;
@ -39,8 +40,9 @@ export function MessageBodyReadMore({
id, id,
messageExpanded, messageExpanded,
openConversation, openConversation,
kickOffBodyDownload,
text, text,
textPending, textAttachment,
}: Props): JSX.Element { }: Props): JSX.Element {
const maxLength = displayLimit || INITIAL_LENGTH; const maxLength = displayLimit || INITIAL_LENGTH;
@ -64,8 +66,9 @@ export function MessageBodyReadMore({
i18n={i18n} i18n={i18n}
onIncreaseTextLength={onIncreaseTextLength} onIncreaseTextLength={onIncreaseTextLength}
openConversation={openConversation} openConversation={openConversation}
kickOffBodyDownload={kickOffBodyDownload}
text={slicedText} text={slicedText}
textPending={textPending} textAttachment={textAttachment}
/> />
); );
} }

View file

@ -7,6 +7,7 @@ import { v4 as getGuid } from 'uuid';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { strictAssert } from '../util/assert';
import { downloadAttachment } from '../util/downloadAttachment'; import { downloadAttachment } from '../util/downloadAttachment';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import type { import type {
@ -16,11 +17,13 @@ import type {
import type { MessageModel } from '../models/messages'; import type { MessageModel } from '../models/messages';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import * as Errors from '../types/errors';
import type { LoggerType } from '../types/Logging'; import type { LoggerType } from '../types/Logging';
import * as log from '../logging/log'; import * as log from '../logging/log';
const { const {
getMessageById, getMessageById,
getAttachmentDownloadJobById,
getNextAttachmentDownloadJobs, getNextAttachmentDownloadJobs,
removeAttachmentDownloadJob, removeAttachmentDownloadJob,
resetAttachmentDownloadPending, resetAttachmentDownloadPending,
@ -91,6 +94,32 @@ export async function addJob(
throw new Error('attachments_download/addJob: index must be a number'); throw new Error('attachments_download/addJob: index must be a number');
} }
if (attachment.downloadJobId) {
let existingJob = await getAttachmentDownloadJobById(
attachment.downloadJobId
);
if (existingJob) {
// Reset job attempts through user's explicit action
existingJob = { ...existingJob, attempts: 0 };
if (_activeAttachmentDownloadJobs[existingJob.id]) {
logger.info(
`attachment_downloads/addJob: ${existingJob.id} already running`
);
} else {
logger.info(
`attachment_downloads/addJob: restarting existing job ${existingJob.id}`
);
_activeAttachmentDownloadJobs[existingJob.id] = _runJob(existingJob);
}
return {
...attachment,
pending: true,
};
}
}
const id = getGuid(); const id = getGuid();
const timestamp = Date.now(); const timestamp = Date.now();
const toSave: AttachmentDownloadJobType = { const toSave: AttachmentDownloadJobType = {
@ -175,7 +204,7 @@ async function _maybeStartJob(): Promise<void> {
async function _runJob(job?: AttachmentDownloadJobType): Promise<void> { async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
if (!job) { if (!job) {
log.warn('_runJob: Job was missing!'); log.warn('attachment_downloads/_runJob: Job was missing!');
return; return;
} }
@ -189,45 +218,60 @@ async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
); );
} }
logger.info(`attachment_downloads/_runJob for job id ${id}`); logger.info(`attachment_downloads/_runJob(${id}): starting`);
const found =
window.MessageController.getById(messageId) ||
(await getMessageById(messageId));
if (!found) {
logger.error('_runJob: Source message not found, deleting job');
await _finishJob(null, id);
return;
}
message = window.MessageController.register(found.id, found);
const pending = true; const pending = true;
await setAttachmentDownloadJobPending(id, pending); await setAttachmentDownloadJobPending(id, pending);
message = window.MessageController.getById(messageId);
if (!message) {
const messageAttributes = await getMessageById(messageId);
if (!messageAttributes) {
logger.error(
`attachment_downloads/_runJob(${id}): ` +
'Source message not found, deleting job'
);
await _finishJob(null, id);
return;
}
strictAssert(messageId === messageAttributes.id, 'message id mismatch');
message = window.MessageController.register(messageId, messageAttributes);
}
await _addAttachmentToMessage(
message,
{ ...attachment, pending: true },
{ type, index }
);
const downloaded = await downloadAttachment(attachment); const downloaded = await downloadAttachment(attachment);
if (!downloaded) { if (!downloaded) {
logger.warn( logger.warn(
`_runJob: Got 404 from server for CDN ${ `attachment_downloads/_runJob(${id}): Got 404 from server for CDN ${
attachment.cdnNumber attachment.cdnNumber
}, marking attachment ${ }, marking attachment ${
attachment.cdnId || attachment.cdnKey attachment.cdnId || attachment.cdnKey
} from message ${message.idForLogging()} as permanent error` } from message ${message.idForLogging()} as permanent error`
); );
await _finishJob(message, id);
await _addAttachmentToMessage( await _addAttachmentToMessage(
message, message,
_markAttachmentAsError(attachment), _markAttachmentAsPermanentError(attachment),
{ type, index } { type, index }
); );
await _finishJob(message, id);
return; return;
} }
const upgradedAttachment = const upgradedAttachment =
await window.Signal.Migrations.processNewAttachment(downloaded); await window.Signal.Migrations.processNewAttachment(downloaded);
await _addAttachmentToMessage(message, upgradedAttachment, { type, index }); await _addAttachmentToMessage(message, omit(upgradedAttachment, 'error'), {
type,
index,
});
await _finishJob(message, id); await _finishJob(message, id);
} catch (error) { } catch (error) {
@ -236,25 +280,43 @@ async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
if (currentAttempt >= 3) { if (currentAttempt >= 3) {
logger.error( logger.error(
`_runJob: ${currentAttempt} failed attempts, marking attachment ${id} from message ${logId} as permanent error:`, `attachment_downloads/runJob(${id}): ${currentAttempt} failed ` +
error && error.stack ? error.stack : error `attempts, marking attachment from message ${logId} as ` +
'error:',
Errors.toLogFormat(error)
); );
await _finishJob(message, id);
await _addAttachmentToMessage( await _addAttachmentToMessage(
message, message,
_markAttachmentAsError(attachment), _markAttachmentAsTransientError(attachment),
{ type, index } { type, index }
); );
await _finishJob(message, id);
return; return;
} }
logger.error( logger.error(
`_runJob: Failed to download attachment type ${type} for message ${logId}, attempt ${currentAttempt}:`, `attachment_downloads/_runJob(${id}): Failed to download attachment ` +
error && error.stack ? error.stack : error `type ${type} for message ${logId}, attempt ${currentAttempt}:`,
Errors.toLogFormat(error)
); );
// Remove `pending` flag from the attachment.
await _addAttachmentToMessage(
message,
{
...attachment,
downloadJobId: id,
},
{ type, index }
);
if (message) {
await saveMessage(message.attributes, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
}
const failedJob = { const failedJob = {
...job, ...job,
pending: 0, pending: 0,
@ -289,13 +351,21 @@ function getActiveJobCount(): number {
return Object.keys(_activeAttachmentDownloadJobs).length; return Object.keys(_activeAttachmentDownloadJobs).length;
} }
function _markAttachmentAsError(attachment: AttachmentType): AttachmentType { function _markAttachmentAsPermanentError(
attachment: AttachmentType
): AttachmentType {
return { return {
...omit(attachment, ['key', 'digest', 'id']), ...omit(attachment, ['key', 'digest', 'id']),
error: true, error: true,
}; };
} }
function _markAttachmentAsTransientError(
attachment: AttachmentType
): AttachmentType {
return { ...attachment, error: true };
}
async function _addAttachmentToMessage( async function _addAttachmentToMessage(
message: MessageModel | null | undefined, message: MessageModel | null | undefined,
attachment: AttachmentType, attachment: AttachmentType,
@ -308,13 +378,21 @@ async function _addAttachmentToMessage(
const logPrefix = `${message.idForLogging()} (type: ${type}, index: ${index})`; const logPrefix = `${message.idForLogging()} (type: ${type}, index: ${index})`;
if (type === 'long-message') { if (type === 'long-message') {
// Attachment wasn't downloaded yet.
if (!attachment.path) {
message.set({
bodyAttachment: attachment,
});
return;
}
try { try {
const { data } = await window.Signal.Migrations.loadAttachmentData( const { data } = await window.Signal.Migrations.loadAttachmentData(
attachment attachment
); );
message.set({ message.set({
body: attachment.error ? message.get('body') : Bytes.toString(data), body: Bytes.toString(data),
bodyPending: false, bodyAttachment: undefined,
}); });
} finally { } finally {
if (attachment.path) { if (attachment.path) {

2
ts/model-types.d.ts vendored
View file

@ -116,7 +116,7 @@ export type MessageReactionType = {
}; };
export type MessageAttributesType = { export type MessageAttributesType = {
bodyPending?: boolean; bodyAttachment?: AttachmentType;
bodyRanges?: BodyRangesType; bodyRanges?: BodyRangesType;
callHistoryDetails?: CallHistoryDetailsFromDiskType; callHistoryDetails?: CallHistoryDetailsFromDiskType;
changedId?: string; changedId?: string;

View file

@ -3833,6 +3833,11 @@ export class ConversationModel extends window.Backbone
contentType, contentType,
width, width,
height, height,
blurHash: await window.imageToBlurHash(
new Blob([data], {
type: IMAGE_JPEG,
})
),
}, },
}; };

View file

@ -254,6 +254,7 @@ const dataInterface: ClientInterface = {
removeUnprocessed, removeUnprocessed,
removeAllUnprocessed, removeAllUnprocessed,
getAttachmentDownloadJobById,
getNextAttachmentDownloadJobs, getNextAttachmentDownloadJobs,
saveAttachmentDownloadJob, saveAttachmentDownloadJob,
resetAttachmentDownloadPending, resetAttachmentDownloadPending,
@ -1476,6 +1477,9 @@ async function removeAllUnprocessed() {
// Attachment downloads // Attachment downloads
async function getAttachmentDownloadJobById(id: string) {
return channels.getAttachmentDownloadJobById(id);
}
async function getNextAttachmentDownloadJobs( async function getNextAttachmentDownloadJobs(
limit?: number, limit?: number,
options?: { timestamp?: number } options?: { timestamp?: number }

View file

@ -496,6 +496,9 @@ export type DataInterface = {
removeUnprocessed: (id: string | Array<string>) => Promise<void>; removeUnprocessed: (id: string | Array<string>) => Promise<void>;
removeAllUnprocessed: () => Promise<void>; removeAllUnprocessed: () => Promise<void>;
getAttachmentDownloadJobById: (
id: string
) => Promise<AttachmentDownloadJobType | undefined>;
getNextAttachmentDownloadJobs: ( getNextAttachmentDownloadJobs: (
limit?: number, limit?: number,
options?: { timestamp?: number } options?: { timestamp?: number }

View file

@ -251,6 +251,7 @@ const dataInterface: ServerInterface = {
removeUnprocessed, removeUnprocessed,
removeAllUnprocessed, removeAllUnprocessed,
getAttachmentDownloadJobById,
getNextAttachmentDownloadJobs, getNextAttachmentDownloadJobs,
saveAttachmentDownloadJob, saveAttachmentDownloadJob,
resetAttachmentDownloadPending, resetAttachmentDownloadPending,
@ -3299,6 +3300,11 @@ async function removeAllUnprocessed(): Promise<void> {
// Attachment Downloads // Attachment Downloads
const ATTACHMENT_DOWNLOADS_TABLE = 'attachment_downloads'; const ATTACHMENT_DOWNLOADS_TABLE = 'attachment_downloads';
async function getAttachmentDownloadJobById(
id: string
): Promise<AttachmentDownloadJobType | undefined> {
return getById(getInstance(), ATTACHMENT_DOWNLOADS_TABLE, id);
}
async function getNextAttachmentDownloadJobs( async function getNextAttachmentDownloadJobs(
limit?: number, limit?: number,
options: { timestamp?: number } = {} options: { timestamp?: number } = {}

View file

@ -39,7 +39,7 @@ import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import { CallMode } from '../../types/Calling'; import { CallMode } from '../../types/Calling';
import { SignalService as Proto } from '../../protobuf'; import { SignalService as Proto } from '../../protobuf';
import type { AttachmentType } from '../../types/Attachment'; import type { AttachmentType } from '../../types/Attachment';
import { isVoiceMessage } from '../../types/Attachment'; import { isVoiceMessage, canBeDownloaded } from '../../types/Attachment';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import type { CallingNotificationType } from '../../util/callingNotification'; import type { CallingNotificationType } from '../../util/callingNotification';
@ -265,7 +265,7 @@ export const getAttachmentsForMessage = createSelectorCreator(memoizeByRoot)(
} }
return attachments return attachments
.filter(attachment => !attachment.error) .filter(attachment => !attachment.error || canBeDownloaded(attachment))
.map(attachment => getPropsForAttachment(attachment)) .map(attachment => getPropsForAttachment(attachment))
.filter(isNotNil); .filter(isNotNil);
} }
@ -358,7 +358,7 @@ export const getPreviewsForMessage = createSelectorCreator(memoizeByRoot)(
...preview, ...preview,
isStickerPack: isStickerPack(preview.url), isStickerPack: isStickerPack(preview.url),
domain: getDomain(preview.url), domain: getDomain(preview.url),
image: preview.image ? getPropsForAttachment(preview.image) : null, image: preview.image ? getPropsForAttachment(preview.image) : undefined,
})); }));
} }
); );
@ -594,7 +594,6 @@ type ShallowPropsType = Pick<
| 'status' | 'status'
| 'text' | 'text'
| 'textDirection' | 'textDirection'
| 'textPending'
| 'timestamp' | 'timestamp'
>; >;
@ -682,7 +681,6 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
status: getMessagePropStatus(message, ourConversationId), status: getMessagePropStatus(message, ourConversationId),
text: message.body, text: message.body,
textDirection: getTextDirection(message.body), textDirection: getTextDirection(message.body),
textPending: message.bodyPending,
timestamp: message.sent_at, timestamp: message.sent_at,
}; };
}, },
@ -690,6 +688,14 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
(_: unknown, props: ShallowPropsType) => props (_: unknown, props: ShallowPropsType) => props
); );
function getTextAttachment(
message: MessageWithUIFieldsType
): AttachmentType | undefined {
return (
message.bodyAttachment && getPropsForAttachment(message.bodyAttachment)
);
}
function getTextDirection(body?: string): TextDirection { function getTextDirection(body?: string): TextDirection {
if (!body) { if (!body) {
return TextDirection.None; return TextDirection.None;
@ -727,6 +733,7 @@ export const getPropsForMessage: (
getReactionsForMessage, getReactionsForMessage,
getPropsForQuote, getPropsForQuote,
getPropsForStoryReplyContext, getPropsForStoryReplyContext,
getTextAttachment,
getShallowPropsForMessage, getShallowPropsForMessage,
( (
_, _,
@ -737,6 +744,7 @@ export const getPropsForMessage: (
reactions: PropsData['reactions'], reactions: PropsData['reactions'],
quote: PropsData['quote'], quote: PropsData['quote'],
storyReplyContext: PropsData['storyReplyContext'], storyReplyContext: PropsData['storyReplyContext'],
textAttachment: PropsData['textAttachment'],
shallowProps: ShallowPropsType shallowProps: ShallowPropsType
): Omit<PropsForMessage, 'renderingContext'> => { ): Omit<PropsForMessage, 'renderingContext'> => {
return { return {
@ -747,6 +755,7 @@ export const getPropsForMessage: (
quote, quote,
reactions, reactions,
storyReplyContext, storyReplyContext,
textAttachment,
...shallowProps, ...shallowProps,
}; };
} }
@ -1468,9 +1477,9 @@ export function getPropsForEmbeddedContact(
export function getPropsForAttachment( export function getPropsForAttachment(
attachment: AttachmentType attachment: AttachmentType
): AttachmentType | null { ): AttachmentType | undefined {
if (!attachment) { if (!attachment) {
return null; return undefined;
} }
const { path, pending, size, screenshot, thumbnail } = attachment; const { path, pending, size, screenshot, thumbnail } = attachment;

View file

@ -73,6 +73,10 @@ export type AttachmentType = {
/** Legacy field. Used only for downloading old attachments */ /** Legacy field. Used only for downloading old attachments */
id?: number; id?: number;
/** Removed once we download the attachment */
digest?: string;
key?: string;
}; };
export enum TextAttachmentStyleType { export enum TextAttachmentStyleType {
@ -1018,3 +1022,9 @@ export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => {
} }
return 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ'; return 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ';
}; };
export const canBeDownloaded = (
attachment: Pick<AttachmentType, 'key' | 'digest'>
): boolean => {
return Boolean(attachment.key && attachment.digest);
};

View file

@ -22,7 +22,7 @@ import {
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
type ReturnType = { type ReturnType = {
bodyPending?: boolean; bodyAttachment?: AttachmentType;
attachments: Array<AttachmentType>; attachments: Array<AttachmentType>;
preview: PreviewMessageType; preview: PreviewMessageType;
contact: Array<EmbeddedContactType>; contact: Array<EmbeddedContactType>;
@ -41,7 +41,7 @@ export async function queueAttachmentDownloads(
const idForLogging = getMessageIdForLogging(message); const idForLogging = getMessageIdForLogging(message);
let count = 0; let count = 0;
let bodyPending; let bodyAttachment;
log.info( log.info(
`Queueing ${attachmentsToQueue.length} attachment downloads for message ${idForLogging}` `Queueing ${attachmentsToQueue.length} attachment downloads for message ${idForLogging}`
@ -64,8 +64,15 @@ export async function queueAttachmentDownloads(
if (longMessageAttachments.length > 0) { if (longMessageAttachments.length > 0) {
count += 1; count += 1;
bodyPending = true; [bodyAttachment] = longMessageAttachments;
await AttachmentDownloads.addJob(longMessageAttachments[0], { }
if (!bodyAttachment && message.bodyAttachment) {
count += 1;
bodyAttachment = message.bodyAttachment;
}
if (bodyAttachment) {
await AttachmentDownloads.addJob(bodyAttachment, {
messageId, messageId,
type: 'long-message', type: 'long-message',
index: 0, index: 0,
@ -252,7 +259,7 @@ export async function queueAttachmentDownloads(
} }
return { return {
bodyPending, bodyAttachment,
attachments, attachments,
preview, preview,
contact, contact,