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