Use streams to download attachments directly to disk

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
Scott Nonnenberg 2023-10-30 09:24:28 -07:00 committed by GitHub
parent 2da49456c6
commit 99b2bc304e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2297 additions and 356 deletions

View file

@ -7,20 +7,20 @@ import classNames from 'classnames';
import { getIncrement, getTimerBucket } from '../../util/timer';
export type Props = {
deletedForEveryone?: boolean;
direction?: 'incoming' | 'outgoing';
expirationLength: number;
expirationTimestamp?: number;
isOutlineOnlyBubble?: boolean;
withImageNoCaption?: boolean;
withSticker?: boolean;
withTapToViewExpired?: boolean;
};
export function ExpireTimer({
deletedForEveryone,
direction,
expirationLength,
expirationTimestamp,
isOutlineOnlyBubble,
withImageNoCaption,
withSticker,
withTapToViewExpired,
@ -44,7 +44,7 @@ export function ExpireTimer({
'module-expire-timer',
`module-expire-timer--${bucket}`,
direction ? `module-expire-timer--${direction}` : null,
deletedForEveryone ? 'module-expire-timer--deleted-for-everyone' : null,
isOutlineOnlyBubble ? 'module-expire-timer--outline-only-bubble' : null,
withTapToViewExpired
? `module-expire-timer--${direction}-with-tap-to-view-expired`
: null,

View file

@ -75,13 +75,16 @@ function getCurves({
curveTopRight = CurveType.Normal;
}
if (shouldCollapseBelow && direction === 'incoming') {
if (withContentBelow) {
curveBottomLeft = CurveType.None;
curveBottomRight = CurveType.None;
} else if (shouldCollapseBelow && direction === 'incoming') {
curveBottomLeft = CurveType.Tiny;
curveBottomRight = CurveType.None;
} else if (shouldCollapseBelow && direction === 'outgoing') {
curveBottomLeft = CurveType.None;
curveBottomRight = CurveType.Tiny;
} else if (!withContentBelow) {
} else {
curveBottomLeft = CurveType.Normal;
curveBottomRight = CurveType.Normal;
}

View file

@ -284,6 +284,7 @@ export type PropsData = {
reactions?: ReactionViewerProps['reactions'];
deletedForEveryone?: boolean;
attachmentDroppedDueToSize?: boolean;
canDeleteForEveryone: boolean;
isBlocked: boolean;
@ -565,6 +566,7 @@ export class Message extends React.PureComponent<Props, State> {
private getMetadataPlacement(
{
attachments,
attachmentDroppedDueToSize,
deletedForEveryone,
direction,
expirationLength,
@ -599,12 +601,16 @@ export class Message extends React.PureComponent<Props, State> {
return MetadataPlacement.Bottom;
}
if (!text && !deletedForEveryone) {
if (!text && !deletedForEveryone && !attachmentDroppedDueToSize) {
return isAudio(attachments)
? MetadataPlacement.RenderedByMessageAudioComponent
: MetadataPlacement.Bottom;
}
if (!text && attachmentDroppedDueToSize) {
return MetadataPlacement.InlineWithText;
}
if (this.canRenderStickerLikeEmoji()) {
return MetadataPlacement.Bottom;
}
@ -796,6 +802,7 @@ export class Message extends React.PureComponent<Props, State> {
}
const {
attachmentDroppedDueToSize,
deletedForEveryone,
direction,
expirationLength,
@ -822,11 +829,14 @@ export class Message extends React.PureComponent<Props, State> {
direction={direction}
expirationLength={expirationLength}
expirationTimestamp={expirationTimestamp}
hasText={Boolean(text)}
hasText={Boolean(text || attachmentDroppedDueToSize)}
i18n={i18n}
id={id}
isEditedMessage={isEditedMessage}
isInline={isInline}
isOutlineOnlyBubble={
deletedForEveryone || (attachmentDroppedDueToSize && !text)
}
isShowingImage={this.isShowingImage()}
isSticker={isStickerLike}
isTapToViewExpired={isTapToViewExpired}
@ -878,6 +888,7 @@ export class Message extends React.PureComponent<Props, State> {
public renderAttachment(): JSX.Element | null {
const {
attachments,
attachmentDroppedDueToSize,
conversationId,
direction,
expirationLength,
@ -912,7 +923,7 @@ export class Message extends React.PureComponent<Props, State> {
const firstAttachment = attachments[0];
// For attachments which aren't full-frame
const withContentBelow = Boolean(text);
const withContentBelow = Boolean(text || attachmentDroppedDueToSize);
const withContentAbove = Boolean(quote) || this.shouldRenderAuthor();
const displayImage = canDisplayImage(attachments);
@ -1274,6 +1285,62 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderAttachmentTooBig(): JSX.Element | null {
const {
attachments,
attachmentDroppedDueToSize,
direction,
i18n,
quote,
shouldCollapseAbove,
shouldCollapseBelow,
text,
} = this.props;
const { metadataWidth } = this.state;
if (!attachmentDroppedDueToSize) {
return null;
}
const labelText = attachments?.length
? i18n('icu:message--attachmentTooBig--multiple')
: i18n('icu:message--attachmentTooBig--one');
const isContentAbove = quote || attachments?.length;
const isContentBelow = Boolean(text);
const willCollapseAbove = shouldCollapseAbove && !isContentAbove;
const willCollapseBelow = shouldCollapseBelow && !isContentBelow;
const maybeSpacer = text
? undefined
: this.getMetadataPlacement() === MetadataPlacement.InlineWithText && (
<MessageTextMetadataSpacer metadataWidth={metadataWidth} />
);
return (
<div
className={classNames(
'module-message__attachment-too-big',
isContentAbove
? 'module-message__attachment-too-big--content-above'
: null,
isContentBelow
? 'module-message__attachment-too-big--content-below'
: null,
willCollapseAbove
? `module-message__attachment-too-big--collapse-above--${direction}`
: null,
willCollapseBelow
? `module-message__attachment-too-big--collapse-below--${direction}`
: null
)}
>
{labelText}
{maybeSpacer}
</div>
);
}
public renderGiftBadge(): JSX.Element | null {
const { conversationTitle, direction, getPreferredBadge, giftBadge, i18n } =
this.props;
@ -1757,6 +1824,19 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private getContents(): string | undefined {
const { deletedForEveryone, direction, i18n, status, text } = this.props;
if (deletedForEveryone) {
return i18n('icu:message--deletedForEveryone');
}
if (direction === 'incoming' && status === 'error') {
return i18n('icu:incomingError');
}
return text;
}
public renderText(): JSX.Element | null {
const {
bodyRanges,
@ -1772,17 +1852,12 @@ export class Message extends React.PureComponent<Props, State> {
showConversation,
showSpoiler,
status,
text,
textAttachment,
} = this.props;
const { metadataWidth } = this.state;
// eslint-disable-next-line no-nested-ternary
const contents = deletedForEveryone
? i18n('icu:message--deletedForEveryone')
: direction === 'incoming' && status === 'error'
? i18n('icu:incomingError')
: text;
const contents = this.getContents();
if (!contents) {
return null;
@ -2296,7 +2371,7 @@ export class Message extends React.PureComponent<Props, State> {
}
public renderContents(): JSX.Element | null {
const { giftBadge, isTapToView, deletedForEveryone } = this.props;
const { deletedForEveryone, giftBadge, isTapToView } = this.props;
if (deletedForEveryone) {
return (
@ -2326,6 +2401,7 @@ export class Message extends React.PureComponent<Props, State> {
{this.renderStoryReplyContext()}
{this.renderAttachment()}
{this.renderPreview()}
{this.renderAttachmentTooBig()}
{this.renderPayment()}
{this.renderEmbeddedContact()}
{this.renderText()}
@ -2534,6 +2610,7 @@ export class Message extends React.PureComponent<Props, State> {
public renderContainer(): JSX.Element {
const {
attachments,
attachmentDroppedDueToSize,
conversationColor,
customColor,
deletedForEveryone,
@ -2597,7 +2674,12 @@ export class Message extends React.PureComponent<Props, State> {
const containerStyles = {
width: shouldUseWidth ? width : undefined,
};
if (!isStickerLike && !deletedForEveryone && direction === 'outgoing') {
if (
!isStickerLike &&
!deletedForEveryone &&
!(attachmentDroppedDueToSize && !text) &&
direction === 'outgoing'
) {
Object.assign(containerStyles, getCustomColorStyle(customColor));
}

View file

@ -28,6 +28,7 @@ type PropsType = {
id: string;
isEditedMessage?: boolean;
isInline?: boolean;
isOutlineOnlyBubble?: boolean;
isShowingImage: boolean;
isSticker?: boolean;
isTapToViewExpired?: boolean;
@ -55,6 +56,7 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
i18n,
id,
isEditedMessage,
isOutlineOnlyBubble,
isInline,
isShowingImage,
isSticker,
@ -136,8 +138,8 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
className={classNames({
'module-message__metadata__date': true,
'module-message__metadata__date--with-sticker': isSticker,
'module-message__metadata__date--deleted-for-everyone':
deletedForEveryone,
'module-message__metadata__date--outline-only-bubble':
isOutlineOnlyBubble,
[`module-message__metadata__date--${direction}`]: !isSticker,
'module-message__metadata__date--with-image-no-caption':
withImageNoCaption,
@ -149,9 +151,9 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
} else {
timestampNode = (
<MessageTimestamp
deletedForEveryone={deletedForEveryone}
direction={metadataDirection}
i18n={i18n}
isOutlineOnlyBubble={isOutlineOnlyBubble}
module="module-message__metadata__date"
timestamp={timestamp}
withImageNoCaption={withImageNoCaption}
@ -195,7 +197,7 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
'module-message__metadata',
isInline && 'module-message__metadata--inline',
withImageNoCaption && 'module-message__metadata--with-image-no-caption',
deletedForEveryone && 'module-message__metadata--deleted-for-everyone'
isOutlineOnlyBubble && 'module-message__metadata--outline-only-bubble'
);
const children = (
<>
@ -212,7 +214,7 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
{expirationLength ? (
<ExpireTimer
direction={metadataDirection}
deletedForEveryone={deletedForEveryone}
isOutlineOnlyBubble={isOutlineOnlyBubble}
expirationLength={expirationLength}
expirationTimestamp={expirationTimestamp}
withImageNoCaption={withImageNoCaption}
@ -240,8 +242,8 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
withImageNoCaption
? 'module-message__metadata__status-icon--with-image-no-caption'
: null,
deletedForEveryone
? 'module-message__metadata__status-icon--deleted-for-everyone'
isOutlineOnlyBubble
? 'module-message__metadata__status-icon--outline-only-bubble'
: null,
isTapToViewExpired
? 'module-message__metadata__status-icon--with-tap-to-view-expired'

View file

@ -12,9 +12,9 @@ import { Time } from '../Time';
import { useNowThatUpdatesEveryMinute } from '../../hooks/useNowThatUpdatesEveryMinute';
export type Props = {
deletedForEveryone?: boolean;
direction?: 'incoming' | 'outgoing';
i18n: LocalizerType;
isOutlineOnlyBubble?: boolean;
isRelativeTime?: boolean;
module?: string;
timestamp: number;
@ -24,10 +24,10 @@ export type Props = {
};
export function MessageTimestamp({
deletedForEveryone,
direction,
i18n,
isRelativeTime,
isOutlineOnlyBubble,
module,
timestamp,
withImageNoCaption,
@ -47,7 +47,7 @@ export function MessageTimestamp({
: null,
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
withSticker ? `${moduleName}--with-sticker` : null,
deletedForEveryone ? `${moduleName}--deleted-for-everyone` : null
isOutlineOnlyBubble ? `${moduleName}--ouline-only-bubble` : null
)}
timestamp={timestamp}
>

View file

@ -244,6 +244,7 @@ const renderAudioAttachment: Props['renderAudioAttachment'] = props => (
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
attachments: overrideProps.attachments,
attachmentDroppedDueToSize: overrideProps.attachmentDroppedDueToSize || false,
author: overrideProps.author || getDefaultConversation(),
bodyRanges: overrideProps.bodyRanges,
canCopy: true,
@ -835,6 +836,25 @@ CanDeleteForEveryone.args = {
direction: 'outgoing',
};
export function AttachmentTooBig(): JSX.Element {
const propsSent = createProps({
conversationType: 'direct',
attachmentDroppedDueToSize: true,
});
return <>{renderBothDirections(propsSent)}</>;
}
export function AttachmentTooBigWithText(): JSX.Element {
const propsSent = createProps({
conversationType: 'direct',
attachmentDroppedDueToSize: true,
text: 'Check out this file!',
});
return <>{renderBothDirections(propsSent)}</>;
}
export const Error = Template.bind({});
Error.args = {
status: 'error',
@ -1233,6 +1253,51 @@ MultipleImages5.args = {
status: 'sent',
};
export const MultipleImagesWithOneTooBig = Template.bind({});
MultipleImagesWithOneTooBig.args = {
attachments: [
fakeAttachment({
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
fakeAttachment({
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
],
attachmentDroppedDueToSize: true,
status: 'sent',
};
export const MultipleImagesWithBodyTextOneTooBig = Template.bind({});
MultipleImagesWithBodyTextOneTooBig.args = {
attachments: [
fakeAttachment({
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
fakeAttachment({
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
],
attachmentDroppedDueToSize: true,
text: 'Hey, check out these images!',
status: 'sent',
};
export const ImageWithCaption = Template.bind({});
ImageWithCaption.args = {
attachments: [
@ -1968,6 +2033,7 @@ PaymentNotification.args = {
function MultiSelectMessage() {
const [selected, setSelected] = React.useState(false);
return (
<TimelineMessage
{...createProps({