Show attachment download progress, new stop button to cancel
This commit is contained in:
parent
7124ba36cd
commit
5cb6b3f686
78 changed files with 2192 additions and 562 deletions
|
@ -1502,6 +1502,30 @@
|
|||
"messageformat": "Icon showing that this image has a caption",
|
||||
"description": "Used for the icon layered on top of an image in message bubbles"
|
||||
},
|
||||
"icu:imageOpenAlt": {
|
||||
"messageformat": "Open this attachment in a larger view",
|
||||
"description": "Used for the button that overlays all attachments in the timeline"
|
||||
},
|
||||
"icu:startDownload": {
|
||||
"messageformat": "Start download",
|
||||
"description": "Describes a button shown on an an attachment to kick off the download"
|
||||
},
|
||||
"icu:cancelDownload": {
|
||||
"messageformat": "Cancel download",
|
||||
"description": "Describes a button shown on an existing download to stop that in-progress or pending download"
|
||||
},
|
||||
"icu:retryDownload": {
|
||||
"messageformat": "Retry download",
|
||||
"description": "Label for button shown on an existing download to restart a download that was partially completed"
|
||||
},
|
||||
"icu:retryDownloadShort": {
|
||||
"messageformat": "Retry",
|
||||
"description": "Describes a button shown on an existing download to restart a download that was partially completed"
|
||||
},
|
||||
"icu:downloadNItems": {
|
||||
"messageformat": "{count, plural, one {# item} other {# items}}",
|
||||
"description": "Describes a button shown on an existing download to restart a download that was partially completed"
|
||||
},
|
||||
"icu:save": {
|
||||
"messageformat": "Save",
|
||||
"description": "Used on save buttons"
|
||||
|
|
1
images/icons/v3/stop/stop-fill.svg
Normal file
1
images/icons/v3/stop/stop-fill.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.417 3.75c-.92 0-1.667.746-1.667 1.667v9.166c0 .92.746 1.667 1.667 1.667h9.166c.92 0 1.667-.746 1.667-1.667V5.417c0-.92-.746-1.667-1.667-1.667H5.417Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 256 B |
|
@ -2677,9 +2677,9 @@ button.ConversationDetails__action-button {
|
|||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 48px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 50px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-color: variables.$color-black-alpha-70;
|
||||
}
|
||||
|
||||
|
@ -2719,42 +2719,29 @@ button.ConversationDetails__action-button {
|
|||
align-items: center;
|
||||
|
||||
content: 'GIF';
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
@include mixins.font-body-1;
|
||||
color: variables.$color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.module-image__download-pending {
|
||||
position: relative;
|
||||
.module-image__progress-circle-wrapper {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
&--spinner-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
inset-inline-start: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
.ProgressCircle .ProgressCircle__background {
|
||||
stroke: variables.$color-white-alpha-20;
|
||||
}
|
||||
.ProgressCircle .ProgressCircle__fill {
|
||||
stroke: variables.$color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&--spinner {
|
||||
background-color: variables.$color-gray-75;
|
||||
border-radius: 48px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
.module-image__spinner-container {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
.module-image-spinner {
|
||||
&__container {
|
||||
margin-block: 12px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
&__arc {
|
||||
background-color: variables.$color-gray-75;
|
||||
background-color: variables.$color-black-alpha-80;
|
||||
}
|
||||
|
||||
&__circle {
|
||||
|
@ -2763,8 +2750,7 @@ button.ConversationDetails__action-button {
|
|||
|
||||
@include mixins.dark-theme {
|
||||
&__arc {
|
||||
background-color: variables.$color-gray-75;
|
||||
}
|
||||
background-color: variables.$color-black-alpha-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2791,10 +2777,10 @@ button.ConversationDetails__action-button {
|
|||
|
||||
.module-image__border-overlay {
|
||||
@include mixins.button-reset;
|
||||
|
||||
& {
|
||||
width: 100%;
|
||||
cursor: inherit;
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -2806,6 +2792,7 @@ button.ConversationDetails__action-button {
|
|||
|
||||
.module-image__border-overlay--with-click-handler {
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.module-image__border-overlay--with-border {
|
||||
|
@ -2818,24 +2805,6 @@ button.ConversationDetails__action-button {
|
|||
}
|
||||
|
||||
.module-image--gif {
|
||||
&__filesize {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
inset-inline-start: 10px;
|
||||
padding-block: 2px;
|
||||
padding-inline: 8px;
|
||||
|
||||
color: variables.$color-white;
|
||||
background: variables.$color-black-alpha-70;
|
||||
|
||||
/* The height is: 14px + 2x2px from the padding */
|
||||
border-radius: 9px;
|
||||
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
video {
|
||||
cursor: pointer;
|
||||
object-fit: cover;
|
||||
|
@ -2897,22 +2866,46 @@ button.module-image__border-overlay:focus {
|
|||
inset-inline: 0;
|
||||
}
|
||||
|
||||
.module-image__play-overlay__circle {
|
||||
.module-image__overlay-circle {
|
||||
@include mixins.position-absolute-center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: variables.$color-white;
|
||||
border-radius: 24px;
|
||||
@include mixins.button-reset;
|
||||
& {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: variables.$color-black-alpha-80;
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-image__play-overlay__icon {
|
||||
.module-image__play-icon {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/play/play-fill.svg',
|
||||
variables.$color-ultramarine
|
||||
variables.$color-white
|
||||
);
|
||||
}
|
||||
.module-image__stop-icon {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
// Smaller to fit within the spinner
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/stop/stop-fill.svg',
|
||||
variables.$color-white
|
||||
);
|
||||
}
|
||||
.module-image__download-icon {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/arrow/arrow-down.svg',
|
||||
variables.$color-white
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2962,6 +2955,7 @@ button.module-image__border-overlay:focus {
|
|||
// Module: Image Grid
|
||||
|
||||
.module-image-grid {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -2969,6 +2963,43 @@ button.module-image__border-overlay:focus {
|
|||
gap: 1px;
|
||||
}
|
||||
|
||||
.module-image-grid__download-pill {
|
||||
@include mixins.position-absolute-center;
|
||||
@include mixins.button-reset;
|
||||
|
||||
& {
|
||||
background-color: variables.$color-black-alpha-80;
|
||||
color: variables.$color-white;
|
||||
|
||||
height: 44px;
|
||||
border-radius: 44px;
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.module-image-grid__download_pill__icon-wrapper {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-inline-end: -6px;
|
||||
}
|
||||
.module-image-grid__download_pill__download-icon {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/arrow/arrow-down.svg',
|
||||
variables.$color-white
|
||||
);
|
||||
}
|
||||
.module-image-grid__download_pill__text-wrapper {
|
||||
@include mixins.font-body-1;
|
||||
margin-inline-end: 14px;
|
||||
}
|
||||
|
||||
.module-image-grid--one-image {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
|
87
stylesheets/components/AttachmentDetailPill.scss
Normal file
87
stylesheets/components/AttachmentDetailPill.scss
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@use '../mixins';
|
||||
@use '../variables';
|
||||
|
||||
// This needs to go before the top-level class, so it doesn't interfere
|
||||
.AttachmentDetailPill--interactive {
|
||||
@include mixins.button-reset;
|
||||
}
|
||||
|
||||
.AttachmentDetailPill {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
left: 6px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
background-color: variables.$color-black-alpha-80;
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
z-index: variables.$z-index-above-base;
|
||||
|
||||
@include mixins.font-caption;
|
||||
color: variables.$color-white;
|
||||
|
||||
transition: width 400ms ease-out;
|
||||
}
|
||||
|
||||
.AttachmentDetailPill__spinner-wrapper {
|
||||
position: relative;
|
||||
margin: 4px;
|
||||
margin-inline-end: -4px;
|
||||
|
||||
.ProgressCircle .ProgressCircle__background {
|
||||
stroke: variables.$color-white-alpha-20;
|
||||
}
|
||||
.ProgressCircle .ProgressCircle__fill {
|
||||
stroke: variables.$color-white;
|
||||
}
|
||||
|
||||
.module-spinner__circle {
|
||||
background-color: variables.$color-white-alpha-20;
|
||||
}
|
||||
|
||||
.module-spinner__arc {
|
||||
background-color: variables.$color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.AttachmentDetailPill__text-wrapper {
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.AttachmentDetailPill__icon-wrapper {
|
||||
position: relative;
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-end: -11px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.AttachmentDetailPill__stop-icon {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
// Smaller to fit within the spinner
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/stop/stop-fill.svg',
|
||||
variables.$color-white
|
||||
);
|
||||
}
|
||||
.AttachmentDetailPill__download-icon {
|
||||
@include mixins.position-absolute-center;
|
||||
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/arrow/arrow-down.svg',
|
||||
variables.$color-white
|
||||
);
|
||||
}
|
|
@ -25,6 +25,7 @@
|
|||
@use 'components/AddUserToAnotherGroupModal.scss';
|
||||
@use 'components/AnnouncementsOnlyGroupBanner.scss';
|
||||
@use 'components/App.scss';
|
||||
@use 'components/AttachmentDetailPill.scss';
|
||||
@use 'components/AudioCapture.scss';
|
||||
@use 'components/AutoSizeInput.scss';
|
||||
@use 'components/Avatar.scss';
|
||||
|
|
|
@ -244,8 +244,10 @@ export async function encryptAttachmentV2({
|
|||
}),
|
||||
peekAndUpdateHash(digest),
|
||||
incrementalDigestCreator,
|
||||
measureSize(finalSize => {
|
||||
measureSize({
|
||||
onComplete: finalSize => {
|
||||
ciphertextSize = finalSize;
|
||||
},
|
||||
}),
|
||||
sink ?? new PassThrough().resume(),
|
||||
].filter(isNotNil)
|
||||
|
@ -434,6 +436,7 @@ export async function decryptAttachmentV2ToSink(
|
|||
let isPaddingAllZeros = false;
|
||||
let readFd;
|
||||
let iv: Uint8Array | undefined;
|
||||
|
||||
try {
|
||||
try {
|
||||
readFd = await open(ciphertextPath, 'r');
|
||||
|
@ -652,15 +655,27 @@ function peekAndUpdateHash(hash: Hash) {
|
|||
});
|
||||
}
|
||||
|
||||
export function measureSize(onComplete: (size: number) => void): Transform {
|
||||
export function measureSize({
|
||||
downloadOffset = 0,
|
||||
onComplete,
|
||||
onSizeUpdate,
|
||||
}: {
|
||||
downloadOffset?: number;
|
||||
onComplete: (size: number) => void;
|
||||
onSizeUpdate?: (size: number) => void;
|
||||
}): Transform {
|
||||
let totalBytes = 0;
|
||||
|
||||
const passthrough = new PassThrough();
|
||||
|
||||
passthrough.on('data', chunk => {
|
||||
totalBytes += chunk.length;
|
||||
onSizeUpdate?.(totalBytes + downloadOffset);
|
||||
});
|
||||
passthrough.on('end', () => {
|
||||
onComplete(totalBytes);
|
||||
});
|
||||
|
||||
return passthrough;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,10 +23,8 @@ export type PropsType = {
|
|||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
platform: string;
|
||||
kickOffAttachmentDownload: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
}) => void;
|
||||
kickOffAttachmentDownload: (options: { messageId: string }) => void;
|
||||
cancelAttachmentDownload: (options: { messageId: string }) => void;
|
||||
showLightbox: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
|
@ -73,6 +71,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
};
|
||||
|
||||
export function EditHistoryMessagesModal({
|
||||
cancelAttachmentDownload,
|
||||
closeEditHistoryModal,
|
||||
getPreferredBadge,
|
||||
editHistoryMessages,
|
||||
|
@ -127,12 +126,8 @@ export function EditHistoryMessagesModal({
|
|||
isEditedMessage
|
||||
isSpoilerExpanded={revealedSpoilersById[currentMessageId] || {}}
|
||||
key={currentMessage.timestamp}
|
||||
kickOffAttachmentDownload={({ attachment }) =>
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: currentMessage.id,
|
||||
})
|
||||
}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
messageExpanded={(messageId, displayLimit) => {
|
||||
const update = {
|
||||
...displayLimitById,
|
||||
|
@ -195,12 +190,8 @@ export function EditHistoryMessagesModal({
|
|||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
isSpoilerExpanded={revealedSpoilersById[syntheticId] || {}}
|
||||
kickOffAttachmentDownload={({ attachment }) =>
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: messageAttributes.id,
|
||||
})
|
||||
}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
messageExpanded={(messageId, displayLimit) => {
|
||||
const update = {
|
||||
...displayLimitById,
|
||||
|
|
|
@ -55,6 +55,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
onToggleSelect: shouldNeverBeCalled,
|
||||
onReplyToMessage: shouldNeverBeCalled,
|
||||
kickOffAttachmentDownload: shouldNeverBeCalled,
|
||||
cancelAttachmentDownload: shouldNeverBeCalled,
|
||||
markAttachmentAsCorrupted: shouldNeverBeCalled,
|
||||
messageExpanded: shouldNeverBeCalled,
|
||||
openGiftBadge: shouldNeverBeCalled,
|
||||
|
|
91
ts/components/conversation/AttachmentDetailPill.stories.tsx
Normal file
91
ts/components/conversation/AttachmentDetailPill.stories.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { type PropsType, AttachmentDetailPill } from './AttachmentDetailPill';
|
||||
import { type ComponentMeta } from '../../storybook/types';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/AttachmentDetailPill',
|
||||
component: AttachmentDetailPill,
|
||||
argTypes: {
|
||||
isGif: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
attachments: [],
|
||||
isGif: false,
|
||||
startDownload: action('startDownload'),
|
||||
cancelDownload: action('cancelDownload'),
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
export function NoneDefaultsBlank(args: PropsType): JSX.Element {
|
||||
return <AttachmentDetailPill {...args} />;
|
||||
}
|
||||
|
||||
export function OneDownloadedBlank(args: PropsType): JSX.Element {
|
||||
return <AttachmentDetailPill {...args} attachments={[fakeAttachment()]} />;
|
||||
}
|
||||
|
||||
export function OneNotPendingNotDownloaded(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
path: undefined,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OnePendingNotDownloading(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
pending: true,
|
||||
path: undefined,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OneDownloading(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
pending: true,
|
||||
path: undefined,
|
||||
totalDownloaded: 5000,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OneNotPendingSomeDownloaded(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AttachmentDetailPill
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
path: undefined,
|
||||
totalDownloaded: 5000,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
57
ts/components/conversation/AttachmentDetailPill.tsx
Normal file
57
ts/components/conversation/AttachmentDetailPill.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { formatFileSize } from '../../util/formatFileSize';
|
||||
|
||||
import type { AttachmentForUIType } from '../../types/Attachment';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
|
||||
export type PropsType = {
|
||||
attachments: ReadonlyArray<AttachmentForUIType>;
|
||||
i18n: LocalizerType;
|
||||
isGif?: boolean;
|
||||
startDownload: () => void;
|
||||
cancelDownload: () => void;
|
||||
};
|
||||
|
||||
export function AttachmentDetailPill({
|
||||
attachments,
|
||||
isGif,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const areAllDownloaded = attachments.every(attachment => attachment.path);
|
||||
const totalSize = attachments.reduce(
|
||||
(total: number, attachment: AttachmentForUIType) => {
|
||||
return total + (attachment.size ?? 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
if (areAllDownloaded || totalSize === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalDownloadedSize = attachments.reduce(
|
||||
(total: number, attachment: AttachmentForUIType) => {
|
||||
return (
|
||||
total +
|
||||
(attachment.path ? attachment.size : (attachment.totalDownloaded ?? 0))
|
||||
);
|
||||
},
|
||||
0
|
||||
);
|
||||
const areAnyPending = attachments.some(attachment => attachment.pending);
|
||||
|
||||
return (
|
||||
<div className="AttachmentDetailPill">
|
||||
<div className="AttachmentDetailPill__text-wrapper">
|
||||
{totalDownloadedSize > 0 && areAnyPending
|
||||
? `${formatFileSize(totalDownloadedSize, 2)} / `
|
||||
: undefined}
|
||||
{formatFileSize(totalSize, 2)}
|
||||
{isGif ? ' · GIF' : undefined}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -91,7 +91,6 @@ export function AttachmentList<T extends AttachmentType | AttachmentDraftType>({
|
|||
isVideo ||
|
||||
attachment.pending
|
||||
) {
|
||||
const isDownloaded = !attachment.pending;
|
||||
const imageUrl =
|
||||
url || (isVideo ? BLANK_VIDEO_THUMBNAIL : undefined);
|
||||
|
||||
|
@ -108,7 +107,6 @@ export function AttachmentList<T extends AttachmentType | AttachmentDraftType>({
|
|||
className="module-staged-attachment"
|
||||
i18n={i18n}
|
||||
attachment={attachment}
|
||||
isDownloaded={isDownloaded}
|
||||
curveBottomLeft={CurveType.Tiny}
|
||||
curveBottomRight={CurveType.Tiny}
|
||||
curveTopLeft={CurveType.Tiny}
|
||||
|
@ -118,7 +116,7 @@ export function AttachmentList<T extends AttachmentType | AttachmentDraftType>({
|
|||
width={IMAGE_WIDTH}
|
||||
url={imageUrl}
|
||||
closeButton
|
||||
onClick={clickAttachment}
|
||||
showVisualAttachment={clickAttachment}
|
||||
onClickClose={closeAttachment}
|
||||
onError={closeAttachment}
|
||||
/>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type { AttachmentForUIType } from '../../types/Attachment';
|
||||
import {
|
||||
hasNotResolved,
|
||||
getImageDimensions,
|
||||
|
@ -17,21 +16,26 @@ import {
|
|||
import * as Errors from '../../types/errors';
|
||||
import * as log from '../../logging/log';
|
||||
import { useReducedMotion } from '../../hooks/useReducedMotion';
|
||||
import { AttachmentDetailPill } from './AttachmentDetailPill';
|
||||
import { getSpinner } from './Image';
|
||||
|
||||
const MAX_GIF_REPEAT = 4;
|
||||
const MAX_GIF_TIME = 8;
|
||||
|
||||
export type Props = {
|
||||
readonly attachment: AttachmentType;
|
||||
readonly attachment: AttachmentForUIType;
|
||||
readonly size?: number;
|
||||
readonly tabIndex: number;
|
||||
// test-only, to force reduced motion experience
|
||||
readonly _forceTapToPlay?: boolean;
|
||||
|
||||
readonly i18n: LocalizerType;
|
||||
readonly theme?: ThemeType;
|
||||
|
||||
onError(): void;
|
||||
showVisualAttachment(): void;
|
||||
kickOffAttachmentDownload(): void;
|
||||
startDownload(): void;
|
||||
cancelDownload(): void;
|
||||
};
|
||||
|
||||
type MediaEvent = React.SyntheticEvent<HTMLVideoElement, Event>;
|
||||
|
@ -41,16 +45,18 @@ export function GIF(props: Props): JSX.Element {
|
|||
attachment,
|
||||
size,
|
||||
tabIndex,
|
||||
_forceTapToPlay,
|
||||
|
||||
i18n,
|
||||
theme,
|
||||
|
||||
onError,
|
||||
showVisualAttachment,
|
||||
kickOffAttachmentDownload,
|
||||
startDownload,
|
||||
cancelDownload,
|
||||
} = props;
|
||||
|
||||
const tapToPlay = useReducedMotion();
|
||||
const tapToPlay = useReducedMotion() || _forceTapToPlay;
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const { height, width } = getImageDimensions(attachment, size);
|
||||
|
@ -142,7 +148,7 @@ export function GIF(props: Props): JSX.Element {
|
|||
event.stopPropagation();
|
||||
|
||||
if (!attachment.url) {
|
||||
kickOffAttachmentDownload();
|
||||
startDownload();
|
||||
} else if (tapToPlay) {
|
||||
setPlayTime(0);
|
||||
setCurrentTime(0);
|
||||
|
@ -158,21 +164,18 @@ export function GIF(props: Props): JSX.Element {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
kickOffAttachmentDownload();
|
||||
if (!attachment.url) {
|
||||
startDownload();
|
||||
} else if (tapToPlay) {
|
||||
setPlayTime(0);
|
||||
setCurrentTime(0);
|
||||
setRepeatCount(0);
|
||||
}
|
||||
};
|
||||
|
||||
const isPending = Boolean(attachment.pending);
|
||||
const isNotResolved = hasNotResolved(attachment) && !isPending;
|
||||
|
||||
let fileSize: JSX.Element | undefined;
|
||||
if (isNotResolved && attachment.fileSize) {
|
||||
fileSize = (
|
||||
<div className="module-image--gif__filesize">
|
||||
{attachment.fileSize} · GIF
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let gif: JSX.Element | undefined;
|
||||
if (isNotResolved || isPending) {
|
||||
gif = (
|
||||
|
@ -208,6 +211,35 @@ export function GIF(props: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
const cancelDownloadClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (cancelDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
const cancelDownloadKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (cancelDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
|
||||
const spinner = getSpinner({
|
||||
attachment,
|
||||
i18n,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
let overlay: JSX.Element | undefined;
|
||||
if ((tapToPlay && !isPlaying) || isNotResolved) {
|
||||
const className = classNames([
|
||||
|
@ -232,26 +264,22 @@ export function GIF(props: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
let spinner: JSX.Element | undefined;
|
||||
if (isPending) {
|
||||
spinner = (
|
||||
<div className="module-image__download-pending--spinner-container">
|
||||
<div
|
||||
className="module-image__download-pending--spinner"
|
||||
title={i18n('icu:loading')}
|
||||
>
|
||||
<Spinner moduleClassName="module-image-spinner" svgSize="small" />
|
||||
</div>
|
||||
</div>
|
||||
const detailPill = (
|
||||
<AttachmentDetailPill
|
||||
attachments={[attachment]}
|
||||
cancelDownload={cancelDownload}
|
||||
i18n={i18n}
|
||||
isGif
|
||||
startDownload={startDownload}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-image module-image--gif">
|
||||
{gif}
|
||||
{overlay}
|
||||
{spinner}
|
||||
{fileSize}
|
||||
{overlay}
|
||||
{detailPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,11 +38,13 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
curveTopLeft: overrideProps.curveTopLeft || CurveType.None,
|
||||
curveTopRight: overrideProps.curveTopRight || CurveType.None,
|
||||
darkOverlay: overrideProps.darkOverlay || false,
|
||||
height: overrideProps.height || 100,
|
||||
height: overrideProps.height || 200,
|
||||
i18n,
|
||||
noBackground: overrideProps.noBackground || false,
|
||||
noBorder: overrideProps.noBorder || false,
|
||||
onClick: action('onClick'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
startDownload: action('startDownload'),
|
||||
cancelDownload: action('cancelDownload'),
|
||||
onClickClose: action('onClickClose'),
|
||||
onError: action('onError'),
|
||||
overlayText: overrideProps.overlayText || '',
|
||||
|
@ -50,7 +52,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
tabIndex: overrideProps.tabIndex || 0,
|
||||
theme: overrideProps.theme || ('light' as ThemeType),
|
||||
url: 'url' in overrideProps ? overrideProps.url || '' : pngUrl,
|
||||
width: overrideProps.width || 100,
|
||||
width: overrideProps.width || 300,
|
||||
});
|
||||
|
||||
export function UrlWithHeightWidth(): JSX.Element {
|
||||
|
@ -107,37 +109,68 @@ export function NoBorderOrBackground(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function Pending(): JSX.Element {
|
||||
export function NotDownloadedNotIncrementalNotPending(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
url: pngUrl,
|
||||
pending: true,
|
||||
path: undefined,
|
||||
size: 5300000,
|
||||
}),
|
||||
url: undefined,
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function PendingWBlurhash(): JSX.Element {
|
||||
export function PendingWDownloadQueuedNotIncremental(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
url: pngUrl,
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 5300000,
|
||||
}),
|
||||
url: undefined,
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
});
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...props}
|
||||
blurHash="LDA,FDBnm+I=p{tkIUI;~UkpELV]"
|
||||
width={300}
|
||||
height={400}
|
||||
/>
|
||||
);
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function PendingWDownloadProgress(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 5300000,
|
||||
totalDownloaded: 1230000,
|
||||
}),
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function NotPendingWDownloadProgress(): JSX.Element {
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
size: 5300000,
|
||||
totalDownloaded: 1230000,
|
||||
}),
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
||||
export function CurvedCorners(): JSX.Element {
|
||||
|
@ -188,11 +221,14 @@ export function FullOverlayWithText(): JSX.Element {
|
|||
}
|
||||
|
||||
export function Blurhash(): JSX.Element {
|
||||
const defaultProps = createProps();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
const props = createProps({
|
||||
attachment: fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
}),
|
||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||
};
|
||||
url: undefined,
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
@ -213,12 +249,10 @@ export function UndefinedBlurHash(): JSX.Element {
|
|||
}
|
||||
|
||||
export function MissingImage(): JSX.Element {
|
||||
const defaultProps = createProps();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
attachment: undefined as any,
|
||||
};
|
||||
const props = createProps({
|
||||
attachment: undefined,
|
||||
url: 'random',
|
||||
});
|
||||
|
||||
return <Image {...props} />;
|
||||
}
|
||||
|
|
|
@ -2,17 +2,18 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import { Spinner } from '../Spinner';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import {
|
||||
isDownloaded as isDownloadedFunction,
|
||||
defaultBlurHash,
|
||||
import type {
|
||||
AttachmentForUIType,
|
||||
AttachmentType,
|
||||
} from '../../types/Attachment';
|
||||
import { defaultBlurHash, isReadyToView } from '../../types/Attachment';
|
||||
import { ProgressCircle } from '../ProgressCircle';
|
||||
|
||||
export enum CurveType {
|
||||
None = 0,
|
||||
|
@ -23,10 +24,9 @@ export enum CurveType {
|
|||
|
||||
export type Props = {
|
||||
alt: string;
|
||||
attachment: AttachmentType;
|
||||
attachment: AttachmentForUIType;
|
||||
url?: string;
|
||||
|
||||
isDownloaded?: boolean;
|
||||
className?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
|
@ -51,7 +51,9 @@ export type Props = {
|
|||
|
||||
i18n: LocalizerType;
|
||||
theme?: ThemeType;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
showVisualAttachment?: (attachment: AttachmentType) => void;
|
||||
cancelDownload?: () => void;
|
||||
startDownload?: () => void;
|
||||
onClickClose?: (attachment: AttachmentType) => void;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
@ -68,12 +70,13 @@ export function Image({
|
|||
curveTopLeft,
|
||||
curveTopRight,
|
||||
darkOverlay,
|
||||
isDownloaded,
|
||||
height = 0,
|
||||
i18n,
|
||||
noBackground,
|
||||
noBorder,
|
||||
onClick,
|
||||
showVisualAttachment,
|
||||
startDownload,
|
||||
cancelDownload,
|
||||
onClickClose,
|
||||
onError,
|
||||
overlayText,
|
||||
|
@ -85,11 +88,6 @@ export function Image({
|
|||
cropWidth = 0,
|
||||
cropHeight = 0,
|
||||
}: Props): JSX.Element {
|
||||
const { caption, pending } = attachment || { caption: null, pending: true };
|
||||
const imgNotDownloaded = isDownloaded
|
||||
? false
|
||||
: !isDownloadedFunction(attachment);
|
||||
|
||||
const resolvedBlurHash = blurHash || defaultBlurHash(theme);
|
||||
|
||||
const curveStyles: CSSProperties = {
|
||||
|
@ -99,48 +97,112 @@ export function Image({
|
|||
borderEndEndRadius: curveBottomRight || CurveType.None,
|
||||
};
|
||||
|
||||
const canClick = useMemo(() => {
|
||||
return onClick != null && !pending;
|
||||
}, [pending, onClick]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
const showVisualAttachmentClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!canClick) {
|
||||
if (showVisualAttachment) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClick(attachment);
|
||||
showVisualAttachment(attachment);
|
||||
}
|
||||
},
|
||||
[attachment, canClick, onClick]
|
||||
[attachment, showVisualAttachment]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
const showVisualAttachmentKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (!canClick) {
|
||||
if (
|
||||
showVisualAttachment &&
|
||||
(event.key === 'Enter' || event.key === 'Space')
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (onClick && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClick(attachment);
|
||||
showVisualAttachment(attachment);
|
||||
}
|
||||
},
|
||||
[attachment, canClick, onClick]
|
||||
[attachment, showVisualAttachment]
|
||||
);
|
||||
const cancelDownloadClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (cancelDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
const cancelDownloadKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (cancelDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelDownload();
|
||||
}
|
||||
},
|
||||
[cancelDownload]
|
||||
);
|
||||
const startDownloadClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (startDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
const startDownloadKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (startDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
const imageOrBlurHash = url ? (
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
) : (
|
||||
<Blurhash
|
||||
hash={resolvedBlurHash}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
);
|
||||
|
||||
const startDownloadButton =
|
||||
startDownload && !attachment.path && !attachment.pending ? (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image__overlay-circle"
|
||||
aria-label={i18n('icu:startDownload')}
|
||||
onClick={startDownloadClick}
|
||||
onKeyDown={startDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__download-icon" />
|
||||
</button>
|
||||
) : undefined;
|
||||
|
||||
const spinner = !cancelDownload
|
||||
? undefined
|
||||
: getSpinner({
|
||||
attachment,
|
||||
i18n,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -155,70 +217,11 @@ export function Image({
|
|||
...curveStyles,
|
||||
}}
|
||||
>
|
||||
{pending ? (
|
||||
url || blurHash ? (
|
||||
<div className="module-image__download-pending">
|
||||
{url ? (
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
) : blurHash ? (
|
||||
<Blurhash
|
||||
hash={blurHash}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
) : undefined}
|
||||
<div className="module-image__download-pending--spinner-container">
|
||||
<div
|
||||
className="module-image__download-pending--spinner"
|
||||
title={i18n('icu:loading')}
|
||||
>
|
||||
<Spinner
|
||||
moduleClassName="module-image-spinner"
|
||||
svgSize="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="module-image__loading-placeholder"
|
||||
style={{
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
lineHeight: `${height}px`,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
title={i18n('icu:loading')}
|
||||
>
|
||||
<Spinner svgSize="normal" />
|
||||
</div>
|
||||
)
|
||||
) : url ? (
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
) : resolvedBlurHash ? (
|
||||
<Blurhash
|
||||
hash={resolvedBlurHash}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
) : null}
|
||||
{caption ? (
|
||||
{imageOrBlurHash}
|
||||
{startDownloadButton}
|
||||
{spinner}
|
||||
|
||||
{attachment.caption ? (
|
||||
<img
|
||||
className="module-image__caption-icon"
|
||||
src="images/caption-shadow.svg"
|
||||
|
@ -234,9 +237,9 @@ export function Image({
|
|||
}}
|
||||
/>
|
||||
) : null}
|
||||
{!pending && !imgNotDownloaded && playIconOverlay ? (
|
||||
<div className="module-image__play-overlay__circle">
|
||||
<div className="module-image__play-overlay__icon" />
|
||||
{attachment.path && playIconOverlay ? (
|
||||
<div className="module-image__overlay-circle">
|
||||
<div className="module-image__play-icon" />
|
||||
</div>
|
||||
) : null}
|
||||
{overlayText ? (
|
||||
|
@ -247,22 +250,27 @@ export function Image({
|
|||
{overlayText}
|
||||
</div>
|
||||
) : null}
|
||||
{canClick ? (
|
||||
{darkOverlay || !noBorder ? (
|
||||
<div
|
||||
className={classNames('module-image__border-overlay', {
|
||||
'module-image__border-overlay--with-border': !noBorder,
|
||||
'module-image__border-overlay--dark': darkOverlay,
|
||||
})}
|
||||
style={curveStyles}
|
||||
/>
|
||||
) : null}
|
||||
{showVisualAttachment && isReadyToView(attachment) ? (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames('module-image__border-overlay', {
|
||||
'module-image__border-overlay--with-border': !noBorder,
|
||||
'module-image__border-overlay--with-click-handler': canClick,
|
||||
'module-image__border-overlay--dark': darkOverlay,
|
||||
'module-image--not-downloaded': imgNotDownloaded,
|
||||
'module-image__border-overlay--with-click-handler': true,
|
||||
})}
|
||||
aria-label={i18n('icu:imageOpenAlt')}
|
||||
style={curveStyles}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={showVisualAttachmentClick}
|
||||
onKeyDown={showVisualAttachmentKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{imgNotDownloaded ? <span /> : null}
|
||||
</button>
|
||||
/>
|
||||
) : null}
|
||||
{closeButton ? (
|
||||
<button
|
||||
|
@ -282,5 +290,71 @@ export function Image({
|
|||
) : null}
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable no-nested-ternary */
|
||||
}
|
||||
|
||||
export function getSpinner({
|
||||
attachment,
|
||||
cancelDownloadClick,
|
||||
cancelDownloadKeyDown,
|
||||
i18n,
|
||||
tabIndex,
|
||||
}: {
|
||||
attachment: AttachmentForUIType;
|
||||
cancelDownloadClick: (event: React.MouseEvent) => void;
|
||||
cancelDownloadKeyDown: (
|
||||
event: React.KeyboardEvent<HTMLButtonElement>
|
||||
) => void;
|
||||
i18n: LocalizerType;
|
||||
tabIndex: number | undefined;
|
||||
}): JSX.Element | undefined {
|
||||
const downloadFraction =
|
||||
attachment.pending && attachment.size && attachment.totalDownloaded
|
||||
? attachment.totalDownloaded / attachment.size
|
||||
: undefined;
|
||||
|
||||
if (downloadFraction) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image__overlay-circle"
|
||||
aria-label={i18n('icu:cancelDownload')}
|
||||
onClick={cancelDownloadClick}
|
||||
onKeyDown={cancelDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__stop-icon" />
|
||||
<div className="module-image__progress-circle-wrapper">
|
||||
<ProgressCircle
|
||||
fractionComplete={downloadFraction}
|
||||
width={44}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!attachment.pending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image__overlay-circle"
|
||||
aria-label={i18n('icu:cancelDownload')}
|
||||
onClick={cancelDownloadClick}
|
||||
onKeyDown={cancelDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__spinner-container">
|
||||
<Spinner
|
||||
moduleClassName="module-image-spinner"
|
||||
svgSize="normal"
|
||||
size="44px"
|
||||
/>
|
||||
<div className="module-image__stop-icon" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ export default {
|
|||
direction: 'incoming',
|
||||
i18n,
|
||||
isSticker: false,
|
||||
onClick: action('onClick'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
startDownload: action('startDownload'),
|
||||
cancelDownload: action('cancelDownload'),
|
||||
onError: action('onError'),
|
||||
stickerSize: 0,
|
||||
tabIndex: 0,
|
||||
|
@ -57,13 +59,111 @@ export function OneImage(args: Props): JSX.Element {
|
|||
return <ImageGrid {...args} />;
|
||||
}
|
||||
|
||||
export function OneVideo(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
url: pngUrl,
|
||||
width: 800,
|
||||
screenshot: {
|
||||
path: 'something',
|
||||
url: pngUrl,
|
||||
contentType: IMAGE_PNG,
|
||||
height: 1200,
|
||||
width: 800,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoNotDownloadedNotPending(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoPendingWDownloadQueued(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
url: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function OneVideoDownloadProgressNotPending(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
export function TwoImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
url: pngUrl,
|
||||
|
@ -81,6 +181,62 @@ export function TwoImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function TwoImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TwoImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function ThreeImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -112,6 +268,74 @@ export function ThreeImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function ThreeImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function ThreeImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FourImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -150,6 +374,89 @@ export function FourImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function FourImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function FourImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FiveImages(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -195,6 +502,104 @@ export function FiveImages(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function FiveImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
|
||||
export function FiveImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const _6Images = (args: Props): JSX.Element => {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -254,6 +659,63 @@ export const _6Images = (args: Props): JSX.Element => {
|
|||
);
|
||||
};
|
||||
|
||||
export function _6ImagesPendingWDownloadProgress(args: Props): JSX.Element {
|
||||
const props = {
|
||||
...args,
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
width: 3000,
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_PNG,
|
||||
fileName: 'sax.png',
|
||||
path: undefined,
|
||||
pending: true,
|
||||
size: 1000000,
|
||||
totalDownloaded: 300000,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
url: undefined,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return <ImageGrid {...props} />;
|
||||
}
|
||||
export function MixedContentTypes(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
@ -295,6 +757,80 @@ export function MixedContentTypes(args: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function EightImagesNotDownloaded(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
{...args}
|
||||
attachments={[
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
fileName: 'sax.png',
|
||||
height: 1200,
|
||||
width: 800,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
fakeAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
fileName: 'tina-rolf-269345-unsplash.jpg',
|
||||
height: 1680,
|
||||
width: 3000,
|
||||
path: undefined,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sticker(args: Props): JSX.Element {
|
||||
return (
|
||||
<ImageGrid
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { Image, CurveType } from './Image';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import { AttachmentDetailPill } from './AttachmentDetailPill';
|
||||
|
||||
export type DirectionType = 'incoming' | 'outgoing';
|
||||
|
||||
|
@ -39,7 +40,9 @@ export type Props = {
|
|||
theme?: ThemeType;
|
||||
|
||||
onError: () => void;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
showVisualAttachment: (attachment: AttachmentType) => void;
|
||||
cancelDownload: () => void;
|
||||
startDownload: () => void;
|
||||
};
|
||||
|
||||
const GAP = 1;
|
||||
|
@ -108,7 +111,9 @@ export function ImageGrid({
|
|||
isSticker,
|
||||
stickerSize,
|
||||
onError,
|
||||
onClick,
|
||||
showVisualAttachment,
|
||||
cancelDownload,
|
||||
startDownload,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
tabIndex,
|
||||
|
@ -127,10 +132,46 @@ export function ImageGrid({
|
|||
|
||||
const withBottomOverlay = Boolean(bottomOverlay && !withContentBelow);
|
||||
|
||||
const startDownloadClick = React.useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (startDownload) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
const startDownloadKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (startDownload && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startDownload();
|
||||
}
|
||||
},
|
||||
[startDownload]
|
||||
);
|
||||
|
||||
if (!attachments || !attachments.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detailPill = (
|
||||
<AttachmentDetailPill
|
||||
attachments={attachments}
|
||||
i18n={i18n}
|
||||
startDownload={startDownload}
|
||||
cancelDownload={cancelDownload}
|
||||
/>
|
||||
);
|
||||
const downloadPill = renderDownloadPill({
|
||||
attachments,
|
||||
i18n,
|
||||
startDownloadClick,
|
||||
startDownloadKeyDown,
|
||||
});
|
||||
|
||||
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
|
||||
const { height, width } = getImageDimensions(
|
||||
attachments[0],
|
||||
|
@ -165,9 +206,12 @@ export function ImageGrid({
|
|||
getUrl(attachments[0]) ?? attachments[0].thumbnailFromBackup?.url
|
||||
}
|
||||
tabIndex={tabIndex}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
{detailPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -190,7 +234,9 @@ export function ImageGrid({
|
|||
width={150}
|
||||
cropWidth={GAP}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -207,9 +253,13 @@ export function ImageGrid({
|
|||
width={150}
|
||||
attachment={attachments[1]}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -232,7 +282,9 @@ export function ImageGrid({
|
|||
width={200}
|
||||
cropWidth={GAP}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<div className="module-image-grid__column">
|
||||
|
@ -248,7 +300,9 @@ export function ImageGrid({
|
|||
attachment={attachments[1]}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -264,10 +318,14 @@ export function ImageGrid({
|
|||
attachment={attachments[2]}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -291,7 +349,9 @@ export function ImageGrid({
|
|||
cropHeight={GAP}
|
||||
cropWidth={GAP}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -307,7 +367,9 @@ export function ImageGrid({
|
|||
cropHeight={GAP}
|
||||
attachment={attachments[1]}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
|
@ -326,7 +388,9 @@ export function ImageGrid({
|
|||
cropWidth={GAP}
|
||||
attachment={attachments[2]}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -342,11 +406,15 @@ export function ImageGrid({
|
|||
width={150}
|
||||
attachment={attachments[3]}
|
||||
url={getThumbnailUrl(attachments[3])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -372,7 +440,9 @@ export function ImageGrid({
|
|||
width={150}
|
||||
cropWidth={GAP}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -386,7 +456,9 @@ export function ImageGrid({
|
|||
width={150}
|
||||
attachment={attachments[1]}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
|
@ -405,7 +477,9 @@ export function ImageGrid({
|
|||
cropWidth={GAP}
|
||||
attachment={attachments[2]}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -421,7 +495,9 @@ export function ImageGrid({
|
|||
cropWidth={GAP}
|
||||
attachment={attachments[3]}
|
||||
url={getThumbnailUrl(attachments[3])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={cancelDownload}
|
||||
startDownload={downloadPill ? undefined : startDownload}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
|
@ -439,11 +515,51 @@ export function ImageGrid({
|
|||
overlayText={moreMessagesOverlayText}
|
||||
attachment={attachments[4]}
|
||||
url={getThumbnailUrl(attachments[4])}
|
||||
onClick={onClick}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
cancelDownload={undefined}
|
||||
startDownload={undefined}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{detailPill}
|
||||
{downloadPill}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDownloadPill({
|
||||
attachments,
|
||||
i18n,
|
||||
startDownloadClick,
|
||||
startDownloadKeyDown,
|
||||
}: {
|
||||
attachments: ReadonlyArray<AttachmentForUIType>;
|
||||
i18n: LocalizerType;
|
||||
startDownloadClick: (event: React.MouseEvent) => void;
|
||||
startDownloadKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||
}): JSX.Element | null {
|
||||
const downloadedOrPending = attachments.some(
|
||||
attachment => attachment.path || attachment.pending
|
||||
);
|
||||
if (downloadedOrPending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image-grid__download-pill"
|
||||
aria-label={i18n('icu:startDownload')}
|
||||
onClick={startDownloadClick}
|
||||
onKeyDown={startDownloadKeyDown}
|
||||
>
|
||||
<div className="module-image-grid__download_pill__icon-wrapper">
|
||||
<div className="module-image-grid__download_pill__download-icon" />
|
||||
</div>
|
||||
<div className="module-image-grid__download_pill__text-wrapper">
|
||||
{i18n('icu:downloadNItems', { count: attachments.length })}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import React from 'react';
|
|||
import { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import getDirection from 'direction';
|
||||
import { drop, groupBy, noop, orderBy, take, unescape } from 'lodash';
|
||||
import { drop, groupBy, orderBy, take, unescape } from 'lodash';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
@ -52,7 +52,10 @@ import type { WidthBreakpoint } from '../_util';
|
|||
import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal';
|
||||
import * as log from '../../logging/log';
|
||||
import { StoryViewModeType } from '../../types/Stories';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import type {
|
||||
AttachmentForUIType,
|
||||
AttachmentType,
|
||||
} from '../../types/Attachment';
|
||||
import {
|
||||
canDisplayImage,
|
||||
getExtensionForDisplay,
|
||||
|
@ -101,6 +104,7 @@ import { UserText } from '../UserText';
|
|||
import { getColorForCallLink } from '../../util/getColorForCallLink';
|
||||
import { getKeyFromCallLink } from '../../util/callLinks';
|
||||
import { InAnotherCallTooltip } from './InAnotherCallTooltip';
|
||||
import { formatFileSize } from '../../util/formatFileSize';
|
||||
|
||||
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 16;
|
||||
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
|
||||
|
@ -173,7 +177,7 @@ export type AudioAttachmentProps = {
|
|||
i18n: LocalizerType;
|
||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||
theme: ThemeType | undefined;
|
||||
attachment: AttachmentType;
|
||||
attachment: AttachmentForUIType;
|
||||
collapseMetadata: boolean;
|
||||
withContentAbove: boolean;
|
||||
withContentBelow: boolean;
|
||||
|
@ -226,7 +230,7 @@ export type PropsData = {
|
|||
activeCallConversationId?: string;
|
||||
text?: string;
|
||||
textDirection: TextDirection;
|
||||
textAttachment?: AttachmentType;
|
||||
textAttachment?: AttachmentForUIType;
|
||||
isEditedMessage?: boolean;
|
||||
isSticker?: boolean;
|
||||
isTargeted?: boolean;
|
||||
|
@ -255,7 +259,7 @@ export type PropsData = {
|
|||
| 'unblurredAvatarUrl'
|
||||
>;
|
||||
conversationType: ConversationTypeType;
|
||||
attachments?: ReadonlyArray<AttachmentType>;
|
||||
attachments?: ReadonlyArray<AttachmentForUIType>;
|
||||
giftBadge?: GiftBadgeType;
|
||||
payment?: AnyPaymentEvent;
|
||||
quote?: {
|
||||
|
@ -312,6 +316,8 @@ export type PropsData = {
|
|||
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
|
||||
item?: never;
|
||||
// test-only, to force GIF's reduced motion experience
|
||||
_forceTapToPlay?: boolean;
|
||||
};
|
||||
|
||||
export type PropsHousekeeping = {
|
||||
|
@ -344,10 +350,8 @@ export type PropsActions = {
|
|||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showSpoiler: (messageId: string, data: Record<number, boolean>) => void;
|
||||
|
||||
kickOffAttachmentDownload: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
}) => void;
|
||||
cancelAttachmentDownload: (options: { messageId: string }) => void;
|
||||
kickOffAttachmentDownload: (options: { messageId: string }) => void;
|
||||
markAttachmentAsCorrupted: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
|
@ -919,10 +923,12 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const {
|
||||
attachments,
|
||||
attachmentDroppedDueToSize,
|
||||
cancelAttachmentDownload,
|
||||
conversationId,
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
_forceTapToPlay,
|
||||
i18n,
|
||||
id,
|
||||
isSticker,
|
||||
|
@ -978,9 +984,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
<GIF
|
||||
attachment={firstAttachment}
|
||||
size={GIF_SIZE}
|
||||
tabIndex={0}
|
||||
_forceTapToPlay={_forceTapToPlay}
|
||||
theme={theme}
|
||||
i18n={i18n}
|
||||
tabIndex={0}
|
||||
onError={this.handleImageError}
|
||||
showVisualAttachment={() => {
|
||||
showLightbox({
|
||||
|
@ -988,9 +995,13 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
messageId: id,
|
||||
});
|
||||
}}
|
||||
kickOffAttachmentDownload={() => {
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({
|
||||
messageId: id,
|
||||
});
|
||||
}}
|
||||
|
@ -1026,12 +1037,14 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
shouldCollapseAbove={shouldCollapseAbove}
|
||||
shouldCollapseBelow={shouldCollapseBelow}
|
||||
tabIndex={tabIndex}
|
||||
onClick={attachment => {
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({ attachment, messageId: id });
|
||||
} else {
|
||||
showVisualAttachment={attachment => {
|
||||
showLightbox({ attachment, messageId: id });
|
||||
}
|
||||
}}
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1063,10 +1076,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
timestamp,
|
||||
|
||||
kickOffAttachmentDownload() {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
},
|
||||
onCorrupted() {
|
||||
markAttachmentAsCorrupted({
|
||||
|
@ -1076,7 +1086,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
},
|
||||
});
|
||||
}
|
||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||
const { pending, fileName, size, contentType } = firstAttachment;
|
||||
const extension = getExtensionForDisplay({ contentType, fileName });
|
||||
const isDangerous = isFileDangerous(fileName || '');
|
||||
|
||||
|
@ -1100,7 +1110,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
if (!isDownloaded(firstAttachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
} else {
|
||||
|
@ -1143,7 +1152,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
`module-message__generic-attachment__file-size--${direction}`
|
||||
)}
|
||||
>
|
||||
{fileSize}
|
||||
{formatFileSize(size)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
@ -1158,6 +1167,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
i18n,
|
||||
id,
|
||||
kickOffAttachmentDownload,
|
||||
cancelAttachmentDownload,
|
||||
previews,
|
||||
quote,
|
||||
shouldCollapseAbove,
|
||||
|
@ -1209,18 +1219,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
'module-message__link-preview--nonclickable': !isClickable,
|
||||
}
|
||||
);
|
||||
const onPreviewImageClick = isClickable
|
||||
? () => {
|
||||
if (first.image && !isDownloaded(first.image)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: first.image,
|
||||
messageId: id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
openLinkInWebBrowser(first.url);
|
||||
}
|
||||
: noop;
|
||||
const contents = (
|
||||
<>
|
||||
{first.image && previewHasImage && isFullSizeImage ? (
|
||||
|
@ -1233,7 +1231,15 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
onError={this.handleImageError}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
onClick={onPreviewImageClick}
|
||||
showVisualAttachment={() => {
|
||||
openLinkInWebBrowser(first.url);
|
||||
}}
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div dir="auto" className="module-message__link-preview__content">
|
||||
|
@ -1261,7 +1267,15 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
blurHash={first.image.blurHash}
|
||||
onError={this.handleImageError}
|
||||
i18n={i18n}
|
||||
onClick={onPreviewImageClick}
|
||||
showVisualAttachment={() => {
|
||||
openLinkInWebBrowser(first.url);
|
||||
}}
|
||||
startDownload={() => {
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
cancelDownload={() => {
|
||||
cancelAttachmentDownload({ messageId: id });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -1970,7 +1984,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return;
|
||||
}
|
||||
kickOffAttachmentDownload({
|
||||
attachment: textAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
}}
|
||||
|
@ -2574,10 +2587,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
if (attachments && !isDownloaded(attachments[0])) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: attachments[0],
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -2597,9 +2607,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const attachment = attachments[0];
|
||||
|
||||
kickOffAttachmentDownload({ attachment, messageId: id });
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -2699,10 +2707,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const attachment = attachments[0];
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ export type PropsSmartActions = Pick<MessagePropsType, 'renderAudioAttachment'>;
|
|||
|
||||
export type PropsReduxActions = Pick<
|
||||
MessagePropsType,
|
||||
| 'cancelAttachmentDownload'
|
||||
| 'checkForAccount'
|
||||
| 'clearTargetedMessage'
|
||||
| 'doubleCheckMissingQuoteReference'
|
||||
|
@ -125,6 +126,7 @@ export function MessageDetail({
|
|||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
cancelAttachmentDownload,
|
||||
checkForAccount,
|
||||
clearTargetedMessage,
|
||||
contactNameColor,
|
||||
|
@ -330,6 +332,7 @@ export function MessageDetail({
|
|||
<Message
|
||||
{...message}
|
||||
renderingContext="conversation/MessageDetail"
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
checkForAccount={checkForAccount}
|
||||
clearTargetedMessage={clearTargetedMessage}
|
||||
contactNameColor={contactNameColor}
|
||||
|
|
|
@ -108,6 +108,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||
isSMS: false,
|
||||
isSpoilerExpanded: {},
|
||||
toggleSelectMessage: action('toggleSelectMessage'),
|
||||
cancelAttachmentDownload: action('default--cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('default--kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('default--message-expanded'),
|
||||
|
|
|
@ -296,6 +296,7 @@ const actions = () => ({
|
|||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showConversation: action('showConversation'),
|
||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
|
|
|
@ -76,6 +76,7 @@ const getDefaultProps = () => ({
|
|||
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
blockGroupLinkRequests: action('blockGroupLinkRequests'),
|
||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
|
|
|
@ -300,6 +300,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
isTapToView: overrideProps.isTapToView,
|
||||
isTapToViewError: overrideProps.isTapToViewError,
|
||||
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
|
@ -1400,6 +1401,22 @@ Gif.args = {
|
|||
status: 'sent',
|
||||
};
|
||||
|
||||
export const GifReducedMotion = Template.bind({});
|
||||
GifReducedMotion.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
url: '/fixtures/cat-gif.mp4',
|
||||
width: 400,
|
||||
height: 332,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
_forceTapToPlay: true,
|
||||
};
|
||||
|
||||
export const GifInAGroup = Template.bind({});
|
||||
GifInAGroup.args = {
|
||||
attachments: [
|
||||
|
@ -1423,10 +1440,10 @@ NotDownloadedGif.args = {
|
|||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
fileSize: '188.61 KB',
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1440,10 +1457,48 @@ PendingGif.args = {
|
|||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
fileSize: '188.61 KB',
|
||||
size: 188610,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const DownloadingGif = Template.bind({});
|
||||
DownloadingGif.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
pending: true,
|
||||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
size: 188610,
|
||||
totalDownloaded: 101010,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const PartialDownloadNotPendingGif = Template.bind({});
|
||||
PartialDownloadNotPendingGif.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: VIDEO_MP4,
|
||||
flags: SignalService.AttachmentPointer.Flags.GIF,
|
||||
fileName: 'cat-gif.mp4',
|
||||
size: 188610,
|
||||
totalDownloaded: 101010,
|
||||
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
|
||||
width: 400,
|
||||
height: 332,
|
||||
path: undefined,
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1553,7 +1608,6 @@ OtherFileType.args = {
|
|||
contentType: stringToMIMEType('text/plain'),
|
||||
fileName: 'my-resume.txt',
|
||||
url: 'my-resume.txt',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1566,7 +1620,6 @@ OtherFileTypeWithCaption.args = {
|
|||
contentType: stringToMIMEType('text/plain'),
|
||||
fileName: 'my-resume.txt',
|
||||
url: 'my-resume.txt',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1581,7 +1634,6 @@ OtherFileTypeWithLongFilename.args = {
|
|||
fileName:
|
||||
'INSERT-APP-NAME_INSERT-APP-APPLE-ID_AppStore_AppsGamesWatch.psd.zip',
|
||||
url: 'a2/a2334324darewer4234',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
|
|
@ -221,10 +221,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
// check if any attachment needs to be downloaded from servers
|
||||
for (const attachment of attachments) {
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
kickOffAttachmentDownload({ messageId: id });
|
||||
|
||||
attachmentsInProgress += 1;
|
||||
}
|
||||
|
|
|
@ -3466,7 +3466,10 @@ async function appendChangeMessages(
|
|||
|
||||
let newMessages = 0;
|
||||
for (const changeMessage of mergedMessages) {
|
||||
const existing = window.MessageCache.__DEPRECATED$getById(changeMessage.id);
|
||||
const existing = window.MessageCache.__DEPRECATED$getById(
|
||||
changeMessage.id,
|
||||
'appendChangeMessages'
|
||||
);
|
||||
|
||||
// Update existing message
|
||||
if (existing) {
|
||||
|
|
|
@ -190,7 +190,10 @@ type RunAttachmentBackupJobDependenciesType = {
|
|||
|
||||
export async function runAttachmentBackupJob(
|
||||
job: AttachmentBackupJobType,
|
||||
_isLastAttempt: boolean,
|
||||
_options: {
|
||||
isLastAttempt: boolean;
|
||||
abortSignal: AbortSignal;
|
||||
},
|
||||
dependencies: RunAttachmentBackupJobDependenciesType = {
|
||||
getAbsoluteAttachmentPath:
|
||||
window.Signal.Migrations.getAbsoluteAttachmentPath,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { omit } from 'lodash';
|
||||
import { debounce, noop, omit } from 'lodash';
|
||||
|
||||
import * as durations from '../util/durations';
|
||||
import * as log from '../logging/log';
|
||||
|
@ -37,6 +37,7 @@ import {
|
|||
JobManager,
|
||||
type JobManagerParamsType,
|
||||
type JobManagerJobResultType,
|
||||
type JobManagerJobType,
|
||||
} from './JobManager';
|
||||
import {
|
||||
isImageTypeSupported,
|
||||
|
@ -93,7 +94,10 @@ type AttachmentDownloadManagerParamsType = Omit<
|
|||
runDownloadAttachmentJob: (args: {
|
||||
job: AttachmentDownloadJobType;
|
||||
isLastAttempt: boolean;
|
||||
options?: { isForCurrentlyVisibleMessage: boolean };
|
||||
options: {
|
||||
abortSignal: AbortSignal;
|
||||
isForCurrentlyVisibleMessage: boolean;
|
||||
};
|
||||
dependencies?: DependenciesType;
|
||||
}) => Promise<JobManagerJobResultType<CoreAttachmentDownloadJobType>>;
|
||||
};
|
||||
|
@ -164,7 +168,13 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
|||
timestamp: Date.now(),
|
||||
});
|
||||
},
|
||||
runJob: (job: AttachmentDownloadJobType, isLastAttempt: boolean) => {
|
||||
runJob: (
|
||||
job: AttachmentDownloadJobType,
|
||||
{
|
||||
abortSignal,
|
||||
isLastAttempt,
|
||||
}: { abortSignal: AbortSignal; isLastAttempt: boolean }
|
||||
) => {
|
||||
const isForCurrentlyVisibleMessage = this.visibleTimelineMessages.has(
|
||||
job.messageId
|
||||
);
|
||||
|
@ -172,6 +182,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
|||
job,
|
||||
isLastAttempt,
|
||||
options: {
|
||||
abortSignal,
|
||||
isForCurrentlyVisibleMessage,
|
||||
},
|
||||
});
|
||||
|
@ -268,6 +279,14 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
|||
return AttachmentDownloadManager.instance.addJob(newJob);
|
||||
}
|
||||
|
||||
static async cancelJobs(
|
||||
predicate: (
|
||||
job: CoreAttachmentDownloadJobType & JobManagerJobType
|
||||
) => boolean
|
||||
): Promise<void> {
|
||||
return AttachmentDownloadManager.instance.cancelJobs(predicate);
|
||||
}
|
||||
|
||||
static updateVisibleTimelineMessages(messageIds: Array<string>): void {
|
||||
AttachmentDownloadManager.instance.updateVisibleTimelineMessages(
|
||||
messageIds
|
||||
|
@ -283,6 +302,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
|||
}
|
||||
|
||||
type DependenciesType = {
|
||||
deleteDownloadData: typeof window.Signal.Migrations.deleteDownloadData;
|
||||
downloadAttachment: typeof downloadAttachmentUtil;
|
||||
processNewAttachment: typeof window.Signal.Migrations.processNewAttachment;
|
||||
};
|
||||
|
@ -291,19 +311,26 @@ async function runDownloadAttachmentJob({
|
|||
isLastAttempt,
|
||||
options,
|
||||
dependencies = {
|
||||
deleteDownloadData: window.Signal.Migrations.deleteDownloadData,
|
||||
downloadAttachment: downloadAttachmentUtil,
|
||||
processNewAttachment: window.Signal.Migrations.processNewAttachment,
|
||||
},
|
||||
}: {
|
||||
job: AttachmentDownloadJobType;
|
||||
isLastAttempt: boolean;
|
||||
options?: { isForCurrentlyVisibleMessage: boolean };
|
||||
options: {
|
||||
abortSignal: AbortSignal;
|
||||
isForCurrentlyVisibleMessage: boolean;
|
||||
};
|
||||
dependencies?: DependenciesType;
|
||||
}): Promise<JobManagerJobResultType<CoreAttachmentDownloadJobType>> {
|
||||
const jobIdForLogging = getJobIdForLogging(job);
|
||||
const logId = `AttachmentDownloadManager/runDownloadAttachmentJob/${jobIdForLogging}`;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(job.messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
job.messageId,
|
||||
'runDownloadAttachmentJob'
|
||||
);
|
||||
|
||||
if (!message) {
|
||||
log.error(`${logId} message not found`);
|
||||
|
@ -315,6 +342,7 @@ async function runDownloadAttachmentJob({
|
|||
|
||||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
abortSignal: options.abortSignal,
|
||||
isForCurrentlyVisibleMessage:
|
||||
options?.isForCurrentlyVisibleMessage ?? false,
|
||||
dependencies,
|
||||
|
@ -342,6 +370,14 @@ async function runDownloadAttachmentJob({
|
|||
status: 'finished',
|
||||
};
|
||||
} catch (error) {
|
||||
if (options.abortSignal.aborted) {
|
||||
log.warn(
|
||||
`${logId}: Cancelled attempt ${job.attempts}. Not scheduling a retry. Error:`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return { status: 'finished' };
|
||||
}
|
||||
|
||||
log.error(
|
||||
`${logId}: Failed to download attachment, attempt ${job.attempts}:`,
|
||||
Errors.toLogFormat(error)
|
||||
|
@ -407,10 +443,12 @@ type DownloadAttachmentResultType =
|
|||
|
||||
export async function runDownloadAttachmentJobInner({
|
||||
job,
|
||||
abortSignal,
|
||||
isForCurrentlyVisibleMessage,
|
||||
dependencies,
|
||||
}: {
|
||||
job: AttachmentDownloadJobType;
|
||||
abortSignal: AbortSignal;
|
||||
isForCurrentlyVisibleMessage: boolean;
|
||||
dependencies: DependenciesType;
|
||||
}): Promise<DownloadAttachmentResultType> {
|
||||
|
@ -458,6 +496,7 @@ export async function runDownloadAttachmentJobInner({
|
|||
try {
|
||||
const attachmentWithThumbnail = await downloadBackupThumbnail({
|
||||
attachment,
|
||||
abortSignal,
|
||||
dependencies,
|
||||
});
|
||||
await addAttachmentToMessage(messageId, attachmentWithThumbnail, logId, {
|
||||
|
@ -482,9 +521,29 @@ export async function runDownloadAttachmentJobInner({
|
|||
);
|
||||
|
||||
try {
|
||||
let totalDownloaded = 0;
|
||||
|
||||
const onSizeUpdate = async (totalBytes: number) => {
|
||||
if (abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
totalDownloaded = Math.min(totalBytes, attachment.size);
|
||||
await addAttachmentToMessage(
|
||||
messageId,
|
||||
{ ...attachment, totalDownloaded, pending: true },
|
||||
logId,
|
||||
{ type: attachmentType }
|
||||
);
|
||||
};
|
||||
|
||||
const downloaded = await dependencies.downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: debounce(onSizeUpdate, 200),
|
||||
abortSignal,
|
||||
},
|
||||
});
|
||||
|
||||
const upgradedAttachment = await dependencies.processNewAttachment({
|
||||
|
@ -510,6 +569,7 @@ export async function runDownloadAttachmentJobInner({
|
|||
const attachmentWithThumbnail = omit(
|
||||
await downloadBackupThumbnail({
|
||||
attachment,
|
||||
abortSignal,
|
||||
dependencies,
|
||||
}),
|
||||
'pending'
|
||||
|
@ -539,14 +599,20 @@ export async function runDownloadAttachmentJobInner({
|
|||
|
||||
async function downloadBackupThumbnail({
|
||||
attachment,
|
||||
abortSignal,
|
||||
dependencies,
|
||||
}: {
|
||||
attachment: AttachmentType;
|
||||
abortSignal: AbortSignal;
|
||||
dependencies: { downloadAttachment: typeof downloadAttachmentUtil };
|
||||
}): Promise<AttachmentType> {
|
||||
const downloadedThumbnail = await dependencies.downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
onSizeUpdate: noop,
|
||||
variant: AttachmentVariant.ThumbnailFromBackup,
|
||||
abortSignal,
|
||||
},
|
||||
});
|
||||
|
||||
const attachmentWithThumbnail = {
|
||||
|
|
|
@ -184,7 +184,7 @@ async function removeJob(
|
|||
|
||||
async function runJob(
|
||||
job: CallLinkDeleteJobType,
|
||||
_isLastAttempt: boolean
|
||||
_options: { isLastAttempt: boolean; abortSignal: AbortSignal }
|
||||
): Promise<JobManagerJobResultType<CoreCallLinkDeleteJobType>> {
|
||||
const logId = `CallLinkDeleteJobType/runJob/${getJobId(job)}`;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as z from 'zod';
|
||||
import { MINUTE } from '../util/durations';
|
||||
import { MINUTE, SECOND } from '../util/durations';
|
||||
import {
|
||||
explodePromise,
|
||||
type ExplodePromiseResultType,
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
exponentialBackoffSleepTime,
|
||||
} from '../util/exponentialBackoff';
|
||||
import * as Errors from '../types/errors';
|
||||
import { sleep } from '../util/sleep';
|
||||
|
||||
export type JobManagerJobType = {
|
||||
active: boolean;
|
||||
|
@ -46,7 +47,10 @@ export type JobManagerParamsType<
|
|||
removeJob: (job: JobType) => Promise<void>;
|
||||
runJob: (
|
||||
job: JobType,
|
||||
isLastAttempt: boolean
|
||||
options: {
|
||||
abortSignal: AbortSignal;
|
||||
isLastAttempt: boolean;
|
||||
}
|
||||
) => Promise<JobManagerJobResultType<CoreJobType>>;
|
||||
shouldHoldOffOnStartingQueuedJobs?: () => boolean;
|
||||
getJobId: (job: CoreJobType) => string;
|
||||
|
@ -66,15 +70,15 @@ export type JobManagerJobResultType<CoreJobType> =
|
|||
| { status: 'finished'; newJob?: CoreJobType }
|
||||
| { status: 'rate-limited'; pauseDurationMs: number };
|
||||
|
||||
export type ActiveJobData<CoreJobType> = {
|
||||
completionPromise: ExplodePromiseResultType<void>;
|
||||
abortController: AbortController;
|
||||
job: CoreJobType & JobManagerJobType;
|
||||
};
|
||||
|
||||
export abstract class JobManager<CoreJobType> {
|
||||
private enabled: boolean = false;
|
||||
private activeJobs: Map<
|
||||
string,
|
||||
{
|
||||
completionPromise: ExplodePromiseResultType<void>;
|
||||
job: CoreJobType & JobManagerJobType;
|
||||
}
|
||||
> = new Map();
|
||||
private activeJobs: Map<string, ActiveJobData<CoreJobType>> = new Map();
|
||||
private jobStartPromises: Map<string, ExplodePromiseResultType<void>> =
|
||||
new Map();
|
||||
private jobCompletePromises: Map<string, ExplodePromiseResultType<void>> =
|
||||
|
@ -108,7 +112,10 @@ export abstract class JobManager<CoreJobType> {
|
|||
clearTimeoutIfNecessary(this.tickTimeout);
|
||||
this.tickTimeout = null;
|
||||
await Promise.all(
|
||||
activeJobs.map(({ completionPromise }) => completionPromise.promise)
|
||||
activeJobs.map(async ({ abortController, completionPromise }) => {
|
||||
abortController.abort();
|
||||
await completionPromise.promise;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -291,9 +298,12 @@ export abstract class JobManager<CoreJobType> {
|
|||
let jobRunResult: JobManagerJobResultType<CoreJobType> | undefined;
|
||||
try {
|
||||
log.info(`${logId}: starting job`);
|
||||
this.addRunningJob(job);
|
||||
const { abortController } = this.addRunningJob(job);
|
||||
await this.params.saveJob({ ...job, active: true });
|
||||
const runJobPromise = this.params.runJob(job, isLastAttempt);
|
||||
const runJobPromise = this.params.runJob(job, {
|
||||
abortSignal: abortController.signal,
|
||||
isLastAttempt,
|
||||
});
|
||||
this.handleJobStartPromises(job);
|
||||
jobRunResult = await runJobPromise;
|
||||
const { status } = jobRunResult;
|
||||
|
@ -388,17 +398,71 @@ export abstract class JobManager<CoreJobType> {
|
|||
this.activeJobs.delete(id);
|
||||
}
|
||||
|
||||
private addRunningJob(job: CoreJobType & JobManagerJobType) {
|
||||
public async cancelJobs(
|
||||
predicate: (job: CoreJobType & JobManagerJobType) => boolean
|
||||
): Promise<void> {
|
||||
const logId = `${this.logPrefix}/cancelJobs`;
|
||||
const jobs = Array.from(this.activeJobs.values()).filter(data =>
|
||||
predicate(data.job)
|
||||
);
|
||||
|
||||
if (jobs.length === 0) {
|
||||
log.warn(`${logId}: found no target jobs`);
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
jobs.map(async jobData => {
|
||||
const { abortController, completionPromise, job } = jobData;
|
||||
|
||||
abortController.abort();
|
||||
|
||||
// First tell those waiting for the job that it's not happening
|
||||
const rejectionError = new Error('Cancelled at JobManager.cancelJobs');
|
||||
const idWithAttempts = this.getJobIdIncludingAttempts(job);
|
||||
this.jobCompletePromises.get(idWithAttempts)?.reject(rejectionError);
|
||||
this.jobCompletePromises.delete(idWithAttempts);
|
||||
|
||||
// Give the job 1 second to cancel itself
|
||||
await Promise.race([completionPromise.promise, sleep(SECOND)]);
|
||||
|
||||
const jobId = this.params.getJobId(job);
|
||||
const hasCompleted = Boolean(this.activeJobs.get(jobId));
|
||||
|
||||
if (!hasCompleted) {
|
||||
const jobIdForLogging = this.params.getJobIdForLogging(job);
|
||||
log.warn(
|
||||
`${logId}: job ${jobIdForLogging} didn't complete; rejecting promises`
|
||||
);
|
||||
completionPromise.reject(rejectionError);
|
||||
this.activeJobs.delete(jobId);
|
||||
}
|
||||
|
||||
await this.params.removeJob(job);
|
||||
})
|
||||
);
|
||||
|
||||
log.warn(`${logId}: Successfully cancelled ${jobs.length} jobs`);
|
||||
}
|
||||
|
||||
private addRunningJob(
|
||||
job: CoreJobType & JobManagerJobType
|
||||
): ActiveJobData<CoreJobType> {
|
||||
if (this.isJobRunning(job)) {
|
||||
const jobIdForLogging = this.params.getJobIdForLogging(job);
|
||||
log.warn(
|
||||
`${this.logPrefix}/addRunningJob: job ${jobIdForLogging} is already running`
|
||||
);
|
||||
}
|
||||
this.activeJobs.set(this.params.getJobId(job), {
|
||||
|
||||
const activeJob = {
|
||||
completionPromise: explodePromise<void>(),
|
||||
abortController: new AbortController(),
|
||||
job,
|
||||
});
|
||||
};
|
||||
this.activeJobs.set(this.params.getJobId(job), activeJob);
|
||||
|
||||
return activeJob;
|
||||
}
|
||||
|
||||
private handleJobStartPromises(job: CoreJobType & JobManagerJobType) {
|
||||
|
|
|
@ -60,7 +60,7 @@ export async function sendDeleteForEveryone(
|
|||
|
||||
const logId = `sendDeleteForEveryone(${conversation.idForLogging()}, ${messageId})`;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(messageId, logId);
|
||||
if (!message) {
|
||||
log.error(`${logId}: Failed to fetch message. Failing job.`);
|
||||
return;
|
||||
|
|
|
@ -46,7 +46,10 @@ export async function sendDeleteStoryForEveryone(
|
|||
|
||||
const logId = `sendDeleteStoryForEveryone(${storyId})`;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(storyId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
storyId,
|
||||
'sendDeleteStoryForEveryone'
|
||||
);
|
||||
if (!message) {
|
||||
log.error(`${logId}: Failed to fetch message. Failing job.`);
|
||||
return;
|
||||
|
|
|
@ -73,7 +73,10 @@ export async function sendNormalMessage(
|
|||
const { Message } = window.Signal.Types;
|
||||
|
||||
const { messageId, revision, editedMessageTimestamp } = data;
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'sendNormalMessage'
|
||||
);
|
||||
if (!message) {
|
||||
log.info(
|
||||
`message ${messageId} was not found, maybe because it was deleted. Giving up on sending it`
|
||||
|
@ -654,7 +657,9 @@ async function getMessageSendData({
|
|||
uploadQueue,
|
||||
}),
|
||||
uploadMessageSticker(message, uploadQueue),
|
||||
storyId ? __DEPRECATED$getMessageById(storyId) : undefined,
|
||||
storyId
|
||||
? __DEPRECATED$getMessageById(storyId, 'sendNormalMessage')
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
// Save message after uploading attachments
|
||||
|
|
|
@ -61,7 +61,7 @@ export async function sendReaction(
|
|||
const ourConversationId =
|
||||
window.ConversationController.getOurConversationIdOrThrow();
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(messageId, 'sendReaction');
|
||||
if (!message) {
|
||||
log.info(
|
||||
`message ${messageId} was not found, maybe because it was deleted. Giving up on sending its reactions`
|
||||
|
|
|
@ -71,7 +71,8 @@ export async function sendStory(
|
|||
}
|
||||
|
||||
const notFound = new Set(messageIds);
|
||||
const messages = (await getMessagesById(messageIds)).filter(message => {
|
||||
const messages = (await getMessagesById(messageIds, 'sendStory')).filter(
|
||||
message => {
|
||||
notFound.delete(message.id);
|
||||
|
||||
const distributionId = message.get('storyDistributionListId');
|
||||
|
@ -102,7 +103,8 @@ export async function sendStory(
|
|||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
for (const messageId of notFound) {
|
||||
log.info(
|
||||
|
|
|
@ -14,13 +14,13 @@ export async function addAttachmentToMessage(
|
|||
jobLogId: string,
|
||||
{ type }: { type: AttachmentDownloadJobTypeType }
|
||||
): Promise<void> {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const logPrefix = `${jobLogId}/addAttachmentToMessage`;
|
||||
const message = await __DEPRECATED$getMessageById(messageId, logPrefix);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logPrefix = `${jobLogId}/addAttachmentToMessage`;
|
||||
const attachmentSignature = getAttachmentSignature(attachment);
|
||||
|
||||
if (type === 'long-message') {
|
||||
|
|
|
@ -8,9 +8,14 @@ import * as Errors from '../types/errors';
|
|||
import type { MessageModel } from '../models/messages';
|
||||
|
||||
export async function __DEPRECATED$getMessageById(
|
||||
messageId: string
|
||||
messageId: string,
|
||||
location: string
|
||||
): Promise<MessageModel | undefined> {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
||||
const innerLocation = `__DEPRECATED$getMessageById/${location}`;
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
innerLocation
|
||||
);
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
@ -32,6 +37,6 @@ export async function __DEPRECATED$getMessageById(
|
|||
return window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'__DEPRECATED$getMessageById'
|
||||
innerLocation
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,13 +8,18 @@ import type { MessageAttributesType } from '../model-types.d';
|
|||
import * as Errors from '../types/errors';
|
||||
|
||||
export async function getMessagesById(
|
||||
messageIds: Iterable<string>
|
||||
messageIds: Iterable<string>,
|
||||
location: string
|
||||
): Promise<Array<MessageModel>> {
|
||||
const innerLocation = `getMessagesById/${location}`;
|
||||
const messagesFromMemory: Array<MessageModel> = [];
|
||||
const messageIdsToLookUpInDatabase: Array<string> = [];
|
||||
|
||||
for (const messageId of messageIds) {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
innerLocation
|
||||
);
|
||||
if (message) {
|
||||
messagesFromMemory.push(message);
|
||||
} else {
|
||||
|
@ -43,7 +48,7 @@ export async function getMessagesById(
|
|||
return window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
message,
|
||||
'getMessagesById'
|
||||
innerLocation
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -3556,7 +3556,10 @@ export class ConversationModel extends window.Backbone
|
|||
`maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification`
|
||||
);
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
notificationId,
|
||||
'maybeRemoveUniversalTimer'
|
||||
);
|
||||
if (message) {
|
||||
await DataWriter.removeMessage(message.id, {
|
||||
singleProtoJobQueue,
|
||||
|
@ -3599,7 +3602,10 @@ export class ConversationModel extends window.Backbone
|
|||
`maybeClearContactRemoved(${this.idForLogging()}): removed notification`
|
||||
);
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
notificationId,
|
||||
'maybeClearContactRemoved'
|
||||
);
|
||||
if (message) {
|
||||
await DataWriter.removeMessage(message.id, {
|
||||
singleProtoJobQueue,
|
||||
|
|
|
@ -393,7 +393,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
`doubleCheckMissingQuoteReference/${logId}: missing story reference`
|
||||
);
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$getById(storyId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
storyId,
|
||||
'doubleCheckMissingQuoteReference'
|
||||
);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,10 @@ export async function enqueueReactionForSend({
|
|||
messageId: string;
|
||||
remove: boolean;
|
||||
}>): Promise<void> {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'enqueueReactionForSend'
|
||||
);
|
||||
strictAssert(message, 'enqueueReactionForSend: no message found');
|
||||
|
||||
const targetAuthorAci = getSourceServiceId(message.attributes);
|
||||
|
|
|
@ -420,7 +420,7 @@ export class MessageCache {
|
|||
);
|
||||
}
|
||||
|
||||
const existing = this.__DEPRECATED$getById(id);
|
||||
const existing = this.__DEPRECATED$getById(id, location);
|
||||
|
||||
if (existing) {
|
||||
this.addMessageToCache(existing.attributes);
|
||||
|
@ -447,13 +447,18 @@ export class MessageCache {
|
|||
}
|
||||
|
||||
// Finds a message in the cache by Id
|
||||
public __DEPRECATED$getById(id: string): MessageModel | undefined {
|
||||
public __DEPRECATED$getById(
|
||||
id: string,
|
||||
location: string
|
||||
): MessageModel | undefined {
|
||||
const data = this.state.messages.get(id);
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.toModel(data);
|
||||
const model = this.toModel(data);
|
||||
model.registerLocations.add(location);
|
||||
return model;
|
||||
}
|
||||
|
||||
public async upgradeSchema(
|
||||
|
@ -513,9 +518,9 @@ export class MessageCache {
|
|||
model.attributes = { ...messageAttributes };
|
||||
|
||||
if (getEnvironment() === Environment.Development) {
|
||||
log.warn('MessageCache: stale model', {
|
||||
log.warn('MessageCache: updating cached backbone model', {
|
||||
cid: model.cid,
|
||||
locations: Array.from(model.registerLocations).join('+'),
|
||||
locations: Array.from(model.registerLocations).join(', '),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -613,8 +613,10 @@ export class BackupsService {
|
|||
createCipheriv(CipherType.AES256CBC, aesKey, iv),
|
||||
prependStream(iv),
|
||||
appendMacStream(macKey),
|
||||
measureSize(size => {
|
||||
measureSize({
|
||||
onComplete: size => {
|
||||
totalBytes = size;
|
||||
},
|
||||
}),
|
||||
sink
|
||||
);
|
||||
|
|
|
@ -40,6 +40,7 @@ import { bytesToUuid } from '../../../util/uuidToBytes';
|
|||
import { createName } from '../../../util/attachmentPath';
|
||||
import { ensureAttachmentIsReencryptable } from '../../../util/ensureAttachmentIsReencryptable';
|
||||
import type { ReencryptionInfo } from '../../../AttachmentCrypto';
|
||||
import { dropZero } from '../../../util/dropZero';
|
||||
|
||||
export function convertFilePointerToAttachment(
|
||||
filePointer: Backups.FilePointer,
|
||||
|
@ -72,7 +73,7 @@ export function convertFilePointerToAttachment(
|
|||
incrementalMac: incrementalMac?.length
|
||||
? Bytes.toBase64(incrementalMac)
|
||||
: undefined,
|
||||
incrementalMacChunkSize: incrementalMacChunkSize ?? undefined,
|
||||
chunkSize: dropZero(incrementalMacChunkSize),
|
||||
downloadPath: doCreateName(),
|
||||
};
|
||||
|
||||
|
@ -182,7 +183,7 @@ export async function getFilePointerForAttachment({
|
|||
incrementalMac: attachment.incrementalMac
|
||||
? Bytes.fromBase64(attachment.incrementalMac)
|
||||
: undefined,
|
||||
incrementalMacChunkSize: attachment.incrementalMacChunkSize,
|
||||
incrementalMacChunkSize: dropZero(attachment.chunkSize),
|
||||
fileName: attachment.fileName,
|
||||
width: attachment.width,
|
||||
height: attachment.height,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import PQueue from 'p-queue';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { DataWriter } from '../sql/Client';
|
||||
import type { ContactSyncEvent } from '../textsecure/messageReceiverEvents';
|
||||
|
@ -23,6 +24,7 @@ import { downloadAttachment } from '../textsecure/downloadAttachment';
|
|||
import { strictAssert } from '../util/assert';
|
||||
import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto';
|
||||
import { SECOND } from '../util/durations';
|
||||
import { AttachmentVariant } from '../types/Attachment';
|
||||
|
||||
// When true - we are running the very first storage and contact sync after
|
||||
// linking.
|
||||
|
@ -103,12 +105,16 @@ async function downloadAndParseContactAttachment(
|
|||
strictAssert(window.textsecure.server, 'server must exist');
|
||||
let downloaded: ReencryptedAttachmentV2 | undefined;
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
downloaded = await downloadAttachment(
|
||||
window.textsecure.server,
|
||||
contactAttachment,
|
||||
{
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
disableRetries: true,
|
||||
timeout: 90 * SECOND,
|
||||
abortSignal: abortController.signal,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -873,6 +873,7 @@ type WritableInterface = {
|
|||
saveAttachmentDownloadJobs: (jobs: Array<AttachmentDownloadJobType>) => void;
|
||||
resetAttachmentDownloadActive: () => void;
|
||||
removeAttachmentDownloadJob: (job: AttachmentDownloadJobType) => void;
|
||||
removeAttachmentDownloadJobsForMessage: (messageId: string) => void;
|
||||
removeAllBackupAttachmentDownloadJobs: () => void;
|
||||
|
||||
getNextAttachmentBackupJobs: (options: {
|
||||
|
|
|
@ -488,6 +488,7 @@ export const DataWriter: ServerWritableInterface = {
|
|||
saveAttachmentDownloadJobs,
|
||||
resetAttachmentDownloadActive,
|
||||
removeAttachmentDownloadJob,
|
||||
removeAttachmentDownloadJobsForMessage,
|
||||
removeAllBackupAttachmentDownloadJobs,
|
||||
|
||||
getNextAttachmentBackupJobs,
|
||||
|
@ -5129,6 +5130,18 @@ function removeAttachmentDownloadJob(
|
|||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
function removeAttachmentDownloadJobsForMessage(
|
||||
db: WritableDB,
|
||||
messageId: string
|
||||
): void {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM attachment_downloads
|
||||
WHERE messageId = ${messageId}
|
||||
`;
|
||||
|
||||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
// Backup Attachments
|
||||
|
||||
function clearAllAttachmentBackupJobs(db: WritableDB): void {
|
||||
|
|
|
@ -731,7 +731,7 @@ export function setQuoteByMessageId(
|
|||
}
|
||||
|
||||
const message = messageId
|
||||
? await __DEPRECATED$getMessageById(messageId)
|
||||
? await __DEPRECATED$getMessageById(messageId, 'setQuoteByMessageId')
|
||||
: undefined;
|
||||
const state = getState();
|
||||
|
||||
|
|
|
@ -187,7 +187,10 @@ import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPe
|
|||
import { getConversationIdForLogging } from '../../util/idForLogging';
|
||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||
import MessageSender from '../../textsecure/SendMessage';
|
||||
import { AttachmentDownloadUrgency } from '../../jobs/AttachmentDownloadManager';
|
||||
import {
|
||||
AttachmentDownloadManager,
|
||||
AttachmentDownloadUrgency,
|
||||
} from '../../jobs/AttachmentDownloadManager';
|
||||
import type {
|
||||
DeleteForMeSyncEventData,
|
||||
MessageToDelete,
|
||||
|
@ -1083,6 +1086,7 @@ export const actions = {
|
|||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
blockGroupLinkRequests,
|
||||
cancelAttachmentDownload,
|
||||
cancelConversationVerification,
|
||||
changeHasGroupLink,
|
||||
clearCancelledConversationVerification,
|
||||
|
@ -1405,7 +1409,10 @@ function markMessageRead(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'markMessageRead'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`markMessageRead: failed to load message ${messageId}`);
|
||||
}
|
||||
|
@ -1759,7 +1766,10 @@ function deleteMessages({
|
|||
await Promise.all(
|
||||
messageIds.map(
|
||||
async (messageId): Promise<MessageToDelete | undefined> => {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'deleteMessages'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`deleteMessages: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -1919,7 +1929,9 @@ function setMessageToEdit(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = (await __DEPRECATED$getMessageById(messageId))?.attributes;
|
||||
const message = (
|
||||
await __DEPRECATED$getMessageById(messageId, 'setMessageToEdit')
|
||||
)?.attributes;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
@ -2012,7 +2024,10 @@ function generateNewGroupLink(
|
|||
* replace it with an actual action that fits in with the redux approach.
|
||||
*/
|
||||
export const markViewed = (messageId: string): void => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'markViewed'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`markViewed: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -2276,7 +2291,10 @@ function kickOffAttachmentDownload(
|
|||
options: Readonly<{ messageId: string }>
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(options.messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
options.messageId,
|
||||
'kickOffAttachmentDownload'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
|
||||
|
@ -2301,6 +2319,47 @@ function kickOffAttachmentDownload(
|
|||
};
|
||||
}
|
||||
|
||||
function cancelAttachmentDownload({
|
||||
messageId,
|
||||
}: Readonly<{ messageId: string }>): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
NoopActionType
|
||||
> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'cancelAttachmentDownload'
|
||||
);
|
||||
if (!message) {
|
||||
log.warn(`cancelAttachmentDownload: Message ${messageId} missing!`);
|
||||
} else {
|
||||
message.set({
|
||||
attachments: (message.get('attachments') || []).map(attachment => ({
|
||||
...attachment,
|
||||
pending: false,
|
||||
})),
|
||||
});
|
||||
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
await DataWriter.saveMessage(message.attributes, { ourAci });
|
||||
}
|
||||
|
||||
// A click kicks off downloads for every attachment in a message, so cancel does too
|
||||
await AttachmentDownloadManager.cancelJobs(job => {
|
||||
return job.messageId === messageId;
|
||||
});
|
||||
|
||||
await DataWriter.removeAttachmentDownloadJobsForMessage(messageId);
|
||||
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
type AttachmentOptions = ReadonlyDeep<{
|
||||
messageId: string;
|
||||
attachment: AttachmentType;
|
||||
|
@ -2310,7 +2369,10 @@ function markAttachmentAsCorrupted(
|
|||
options: AttachmentOptions
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(options.messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
options.messageId,
|
||||
'markAttachmentAsCorrupted'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
|
||||
|
@ -2329,7 +2391,10 @@ function openGiftBadge(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'openGiftBadge'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`openGiftBadge: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -2349,7 +2414,10 @@ function retryMessageSend(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'retryMessageSend'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`retryMessageSend: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -2366,7 +2434,10 @@ export function copyMessageText(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'copyMessageText'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`copy: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -2385,7 +2456,10 @@ export function retryDeleteForEveryone(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'retryDeleteForEveryone'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -3172,7 +3246,12 @@ function pushPanelForConversation(
|
|||
|
||||
const message =
|
||||
conversations.messagesLookup[messageId] ||
|
||||
(await __DEPRECATED$getMessageById(messageId))?.attributes;
|
||||
(
|
||||
await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'pushPanelForConversation'
|
||||
)
|
||||
)?.attributes;
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
'pushPanelForConversation: could not find message for MessageDetails'
|
||||
|
@ -3248,7 +3327,10 @@ function deleteMessagesForEveryone(
|
|||
await Promise.all(
|
||||
messageIds.map(async messageId => {
|
||||
try {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'deleteMessagesForEveryone'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`deleteMessageForEveryone: Message ${messageId} missing!`
|
||||
|
@ -3959,7 +4041,10 @@ export function saveAttachmentFromMessage(
|
|||
providedAttachment?: AttachmentType
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'saveAttachmentFromMessage'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`saveAttachmentFromMessage: Message ${messageId} missing!`
|
||||
|
@ -4052,7 +4137,10 @@ export function scrollToMessage(
|
|||
throw new Error('scrollToMessage: No conversation found');
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'scrollToMessage'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`scrollToMessage: failed to load message ${messageId}`);
|
||||
}
|
||||
|
@ -4066,7 +4154,12 @@ export function scrollToMessage(
|
|||
|
||||
let isInMemory = true;
|
||||
|
||||
if (!window.MessageCache.__DEPRECATED$getById(messageId)) {
|
||||
if (
|
||||
!window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'scrollToMessage/notInMemory'
|
||||
)
|
||||
) {
|
||||
isInMemory = false;
|
||||
}
|
||||
|
||||
|
@ -4497,7 +4590,10 @@ function onConversationOpened(
|
|||
log.info(`${logId}: Updating newly opened conversation state`);
|
||||
|
||||
if (messageId) {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'onConversationOpened'
|
||||
);
|
||||
|
||||
if (message) {
|
||||
drop(conversation.loadAndScroll(messageId));
|
||||
|
@ -4636,7 +4732,10 @@ function showArchivedConversations(): ShowArchivedConversationsActionType {
|
|||
}
|
||||
|
||||
function doubleCheckMissingQuoteReference(messageId: string): NoopActionType {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'doubleCheckMissingQuoteReference'
|
||||
);
|
||||
if (message) {
|
||||
void message.doubleCheckMissingQuoteReference();
|
||||
}
|
||||
|
|
|
@ -156,7 +156,10 @@ function showLightboxForViewOnceMedia(
|
|||
return async dispatch => {
|
||||
log.info('showLightboxForViewOnceMedia: attempting to display message');
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'showLightboxForViewOnceMedia'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`showLightboxForViewOnceMedia: Message ${messageId} missing!`
|
||||
|
@ -250,7 +253,10 @@ function showLightbox(opts: {
|
|||
return async (dispatch, getState) => {
|
||||
const { attachment, messageId } = opts;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'showLightbox'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(`showLightbox: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -387,7 +393,10 @@ function showLightboxForAdjacentMessage(
|
|||
const [media] = lightbox.media;
|
||||
const { id: messageId, receivedAt, sentAt } = media.message;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'showLightboxForAdjacentMessage'
|
||||
);
|
||||
if (!message) {
|
||||
log.warn('showLightboxForAdjacentMessage: original message is gone');
|
||||
dispatch({
|
||||
|
|
|
@ -382,7 +382,10 @@ function markStoryRead(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'markStoryRead'
|
||||
);
|
||||
|
||||
if (!message) {
|
||||
log.warn(`markStoryRead: no message found ${messageId}`);
|
||||
|
@ -521,7 +524,10 @@ function queueStoryDownload(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(storyId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
storyId,
|
||||
'queueStoryDownload'
|
||||
);
|
||||
|
||||
if (message) {
|
||||
// We want to ensure that we re-hydrate the story reply context with the
|
||||
|
@ -1396,7 +1402,10 @@ function removeAllContactStories(
|
|||
const messages = (
|
||||
await Promise.all(
|
||||
messageIds.map(async messageId => {
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'removeAllContactStories'
|
||||
);
|
||||
|
||||
if (!message) {
|
||||
log.warn(`${logId}: no message found ${messageId}`);
|
||||
|
|
|
@ -152,7 +152,6 @@ import { CallMode, CallDirection } from '../../types/CallDisposition';
|
|||
import { getCallIdFromEra } from '../../util/callDisposition';
|
||||
import { LONG_MESSAGE } from '../../types/MIME';
|
||||
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification';
|
||||
import { formatFileSize } from '../../util/formatFileSize';
|
||||
|
||||
export { isIncoming, isOutgoing, isStory };
|
||||
|
||||
|
@ -1837,12 +1836,11 @@ export function getPropsForAttachment(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const { path, pending, size, screenshot, thumbnail, thumbnailFromBackup } =
|
||||
const { path, pending, screenshot, thumbnail, thumbnailFromBackup } =
|
||||
attachment;
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
fileSize: size ? formatFileSize(size) : undefined,
|
||||
isVoiceMessage: isVoiceMessage(attachment),
|
||||
pending,
|
||||
url: path ? getLocalAttachmentUrl(attachment) : undefined,
|
||||
|
|
|
@ -20,7 +20,8 @@ export const SmartEditHistoryMessagesModal = memo(
|
|||
const platform = useSelector(getPlatform);
|
||||
|
||||
const { closeEditHistoryModal } = useGlobalModalActions();
|
||||
const { kickOffAttachmentDownload } = useConversationsActions();
|
||||
const { cancelAttachmentDownload, kickOffAttachmentDownload } =
|
||||
useConversationsActions();
|
||||
const { showLightbox } = useLightboxActions();
|
||||
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
|
@ -46,6 +47,7 @@ export const SmartEditHistoryMessagesModal = memo(
|
|||
|
||||
return (
|
||||
<EditHistoryMessagesModal
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
closeEditHistoryModal={closeEditHistoryModal}
|
||||
editHistoryMessages={editHistoryMessages}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
|
|
|
@ -118,7 +118,8 @@ function SmartForwardMessagesModalInner({
|
|||
return { draft, originalMessage: null };
|
||||
}
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
draft.originalMessageId
|
||||
draft.originalMessageId,
|
||||
'doForwardMessages'
|
||||
);
|
||||
strictAssert(message, 'no message found');
|
||||
return {
|
||||
|
|
|
@ -39,19 +39,20 @@ export const SmartMessageDetail = memo(
|
|||
const theme = useSelector(getTheme);
|
||||
const { checkForAccount } = useAccountsActions();
|
||||
const {
|
||||
cancelAttachmentDownload,
|
||||
clearTargetedMessage: clearSelectedMessage,
|
||||
doubleCheckMissingQuoteReference,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
messageExpanded,
|
||||
openGiftBadge,
|
||||
retryMessageSend,
|
||||
popPanelForConversation,
|
||||
pushPanelForConversation,
|
||||
retryMessageSend,
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
showConversation,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
showConversation,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showSpoiler,
|
||||
|
@ -91,6 +92,7 @@ export const SmartMessageDetail = memo(
|
|||
i18n={i18n}
|
||||
platform={platform}
|
||||
interactionMode={interactionMode}
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
message={message}
|
||||
|
|
|
@ -115,27 +115,28 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||
|
||||
const {
|
||||
blockGroupLinkRequests,
|
||||
cancelAttachmentDownload,
|
||||
clearTargetedMessage: clearSelectedMessage,
|
||||
copyMessageText,
|
||||
doubleCheckMissingQuoteReference,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
messageExpanded,
|
||||
openGiftBadge,
|
||||
pushPanelForConversation,
|
||||
copyMessageText,
|
||||
retryDeleteForEveryone,
|
||||
retryMessageSend,
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
targetMessage,
|
||||
toggleSelectMessage,
|
||||
setMessageToEdit,
|
||||
showConversation,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
showConversation,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showSpoiler,
|
||||
startConversation,
|
||||
targetMessage,
|
||||
toggleSelectMessage,
|
||||
} = useConversationsActions();
|
||||
|
||||
const { reactToMessage, scrollToQuotedMessage, setQuoteByMessageId } =
|
||||
|
@ -203,6 +204,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||
checkForAccount={checkForAccount}
|
||||
clearTargetedMessage={clearSelectedMessage}
|
||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
messageExpanded={messageExpanded}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import type {
|
||||
AttachmentType,
|
||||
AttachmentDraftType,
|
||||
ThumbnailType,
|
||||
AttachmentForUIType,
|
||||
|
@ -12,7 +11,7 @@ import type {
|
|||
import { IMAGE_JPEG } from '../../types/MIME';
|
||||
|
||||
export const fakeAttachment = (
|
||||
overrides: Partial<AttachmentType> = {}
|
||||
overrides: Partial<AttachmentForUIType> = {}
|
||||
): AttachmentForUIType => ({
|
||||
contentType: IMAGE_JPEG,
|
||||
width: 800,
|
||||
|
|
|
@ -100,9 +100,9 @@ describe('processDataMessage', () => {
|
|||
assert.deepStrictEqual(out.attachments, [
|
||||
{
|
||||
...PROCESSED_ATTACHMENT,
|
||||
chunkSize: 2,
|
||||
downloadPath: 'random-path',
|
||||
incrementalMac: 'AAAA',
|
||||
chunkSize: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('convertFilePointerToAttachment', () => {
|
|||
digest: Bytes.toBase64(Bytes.fromString('digest')),
|
||||
uploadTimestamp: 1970,
|
||||
incrementalMac: Bytes.toBase64(Bytes.fromString('incrementalMac')),
|
||||
incrementalMacChunkSize: 1000,
|
||||
chunkSize: 1000,
|
||||
downloadPath: 'downloadPath',
|
||||
});
|
||||
});
|
||||
|
@ -102,7 +102,7 @@ describe('convertFilePointerToAttachment', () => {
|
|||
key: Bytes.toBase64(Bytes.fromString('key')),
|
||||
digest: Bytes.toBase64(Bytes.fromString('digest')),
|
||||
incrementalMac: Bytes.toBase64(Bytes.fromString('incrementalMac')),
|
||||
incrementalMacChunkSize: 1000,
|
||||
chunkSize: 1000,
|
||||
backupLocator: {
|
||||
mediaName: 'mediaName',
|
||||
cdnNumber: 3,
|
||||
|
@ -135,7 +135,7 @@ describe('convertFilePointerToAttachment', () => {
|
|||
fileName: 'filename',
|
||||
caption: 'caption',
|
||||
incrementalMac: Bytes.toBase64(Bytes.fromString('incrementalMac')),
|
||||
incrementalMacChunkSize: 1000,
|
||||
chunkSize: 1000,
|
||||
size: 0,
|
||||
error: true,
|
||||
});
|
||||
|
@ -163,7 +163,7 @@ describe('convertFilePointerToAttachment', () => {
|
|||
key: undefined,
|
||||
digest: undefined,
|
||||
incrementalMac: undefined,
|
||||
incrementalMacChunkSize: undefined,
|
||||
chunkSize: undefined,
|
||||
backupLocator: undefined,
|
||||
});
|
||||
});
|
||||
|
@ -190,7 +190,7 @@ function composeAttachment(
|
|||
fileName: 'filename',
|
||||
caption: 'caption',
|
||||
incrementalMac: 'incrementalMac',
|
||||
incrementalMacChunkSize: 1000,
|
||||
chunkSize: 1000,
|
||||
uploadTimestamp: 1234,
|
||||
localKey: Bytes.toBase64(generateKeys()),
|
||||
isReencryptableToSameDigest: true,
|
||||
|
|
|
@ -142,15 +142,20 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager(
|
|||
const decryptAttachmentV2ToSink = sinon.stub();
|
||||
|
||||
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
|
||||
const abortController = new AbortController();
|
||||
runJob = sandbox.stub().callsFake((job: AttachmentBackupJobType) => {
|
||||
return runAttachmentBackupJob(job, false, {
|
||||
return runAttachmentBackupJob(
|
||||
job,
|
||||
{ abortSignal: abortController.signal, isLastAttempt: false },
|
||||
{
|
||||
// @ts-expect-error incomplete stubbing
|
||||
backupsService,
|
||||
backupMediaBatch,
|
||||
getAbsoluteAttachmentPath,
|
||||
encryptAndUploadAttachment,
|
||||
decryptAttachmentV2ToSink,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
backupManager = new AttachmentBackupManager({
|
||||
|
|
|
@ -444,8 +444,11 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
|
||||
describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let deleteDownloadData: sinon.SinonStub;
|
||||
let downloadAttachment: sinon.SinonStub;
|
||||
let processNewAttachment: sinon.SinonStub;
|
||||
const abortController = new AbortController();
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
downloadAttachment = sandbox.stub().returns({
|
||||
|
@ -470,7 +473,9 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
processNewAttachment,
|
||||
},
|
||||
|
@ -478,10 +483,13 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
|
||||
assert.strictEqual(result.downloadedVariant, AttachmentVariant.Default);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
|
||||
const downloadCallArgs = downloadAttachment.getCall(0).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs.options.variant,
|
||||
AttachmentVariant.Default
|
||||
);
|
||||
});
|
||||
it('will download thumbnail if attachment is from backup', async () => {
|
||||
const job = composeJob({
|
||||
|
@ -497,7 +505,9 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
processNewAttachment,
|
||||
},
|
||||
|
@ -521,10 +531,13 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
'/path/to/file'
|
||||
);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.ThumbnailFromBackup,
|
||||
});
|
||||
|
||||
const downloadCallArgs = downloadAttachment.getCall(0).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs.options.variant,
|
||||
AttachmentVariant.ThumbnailFromBackup
|
||||
);
|
||||
});
|
||||
it('will download full size if thumbnail already backed up', async () => {
|
||||
const job = composeJob({
|
||||
|
@ -543,17 +556,22 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
processNewAttachment,
|
||||
},
|
||||
});
|
||||
assert.strictEqual(result.downloadedVariant, AttachmentVariant.Default);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
|
||||
const downloadCallArgs = downloadAttachment.getCall(0).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs.options.variant,
|
||||
AttachmentVariant.Default
|
||||
);
|
||||
});
|
||||
|
||||
it('will attempt to download full size if thumbnail fails', async () => {
|
||||
|
@ -575,7 +593,9 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
processNewAttachment,
|
||||
},
|
||||
|
@ -583,14 +603,20 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
);
|
||||
|
||||
assert.strictEqual(downloadAttachment.callCount, 2);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.ThumbnailFromBackup,
|
||||
});
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(1).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
|
||||
const downloadCallArgs0 = downloadAttachment.getCall(0).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs0.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs0.options.variant,
|
||||
AttachmentVariant.ThumbnailFromBackup
|
||||
);
|
||||
|
||||
const downloadCallArgs1 = downloadAttachment.getCall(1).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs1.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs1.options.variant,
|
||||
AttachmentVariant.Default
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('message not visible', () => {
|
||||
|
@ -608,21 +634,26 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
abortSignal: abortController.signal,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
processNewAttachment,
|
||||
},
|
||||
});
|
||||
assert.strictEqual(result.downloadedVariant, AttachmentVariant.Default);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
|
||||
const downloadCallArgs = downloadAttachment.getCall(0).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs.options.variant,
|
||||
AttachmentVariant.Default
|
||||
);
|
||||
});
|
||||
it('will fallback to thumbnail if main download fails and backuplocator exists', async () => {
|
||||
downloadAttachment = sandbox.stub().callsFake(({ variant }) => {
|
||||
if (variant === AttachmentVariant.Default) {
|
||||
downloadAttachment = sandbox.stub().callsFake(({ options }) => {
|
||||
if (options.variant === AttachmentVariant.Default) {
|
||||
throw new Error('error while downloading');
|
||||
}
|
||||
return {
|
||||
|
@ -645,7 +676,9 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
abortSignal: abortController.signal,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
processNewAttachment,
|
||||
},
|
||||
|
@ -655,19 +688,25 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
AttachmentVariant.ThumbnailFromBackup
|
||||
);
|
||||
assert.strictEqual(downloadAttachment.callCount, 2);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(1).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.ThumbnailFromBackup,
|
||||
});
|
||||
|
||||
const downloadCallArgs0 = downloadAttachment.getCall(0).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs0.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs0.options.variant,
|
||||
AttachmentVariant.Default
|
||||
);
|
||||
|
||||
const downloadCallArgs1 = downloadAttachment.getCall(1).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs1.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs1.options.variant,
|
||||
AttachmentVariant.ThumbnailFromBackup
|
||||
);
|
||||
});
|
||||
|
||||
it("won't fallback to thumbnail if main download fails and no backup locator", async () => {
|
||||
downloadAttachment = sandbox.stub().callsFake(({ variant }) => {
|
||||
if (variant === AttachmentVariant.Default) {
|
||||
downloadAttachment = sandbox.stub().callsFake(({ options }) => {
|
||||
if (options.variant === AttachmentVariant.Default) {
|
||||
throw new Error('error while downloading');
|
||||
}
|
||||
return {
|
||||
|
@ -686,7 +725,9 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
abortSignal: abortController.signal,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
processNewAttachment,
|
||||
},
|
||||
|
@ -694,10 +735,13 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
|||
);
|
||||
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
|
||||
const downloadCallArgs = downloadAttachment.getCall(0).args[0];
|
||||
assert.deepStrictEqual(downloadCallArgs.attachment, job.attachment);
|
||||
assert.deepStrictEqual(
|
||||
downloadCallArgs.options.variant,
|
||||
AttachmentVariant.Default
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -97,7 +97,10 @@ describe('MessageCache', () => {
|
|||
'same objects from mc.__DEPRECATED$register'
|
||||
);
|
||||
|
||||
const messageById = window.MessageCache.__DEPRECATED$getById(message1.id);
|
||||
const messageById = window.MessageCache.__DEPRECATED$getById(
|
||||
message1.id,
|
||||
'test'
|
||||
);
|
||||
|
||||
assert.strictEqual(message1, messageById, 'same objects from mc.getById');
|
||||
|
||||
|
@ -123,7 +126,8 @@ describe('MessageCache', () => {
|
|||
);
|
||||
|
||||
const newMessageById = window.MessageCache.__DEPRECATED$getById(
|
||||
message1.id
|
||||
message1.id,
|
||||
'test'
|
||||
);
|
||||
assert.deepEqual(
|
||||
message1.attributes,
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { DataWriter } from '../../sql/Client';
|
||||
import { IMAGE_PNG } from '../../types/MIME';
|
||||
import {
|
||||
|
@ -22,6 +24,7 @@ describe('utils/downloadAttachment', () => {
|
|||
contentType: IMAGE_PNG,
|
||||
digest: 'digest',
|
||||
};
|
||||
const abortController = new AbortController();
|
||||
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
const fakeServer = {};
|
||||
|
@ -42,6 +45,10 @@ describe('utils/downloadAttachment', () => {
|
|||
};
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
|
@ -53,6 +60,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -72,6 +81,10 @@ describe('utils/downloadAttachment', () => {
|
|||
await assert.isRejected(
|
||||
downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
|
@ -86,6 +99,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -103,6 +118,10 @@ describe('utils/downloadAttachment', () => {
|
|||
};
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
|
@ -114,6 +133,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -135,6 +156,10 @@ describe('utils/downloadAttachment', () => {
|
|||
};
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
|
@ -146,6 +171,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -155,6 +182,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -176,6 +205,10 @@ describe('utils/downloadAttachment', () => {
|
|||
};
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
|
@ -187,6 +220,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -196,6 +231,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -218,6 +255,10 @@ describe('utils/downloadAttachment', () => {
|
|||
await assert.isRejected(
|
||||
downloadAttachment({
|
||||
attachment,
|
||||
options: {
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
|
@ -231,6 +272,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
@ -240,6 +283,8 @@ describe('utils/downloadAttachment', () => {
|
|||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
onSizeUpdate: noop,
|
||||
abortSignal: abortController.signal,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -1328,6 +1328,7 @@ export type WebAPIType = {
|
|||
disableRetries?: boolean;
|
||||
timeout?: number;
|
||||
downloadOffset?: number;
|
||||
abortSignal: AbortSignal;
|
||||
};
|
||||
}) => Promise<Readable>;
|
||||
getAttachment: (args: {
|
||||
|
@ -1337,6 +1338,7 @@ export type WebAPIType = {
|
|||
disableRetries?: boolean;
|
||||
timeout?: number;
|
||||
downloadOffset?: number;
|
||||
abortSignal?: AbortSignal;
|
||||
};
|
||||
}) => Promise<Readable>;
|
||||
getAttachmentUploadForm: () => Promise<AttachmentUploadFormResponseType>;
|
||||
|
@ -3784,6 +3786,7 @@ export function initialize({
|
|||
disableRetries?: boolean;
|
||||
timeout?: number;
|
||||
downloadOffset?: number;
|
||||
abortSignal?: AbortSignal;
|
||||
};
|
||||
}) {
|
||||
return _getAttachment({
|
||||
|
|
|
@ -102,16 +102,18 @@ export async function downloadAttachment(
|
|||
server: WebAPIType,
|
||||
attachment: ProcessedAttachment,
|
||||
options: {
|
||||
variant?: AttachmentVariant;
|
||||
disableRetries?: boolean;
|
||||
timeout?: number;
|
||||
mediaTier?: MediaTier;
|
||||
logPrefix?: string;
|
||||
} = { variant: AttachmentVariant.Default }
|
||||
mediaTier?: MediaTier;
|
||||
onSizeUpdate: (totalBytes: number) => void;
|
||||
timeout?: number;
|
||||
variant: AttachmentVariant;
|
||||
abortSignal: AbortSignal;
|
||||
}
|
||||
): Promise<ReencryptedAttachmentV2 & { size?: number }> {
|
||||
const logId = `downloadAttachment/${options.logPrefix ?? ''}`;
|
||||
|
||||
const { chunkSize, digest, incrementalMac, key, size } = attachment;
|
||||
const { digest, incrementalMac, chunkSize, key, size } = attachment;
|
||||
|
||||
strictAssert(digest, `${logId}: missing digest`);
|
||||
strictAssert(key, `${logId}: missing key`);
|
||||
|
@ -127,7 +129,7 @@ export async function downloadAttachment(
|
|||
let downloadOffset = 0;
|
||||
if (downloadPath) {
|
||||
const absoluteDownloadPath =
|
||||
window.Signal.Migrations.getAbsoluteAttachmentPath(downloadPath);
|
||||
window.Signal.Migrations.getAbsoluteDownloadsPath(downloadPath);
|
||||
try {
|
||||
({ size: downloadOffset } = await stat(absoluteDownloadPath));
|
||||
} catch (error) {
|
||||
|
@ -173,10 +175,11 @@ export async function downloadAttachment(
|
|||
},
|
||||
});
|
||||
downloadResult = await downloadToDisk({
|
||||
downloadStream,
|
||||
size,
|
||||
downloadPath,
|
||||
downloadOffset,
|
||||
downloadPath,
|
||||
downloadStream,
|
||||
onSizeUpdate: options.onSizeUpdate,
|
||||
size,
|
||||
});
|
||||
} else {
|
||||
const mediaId =
|
||||
|
@ -209,6 +212,7 @@ export async function downloadAttachment(
|
|||
downloadStream,
|
||||
downloadPath,
|
||||
downloadOffset,
|
||||
onSizeUpdate: options.onSizeUpdate,
|
||||
size: getAttachmentCiphertextLength(
|
||||
options.variant === AttachmentVariant.ThumbnailFromBackup
|
||||
? // be generous, accept downloads of up to twice what we expect for thumbnail
|
||||
|
@ -275,19 +279,23 @@ export async function downloadAttachment(
|
|||
}
|
||||
}
|
||||
} finally {
|
||||
if (!downloadPath) {
|
||||
await safeUnlink(cipherTextAbsolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadToDisk({
|
||||
downloadStream,
|
||||
downloadPath,
|
||||
downloadOffset = 0,
|
||||
downloadPath,
|
||||
downloadStream,
|
||||
onSizeUpdate,
|
||||
size,
|
||||
}: {
|
||||
downloadStream: Readable;
|
||||
downloadPath?: string;
|
||||
downloadOffset?: number;
|
||||
downloadPath?: string;
|
||||
downloadStream: Readable;
|
||||
onSizeUpdate: (totalBytes: number) => void;
|
||||
size: number;
|
||||
}): Promise<{ absolutePath: string; downloadSize: number }> {
|
||||
const absoluteTargetPath = downloadPath
|
||||
|
@ -317,8 +325,12 @@ async function downloadToDisk({
|
|||
await pipeline(
|
||||
downloadStream,
|
||||
checkSize(targetSize),
|
||||
measureSize(bytesSeen => {
|
||||
measureSize({
|
||||
downloadOffset,
|
||||
onSizeUpdate,
|
||||
onComplete: bytesSeen => {
|
||||
downloadSize = bytesSeen;
|
||||
},
|
||||
}),
|
||||
writeStream
|
||||
);
|
||||
|
|
|
@ -66,7 +66,6 @@ export type AttachmentType = {
|
|||
/** For messages not already on disk, this will be a data url */
|
||||
url?: string;
|
||||
size: number;
|
||||
fileSize?: string;
|
||||
pending?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
@ -88,8 +87,9 @@ export type AttachmentType = {
|
|||
textAttachment?: TextAttachmentType;
|
||||
wasTooBig?: boolean;
|
||||
|
||||
totalDownloaded?: number;
|
||||
incrementalMac?: string;
|
||||
incrementalMacChunkSize?: number;
|
||||
chunkSize?: number;
|
||||
|
||||
backupLocator?: {
|
||||
mediaName: string;
|
||||
|
@ -779,6 +779,21 @@ export function isDownloaded(
|
|||
return Boolean(resolved && (resolved.path || resolved.textAttachment));
|
||||
}
|
||||
|
||||
export function isReadyToView(
|
||||
attachment?: Pick<
|
||||
AttachmentType,
|
||||
'incrementalMac' | 'chunkSize' | 'path' | 'textAttachment'
|
||||
>
|
||||
): boolean {
|
||||
const fullyDownloaded = isDownloaded(attachment);
|
||||
if (fullyDownloaded) {
|
||||
return fullyDownloaded;
|
||||
}
|
||||
|
||||
const resolved = resolveNestedAttachment(attachment);
|
||||
return Boolean(resolved && (resolved.path || resolved.textAttachment));
|
||||
}
|
||||
|
||||
export function hasNotResolved(attachment?: AttachmentType): boolean {
|
||||
const resolved = resolveNestedAttachment(attachment);
|
||||
return Boolean(resolved && !resolved.url && !resolved.textAttachment);
|
||||
|
|
|
@ -12,7 +12,7 @@ export function getMessageModelLogger(model: MessageModel): MessageModel {
|
|||
}
|
||||
|
||||
const proxyHandler: ProxyHandler<MessageModel> = {
|
||||
get(target: MessageModel, property: keyof MessageModel) {
|
||||
get(_: MessageModel, property: keyof MessageModel) {
|
||||
// Allowed set of attributes & methods
|
||||
if (property === 'attributes') {
|
||||
return model.attributes;
|
||||
|
@ -31,17 +31,17 @@ export function getMessageModelLogger(model: MessageModel): MessageModel {
|
|||
}
|
||||
|
||||
if (property === 'registerLocations') {
|
||||
return target.registerLocations;
|
||||
return model.registerLocations;
|
||||
}
|
||||
|
||||
// Disallowed set of methods & attributes
|
||||
|
||||
if (typeof target[property] === 'function') {
|
||||
return target[property].bind(target);
|
||||
if (typeof model[property] === 'function') {
|
||||
return model[property].bind(model);
|
||||
}
|
||||
|
||||
if (typeof target[property] !== 'undefined') {
|
||||
return target[property];
|
||||
if (typeof model[property] !== 'undefined') {
|
||||
return model[property];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
|
@ -66,7 +66,10 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
|
|||
let numMessagesQueued = 0;
|
||||
await Promise.all(
|
||||
messageIdsToDownload.map(async messageId => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'flushAttachmentDownloadQueue'
|
||||
);
|
||||
if (!message) {
|
||||
log.warn(
|
||||
'attachmentDownloadQueue: message not found in messageCache, maybe it was deleted?'
|
||||
|
|
|
@ -83,7 +83,7 @@ export type CdnFieldsType = Pick<
|
|||
| 'cdnNumber'
|
||||
| 'digest'
|
||||
| 'incrementalMac'
|
||||
| 'incrementalMacChunkSize'
|
||||
| 'chunkSize'
|
||||
| 'isReencryptableToSameDigest'
|
||||
| 'iv'
|
||||
| 'key'
|
||||
|
@ -104,7 +104,7 @@ export function copyCdnFields(
|
|||
incrementalMac: uploaded.incrementalMac
|
||||
? Bytes.toBase64(uploaded.incrementalMac)
|
||||
: undefined,
|
||||
incrementalMacChunkSize: dropNull(uploaded.chunkSize),
|
||||
chunkSize: dropNull(uploaded.chunkSize),
|
||||
isReencryptableToSameDigest: uploaded.isReencryptableToSameDigest,
|
||||
iv: Bytes.toBase64(uploaded.iv),
|
||||
key: Bytes.toBase64(uploaded.key),
|
||||
|
|
|
@ -1356,7 +1356,10 @@ export async function updateCallHistoryFromLocalEvent(
|
|||
|
||||
export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void {
|
||||
messageIds.forEach(messageId => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(messageId);
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'updateDeletedMessages'
|
||||
);
|
||||
const conversation = message?.getConversation();
|
||||
if (message == null || conversation == null) {
|
||||
return;
|
||||
|
|
|
@ -9,7 +9,10 @@ import * as log from '../logging/log';
|
|||
export async function deleteGroupStoryReplyForEveryone(
|
||||
replyMessageId: string
|
||||
): Promise<void> {
|
||||
const messageModel = await __DEPRECATED$getMessageById(replyMessageId);
|
||||
const messageModel = await __DEPRECATED$getMessageById(
|
||||
replyMessageId,
|
||||
'deleteGroupStoryReplyForEveryone'
|
||||
);
|
||||
|
||||
if (!messageModel) {
|
||||
log.warn(
|
||||
|
|
|
@ -47,7 +47,10 @@ export async function deleteStoryForEveryone(
|
|||
}
|
||||
|
||||
const logId = `deleteStoryForEveryone(${story.messageId})`;
|
||||
const message = await __DEPRECATED$getMessageById(story.messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
story.messageId,
|
||||
'deleteStoryForEveryone'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error('Story not found');
|
||||
}
|
||||
|
|
|
@ -18,11 +18,15 @@ export class AttachmentPermanentlyUndownloadableError extends Error {}
|
|||
|
||||
export async function downloadAttachment({
|
||||
attachment,
|
||||
variant = AttachmentVariant.Default,
|
||||
options: { variant = AttachmentVariant.Default, onSizeUpdate, abortSignal },
|
||||
dependencies = { downloadAttachmentFromServer: doDownloadAttachment },
|
||||
}: {
|
||||
attachment: AttachmentType;
|
||||
options: {
|
||||
variant?: AttachmentVariant;
|
||||
onSizeUpdate: (totalBytes: number) => void;
|
||||
abortSignal: AbortSignal;
|
||||
};
|
||||
dependencies?: { downloadAttachmentFromServer: typeof doDownloadAttachment };
|
||||
}): Promise<ReencryptedAttachmentV2> {
|
||||
const attachmentId = getAttachmentIdForLogging(attachment);
|
||||
|
@ -54,9 +58,11 @@ export async function downloadAttachment({
|
|||
server,
|
||||
migratedAttachment,
|
||||
{
|
||||
variant,
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
logPrefix: dataId,
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
onSizeUpdate,
|
||||
variant,
|
||||
abortSignal,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
|
@ -80,9 +86,11 @@ export async function downloadAttachment({
|
|||
server,
|
||||
migratedAttachment,
|
||||
{
|
||||
variant,
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
logPrefix: dataId,
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
onSizeUpdate,
|
||||
variant,
|
||||
abortSignal,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
11
ts/util/dropZero.ts
Normal file
11
ts/util/dropZero.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
export function dropZero(value: number | null | undefined): number | undefined {
|
||||
if (isNumber(value) && value !== 0) {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
|
@ -2,6 +2,6 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import filesize from 'filesize';
|
||||
|
||||
export function formatFileSize(size: number): string {
|
||||
return filesize(size, { round: 0 });
|
||||
export function formatFileSize(size: number, decimals = 0): string {
|
||||
return filesize(size, { round: decimals });
|
||||
}
|
||||
|
|
|
@ -104,7 +104,8 @@ export async function markConversationRead(
|
|||
const allReadMessagesSync = allUnreadMessages
|
||||
.map(messageSyncData => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageSyncData.id
|
||||
messageSyncData.id,
|
||||
'markConversationRead'
|
||||
);
|
||||
// we update the in-memory MessageModel with fresh read/seen status
|
||||
if (message) {
|
||||
|
|
|
@ -20,7 +20,9 @@ export async function markOnboardingStoryAsRead(): Promise<boolean> {
|
|||
}
|
||||
|
||||
const messages = await Promise.all(
|
||||
existingOnboardingStoryMessageIds.map(__DEPRECATED$getMessageById)
|
||||
existingOnboardingStoryMessageIds.map(id =>
|
||||
__DEPRECATED$getMessageById(id, 'markOnboardingStoryAsRead')
|
||||
)
|
||||
);
|
||||
|
||||
const storyReadDate = Date.now();
|
||||
|
|
|
@ -35,7 +35,10 @@ export async function sendDeleteForEveryoneMessage(
|
|||
timestamp: targetTimestamp,
|
||||
id: messageId,
|
||||
} = options;
|
||||
const message = await __DEPRECATED$getMessageById(messageId);
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'sendDeleteForEveryoneMessage'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error('sendDeleteForEveryoneMessage: Cannot find message!');
|
||||
}
|
||||
|
|
|
@ -65,7 +65,10 @@ export async function sendEditedMessage(
|
|||
conversation.attributes
|
||||
)})`;
|
||||
|
||||
const targetMessage = await __DEPRECATED$getMessageById(targetMessageId);
|
||||
const targetMessage = await __DEPRECATED$getMessageById(
|
||||
targetMessageId,
|
||||
'sendEditedMessage'
|
||||
);
|
||||
strictAssert(targetMessage, 'could not find message to edit');
|
||||
|
||||
if (isGroupV1(conversation.attributes)) {
|
||||
|
|
|
@ -66,7 +66,7 @@ if (
|
|||
},
|
||||
getConversation: (id: string) => window.ConversationController.get(id),
|
||||
getMessageById: (id: string) =>
|
||||
window.MessageCache.__DEPRECATED$getById(id),
|
||||
window.MessageCache.__DEPRECATED$getById(id, 'SignalDebug'),
|
||||
getMessageBySentAt: (timestamp: number) =>
|
||||
window.MessageCache.findBySentAt(timestamp, () => true),
|
||||
getReduxState: () => window.reduxStore.getState(),
|
||||
|
|
Loading…
Reference in a new issue