Show dialog for permanently undownloadable attachments

This commit is contained in:
ayumi-signal 2025-02-11 13:47:54 -08:00 committed by GitHub
parent 9859383b1d
commit c235cdf58b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 734 additions and 55 deletions

View file

@ -1526,18 +1526,70 @@
"messageformat": "This media is not available",
"description": "Shown in info toast for messages with old image and video attachments which are no longer available for download. Also used for accessibility label for the download attachment button."
},
"icu:mediaNotAvailable--short": {
"messageformat": "Media not available",
"description": "Title of info dialog shown for messages with missing old image and video attachments which are no longer available for download."
},
"icu:attachmentNoLongerAvailable__learnMore": {
"messageformat": "Learn more",
"description": "Link in message placeholder and info toast for messages with old attachments which are no longer available for download."
},
"icu:fileNotAvailable": {
"messageformat": "This file is not available",
"description": "Shown in chat timeline for messages with old generic file attachments which are no longer available for download."
"icu:attachmentNotAvailable__file": {
"messageformat": "File not available",
"description": "Shown in chat timeline for messages with old file attachments which are no longer available for download."
},
"icu:voiceMessageNotAvailable": {
"messageformat": "This voice message is not available",
"icu:attachmentNotAvailable__longMessage": {
"messageformat": "This message is incomplete",
"description": "Shown in chat timeline for long messages when the complete messages are no longer available for download."
},
"icu:attachmentNotAvailable__sticker": {
"messageformat": "Sticker not available",
"description": "Shown in chat timeline for messages with old audio attachments which are no longer available for download."
},
"icu:attachmentNotAvailable__voice": {
"messageformat": "Voice message not available",
"description": "Shown in chat timeline for messages with old audio attachments which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__title--file": {
"messageformat": "File not available",
"description": "Title for info dialog for messages with old file attachments which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__title--long-text": {
"messageformat": "Complete message not available",
"description": "Title for info dialog for long messages which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__title--media": {
"messageformat": "Media not available",
"description": "Title for info dialog for messages with old visual media which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__title--sticker": {
"messageformat": "Sticker not available",
"description": "Title for info dialog for old sticker messages which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__title--voice-message": {
"messageformat": "Voice message not available",
"description": "Title for info dialog for messages with old voice messages which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__body--file": {
"messageformat": "This file was not transferred from your phone when this device was linked. Media and files older than 45 days at the time of device linking can not be synced.",
"description": "Body text for info dialog for messages with old file attachments which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__body--long-text": {
"messageformat": "The complete text in this message was not transferred from your phone when this device was linked. Long text messages older than 45 days at the time of device linking can not be synced.",
"description": "Body text for info dialog for long messages which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__body--media": {
"messageformat": "This media was not transferred from your phone when this device was linked. Media older than 45 days at the time of device linking can not be synced.",
"description": "Body text for info dialog for messages with old visual media which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__body--sticker": {
"messageformat": "This sticker was not transferred from your phone when this device was linked. Media and files older than 45 days at the time of device linking can not be synced.",
"description": "Body text for info dialog for old sticker messages which are no longer available for download."
},
"icu:AttachmentNotAvailableModal__body--voice-message": {
"messageformat": "This voice message was not transferred from your phone when this device was linked. Media and files older than 45 days at the time of device linking can not be synced.",
"description": "Body text for info dialog for messages with old voice messages which are no longer available for download."
},
"icu:save": {
"messageformat": "Save",
"description": "Used on save buttons"

View file

@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.61872 1.38128C2.27701 1.03957 1.72299 1.03957 1.38128 1.38128C1.03957 1.72299 1.03957 2.27701 1.38128 2.61872L21.3813 22.6187C21.723 22.9604 22.277 22.9604 22.6187 22.6187C22.9604 22.277 22.9604 21.723 22.6187 21.3813L2.61872 1.38128Z" fill="black"/>
<path d="M3.62507 6.98384L3.625 16.7368C3.62499 17.5456 3.62498 18.2056 3.66878 18.7417C3.71407 19.296 3.81053 19.7944 4.04735 20.2592C4.41886 20.9884 5.01166 21.5812 5.74079 21.9527C6.20556 22.1895 6.70399 22.2859 7.25835 22.3312C7.79438 22.375 8.45431 22.375 9.2631 22.375H14.7368C15.5456 22.375 16.2056 22.375 16.7416 22.3312C17.296 22.2859 17.7944 22.1895 18.2592 21.9527C18.3326 21.9153 18.4046 21.8757 18.4751 21.8339L17.148 20.5068C17.0069 20.541 16.8304 20.5682 16.5991 20.587C16.1428 20.6243 15.5545 20.625 14.7 20.625H9.3C8.44548 20.625 7.85722 20.6243 7.40086 20.587C6.95471 20.5506 6.71223 20.4836 6.53527 20.3934C6.13543 20.1897 5.81034 19.8646 5.60661 19.4647C5.51645 19.2878 5.44942 19.0453 5.41297 18.5992C5.37568 18.1428 5.375 17.5545 5.375 16.7V8.73377L3.62507 6.98384Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.625 15.2663V10.375H17.2632C16.4544 10.375 15.7944 10.375 15.2584 10.3312C14.704 10.2859 14.2056 10.1895 13.7408 9.95265C13.0117 9.58114 12.4189 8.98834 12.0473 8.25921C11.8105 7.79444 11.7141 7.29601 11.6688 6.74165C11.625 6.2056 11.625 5.54562 11.625 4.73679V3.37501H9.3C8.44548 3.37501 7.85722 3.37569 7.40086 3.41298C7.16961 3.43187 6.99307 3.45898 6.85202 3.49327L5.5249 2.16615C5.59542 2.12437 5.66742 2.08475 5.74079 2.04736C6.20556 1.81055 6.70399 1.71409 7.25835 1.66879C7.7944 1.62499 8.45438 1.625 9.26322 1.62501L12.3224 1.62496C12.6229 1.6246 12.9067 1.62427 13.1831 1.69063C13.4254 1.74881 13.6571 1.84477 13.8696 1.97499C14.112 2.12354 14.3125 2.32445 14.5247 2.53717L19.4628 7.47534C19.6756 7.68755 19.8765 7.888 20.025 8.1304C20.1552 8.3429 20.2512 8.57456 20.3094 8.8169C20.3757 9.09334 20.3754 9.37714 20.3751 9.67762L20.3749 17.0162L18.625 15.2663ZM13.375 3.86245L18.1376 8.625H17.3C16.4455 8.625 15.8572 8.62432 15.4009 8.58703C14.9547 8.55058 14.7122 8.48355 14.5353 8.39339C14.1354 8.18966 13.8103 7.86457 13.6066 7.46473C13.5164 7.28777 13.4494 7.04529 13.413 6.59914C13.3757 6.14278 13.375 5.55452 13.375 4.7V3.86245Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.11872 1.88128C2.77701 1.53957 2.22299 1.53957 1.88128 1.88128C1.53957 2.22299 1.53957 2.77701 1.88128 3.11872L20.3813 21.6187C20.723 21.9604 21.277 21.9604 21.6187 21.6187C21.9604 21.277 21.9604 20.723 21.6187 20.3813L3.11872 1.88128Z" fill="black"/>
<path d="M2.18922 7.17316C2.23053 6.66753 2.30013 6.20963 2.42522 5.78398L3.9352 7.29396L3.93341 7.31566C3.87568 8.02215 3.875 8.92544 3.875 10.2V16.125C3.875 16.4871 3.87547 16.6259 3.88276 16.7371C4.00174 18.5523 5.44767 19.9983 7.26292 20.1172C7.37413 20.1245 7.51293 20.125 7.875 20.125C8.49013 20.125 8.79075 19.9718 8.94378 19.8188C9.09681 19.6658 9.25 19.3651 9.25 18.75L9.24999 18.64C9.24984 17.7 9.24975 17.0969 9.35328 16.5764C9.54812 15.5969 10.0052 14.7156 10.6492 14.0079L11.8886 15.2474C11.4833 15.7138 11.1954 16.2854 11.0697 16.9178C11.0042 17.2467 11 17.6626 11 18.75C11 18.9242 10.9914 19.0961 10.973 19.2644C11.0193 19.2321 11.0669 19.1984 11.1159 19.1634C12.169 18.4099 13.0333 17.7916 13.8495 17.2082L15.1039 18.4627C14.2175 19.0962 13.2895 19.76 12.1341 20.5866C11.3993 21.1124 10.8016 21.4616 10.1153 21.6576C9.44285 21.8496 8.74798 21.875 7.875 21.875H7.83831C7.5252 21.875 7.32446 21.875 7.14846 21.8635C4.45689 21.6871 2.31293 19.5431 2.13651 16.8515C2.12497 16.6755 2.12498 16.4748 2.125 16.1616L2.125 10.1616C2.125 8.93403 2.12499 7.95926 2.18922 7.17316Z" fill="black"/>
<path d="M15.6087 12.25L17.3588 14H18.3435L17.7687 14.4099L19.0234 15.6646L20.008 14.9624C20.0133 14.9587 20.0186 14.9548 20.0238 14.9509C20.6414 14.4894 21.1522 14.0085 21.4739 13.3114C21.7868 12.6335 21.875 11.8489 21.875 10.875V10.1616C21.875 8.93401 21.875 7.95926 21.8108 7.17316C21.7451 6.36866 21.6077 5.68501 21.2892 5.0598C20.7738 4.04843 19.9516 3.22616 18.9402 2.71084C18.315 2.39228 17.6313 2.25494 16.8268 2.18922C16.0407 2.12499 15.066 2.12499 13.8384 2.125H10.1616C8.93403 2.12499 7.95926 2.12499 7.17316 2.18922C6.66753 2.23053 6.20963 2.30012 5.78398 2.42522L7.29396 3.9352L7.31566 3.9334C8.02215 3.87568 8.92545 3.875 10.2 3.875H13.8C15.0746 3.875 15.9779 3.87568 16.6843 3.9334C17.3806 3.99029 17.8083 4.09819 18.1457 4.2701C18.8278 4.61764 19.3824 5.1722 19.7299 5.85428C19.9018 6.19168 20.0097 6.61939 20.0666 7.31566C20.1243 8.02215 20.125 8.92545 20.125 10.2V10.875C20.125 11.4901 19.9718 11.7908 19.8188 11.9438C19.6658 12.0968 19.3651 12.25 18.75 12.25L15.6087 12.25Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1,7 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.61872 1.38128C2.27701 1.03957 1.72299 1.03957 1.38128 1.38128C1.03957 1.72299 1.03957 2.27701 1.38128 2.61872L21.3813 22.6187C21.723 22.9604 22.277 22.9604 22.6187 22.6187C22.9604 22.277 22.9604 21.723 22.6187 21.3813L2.61872 1.38128Z" fill="black"/>
<path d="M1.68922 6.67316C1.73053 6.16753 1.80012 5.70963 1.92522 5.28398L3.4352 6.79396L3.4334 6.81566C3.37568 7.52215 3.375 8.42544 3.375 9.7V14.3C3.375 15.5746 3.37568 16.4778 3.4334 17.1843C3.49029 17.8806 3.59819 18.3083 3.7701 18.6457C4.11764 19.3278 4.6722 19.8824 5.35428 20.2299C5.69168 20.4018 6.11939 20.5097 6.81566 20.5666C7.52215 20.6243 8.42544 20.625 9.7 20.625H14.3C15.5746 20.625 16.4778 20.6243 17.1843 20.5666L17.206 20.5648L18.716 22.0748C18.2904 22.1999 17.8325 22.2695 17.3268 22.3108C16.5407 22.375 15.566 22.375 14.3384 22.375H9.6616C8.43401 22.375 7.45926 22.375 6.67316 22.3108C5.86866 22.2451 5.18501 22.1077 4.5598 21.7892C3.54843 21.2738 2.72616 20.4516 2.21084 19.4402C1.89228 18.815 1.75495 18.1313 1.68922 17.3268C1.62499 16.5407 1.62499 15.566 1.625 14.3384V9.66162C1.62499 8.43403 1.62499 7.45926 1.68922 6.67316Z" fill="black"/>
<path d="M8.75 15.4641V12.1088L11.9753 15.334L10.25 16.3301C9.58333 16.715 8.75 16.2339 8.75 15.4641Z" fill="black"/>
<path d="M12.0922 8.7334L16.234 12.8752L16.25 12.866C16.9167 12.4811 16.9167 11.5188 16.25 11.1339L12.0922 8.7334Z" fill="black"/>
<path d="M1.68922 6.67316C1.73053 6.16753 1.80012 5.70963 1.92522 5.28398L3.4352 6.79396L3.4334 6.81566C3.37568 7.52215 3.375 8.42545 3.375 9.7V14.3C3.375 15.5746 3.37568 16.4778 3.4334 17.1843C3.49029 17.8806 3.59819 18.3083 3.7701 18.6457C4.11764 19.3278 4.6722 19.8824 5.35428 20.2299C5.69168 20.4018 6.11939 20.5097 6.81566 20.5666C7.52215 20.6243 8.42545 20.625 9.7 20.625H14.3C15.5746 20.625 16.4778 20.6243 17.1843 20.5666L17.206 20.5648L18.716 22.0748C18.2904 22.1999 17.8325 22.2695 17.3268 22.3108C16.5407 22.375 15.566 22.375 14.3384 22.375H9.6616C8.43401 22.375 7.45926 22.375 6.67316 22.3108C5.86866 22.2451 5.18501 22.1077 4.5598 21.7892C3.54843 21.2738 2.72616 20.4516 2.21084 19.4402C1.89228 18.815 1.75495 18.1313 1.68922 17.3268C1.62499 16.5407 1.62499 15.566 1.625 14.3384V9.66162C1.62499 8.43403 1.62499 7.45926 1.68922 6.67316Z" fill="black"/>
<path d="M9.875 13.2338L8.125 11.4838V14C8.125 14.4832 8.51675 14.875 9 14.875C9.48325 14.875 9.875 14.4832 9.875 14V13.2338Z" fill="black"/>
<path d="M6.875 10.2338L5.12515 8.4839L5.125 8.5L5.125 15.5C5.125 15.9832 5.51675 16.375 6 16.375C6.48325 16.375 6.875 15.9832 6.875 15.5L6.875 10.2338Z" fill="black"/>
<path d="M12.875 17V16.2338L11.125 14.4838V17C11.125 17.4832 11.5168 17.875 12 17.875C12.4832 17.875 12.875 17.4832 12.875 17Z" fill="black"/>
<path d="M11.125 7V7.76624L12.875 9.51624V7C12.875 6.51675 12.4832 6.125 12 6.125C11.5168 6.125 11.125 6.51675 11.125 7Z" fill="black"/>
<path d="M14.125 9V10.7662L15.875 12.5162V9C15.875 8.51675 15.4832 8.125 15 8.125C14.5168 8.125 14.125 8.51675 14.125 9Z" fill="black"/>
<path d="M17.1898 13.831L17.669 14.3102C17.7711 14.352 17.8829 14.375 18 14.375C18.4832 14.375 18.875 13.9832 18.875 13.5V10.5C18.875 10.0168 18.4832 9.625 18 9.625C17.5168 9.625 17.125 10.0168 17.125 10.5V13.5C17.125 13.6171 17.148 13.7289 17.1898 13.831Z" fill="black"/>
<path d="M20.5666 17.1843L20.5648 17.206L22.0748 18.716C22.1999 18.2904 22.2695 17.8325 22.3108 17.3268C22.375 16.5407 22.375 15.566 22.375 14.3384V9.6616C22.375 8.43401 22.375 7.45926 22.3108 6.67316C22.2451 5.86866 22.1077 5.18501 21.7892 4.5598C21.2738 3.54843 20.4516 2.72616 19.4402 2.21084C18.815 1.89228 18.1313 1.75495 17.3268 1.68922C16.5407 1.62499 15.566 1.62499 14.3384 1.625H9.66162C8.43403 1.62499 7.45926 1.62499 6.67316 1.68922C6.16753 1.73053 5.70963 1.80012 5.28398 1.92522L6.79396 3.4352L6.81566 3.4334C7.52215 3.37568 8.42544 3.375 9.7 3.375H14.3C15.5746 3.375 16.4778 3.37568 17.1843 3.4334C17.8806 3.49029 18.3083 3.59819 18.6457 3.7701C19.3278 4.11764 19.8824 4.6722 20.2299 5.35428C20.4018 5.69168 20.5097 6.11939 20.5666 6.81566C20.6243 7.52215 20.625 8.42544 20.625 9.7V14.3C20.625 15.5746 20.6243 16.4778 20.5666 17.1843Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

View file

@ -594,6 +594,7 @@ $message-padding-horizontal: 12px;
}
.module-message__attachment-too-big--content-above {
margin-block-start: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
@ -784,6 +785,11 @@ $message-padding-horizontal: 12px;
margin-block-start: -25px;
}
.module-message__generic-attachment--undownloadable-no-text
+ .module-message__metadata {
margin-block-start: -$message-padding-vertical - 2px;
}
.module-message__sticker-container {
// To ensure that images are centered if they aren't full width of bubble
text-align: center;
@ -825,6 +831,10 @@ $message-padding-horizontal: 12px;
}
}
.module-message__generic-attachment--undownloadable {
min-width: 260px;
}
.module-message__generic-attachment--with-content-below {
padding-bottom: 6px;
}
@ -896,18 +906,21 @@ $message-padding-horizontal: 12px;
color: variables.$color-gray-90;
}
$message-attachment-padding-horizontal: 8px;
.module-message__generic-attachment__text {
flex-grow: 1;
margin-inline-start: 8px;
margin-inline-start: $message-attachment-padding-horizontal + 2px;
// The width of the icon plus our 8px margin plus 1px leeway
max-width: calc(100% - 36px);
}
.module-message__generic-attachment__file-name {
@include mixins.font-body-2-bold;
@include mixins.font-body-1;
margin-top: 2px;
user-select: none;
font-weight: 500;
// Handling really long filenames - cut them off
overflow-x: hidden;
@ -933,6 +946,11 @@ $message-padding-horizontal: 12px;
}
}
.module-message__container--incoming
.module-message__generic-attachment__file-name--undownloadable {
color: variables.$color-black-alpha-50;
}
.module-message__generic-attachment__file-size {
@include mixins.font-body-2;
@ -959,7 +977,12 @@ $message-padding-horizontal: 12px;
}
.module-message__undownloadable-attachment__icon-container {
margin-inline-end: 8px;
margin-inline-end: $message-attachment-padding-horizontal;
}
.module-message__undownloadable-attachment__icon-container--file {
margin-block-start: 1px;
margin-inline-end: 3px;
}
.module-message__undownloadable-attachment__icon {
@ -968,35 +991,103 @@ $message-padding-horizontal: 12px;
&--audio {
@include mixins.color-svg(
'../images/icons/v3/play/play-slash.svg',
'../images/icons/v3/waveform/waveform-slash.svg',
currentColor
);
}
&--generic {
&--file {
@include mixins.color-svg(
'../images/icons/v3/file/file-slash.svg',
'../images/icons/v3/error/error-circle.svg',
currentColor
);
}
&--sticker {
@include mixins.color-svg(
'../images/icons/v3/sticker/sticker-slash.svg',
currentColor
);
}
&--small {
width: 16px;
height: 16px;
}
}
.module-message__undownloadable-attachment-info {
margin-inline-end: 15px;
.module-message__undownloadable-attachment-info--file {
@include mixins.font-body-2;
}
.module-message__undownloadable-attachment-learn-more-container {
font-weight: 500;
.module-message__container--incoming
.module-message__undownloadable-attachment-info--file {
color: variables.$color-black-alpha-90;
}
.module-message__undownloadable-attachment {
min-width: 200px;
}
.module-message__undownloadable-attachment-file {
@include mixins.font-body-2;
display: flex;
}
.module-message__undownloadable-attachment-text {
@include mixins.button-reset;
& {
@include mixins.font-body-1;
@include mixins.light-theme {
border-block-start-color: variables.$color-black-alpha-10;
}
@include mixins.dark-theme {
border-block-start-color: variables.$color-white-alpha-10;
}
width: calc(100% + 24px);
margin-block-start: 9px;
margin-inline: -12px;
padding-block-start: 9px;
padding-inline: 12px;
border-block-start: 0.5px solid;
}
}
.module-message__container--outgoing
.module-message__undownloadable-attachment-text {
border-block-start-color: variables.$color-white-alpha-30;
}
.module-message__undownloadable-attachment-text__icon-container {
margin-inline-end: 8px;
align-self: flex-start;
}
.module-message--outgoing {
.module-message__undownloadable-attachment + .module-message__metadata {
.module-message__metadata__date--with-sticker {
color: inherit;
}
.module-message__metadata__status-icon--with-sticker {
background-color: variables.$color-white-alpha-80;
}
}
}
.module-message__undownloadable-attachment-learn-more {
@include mixins.button-reset;
@include mixins.font-body-1-bold;
@include mixins.keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px variables.$color-ultramarine;
}
}
& {
@include mixins.font-body-1-bold;
font-weight: 500;
}
}
.module-message__link-preview {

View file

@ -0,0 +1,22 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
@use '../variables';
.AttachmentNotAvailableModal__width-container {
max-width: 440px;
}
.AttachmentNotAvailableModal .AttachmentNotAvailableModal__headerTitle {
padding-block-end: 5px;
}
.AttachmentNotAvailableModal__body {
padding-block: 16px 0;
padding-inline: 16px;
}
.AttachmentNotAvailableModal .module-Button {
padding-inline: 24px;
}

View file

@ -25,6 +25,7 @@
@use 'components/AnnouncementsOnlyGroupBanner.scss';
@use 'components/App.scss';
@use 'components/AttachmentDetailPill.scss';
@use 'components/AttachmentNotAvailableModal.scss';
@use 'components/AudioCapture.scss';
@use 'components/AutoSizeInput.scss';
@use 'components/Avatar.scss';

View file

@ -0,0 +1,70 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import type { PropsType } from './AttachmentNotAvailableModal';
import {
AttachmentNotAvailableModal,
AttachmentNotAvailableModalType,
} from './AttachmentNotAvailableModal';
import type { ComponentMeta } from '../storybook/types';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/AttachmentNotAvailableModal',
component: AttachmentNotAvailableModal,
args: {
modalType: AttachmentNotAvailableModalType.VisualMedia,
i18n,
onClose: action('onClose'),
},
} satisfies ComponentMeta<PropsType>;
export function File(args: PropsType): JSX.Element {
return (
<AttachmentNotAvailableModal
{...args}
modalType={AttachmentNotAvailableModalType.File}
/>
);
}
export function LongText(args: PropsType): JSX.Element {
return (
<AttachmentNotAvailableModal
{...args}
modalType={AttachmentNotAvailableModalType.LongText}
/>
);
}
export function Sticker(args: PropsType): JSX.Element {
return (
<AttachmentNotAvailableModal
{...args}
modalType={AttachmentNotAvailableModalType.Sticker}
/>
);
}
export function VisualMedia(args: PropsType): JSX.Element {
return (
<AttachmentNotAvailableModal
{...args}
modalType={AttachmentNotAvailableModalType.VisualMedia}
/>
);
}
export function VoiceMessage(args: PropsType): JSX.Element {
return (
<AttachmentNotAvailableModal
{...args}
modalType={AttachmentNotAvailableModalType.VoiceMessage}
/>
);
}

View file

@ -0,0 +1,92 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { LocalizerType } from '../types/Util';
import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button';
export type PropsType = {
i18n: LocalizerType;
modalType: AttachmentNotAvailableModalType;
onClose: () => void;
};
export enum AttachmentNotAvailableModalType {
File = 'File',
LongText = 'LongText',
Sticker = 'Sticker',
VisualMedia = 'VisualMedia',
VoiceMessage = 'VoiceMessage',
}
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}
function getTitle(
i18n: LocalizerType,
modalType: AttachmentNotAvailableModalType
): string {
switch (modalType) {
case AttachmentNotAvailableModalType.LongText:
return i18n('icu:AttachmentNotAvailableModal__title--long-text');
case AttachmentNotAvailableModalType.Sticker:
return i18n('icu:AttachmentNotAvailableModal__title--sticker');
case AttachmentNotAvailableModalType.VisualMedia:
return i18n('icu:AttachmentNotAvailableModal__title--media');
case AttachmentNotAvailableModalType.VoiceMessage:
return i18n('icu:AttachmentNotAvailableModal__title--voice-message');
case AttachmentNotAvailableModalType.File:
default:
return i18n('icu:AttachmentNotAvailableModal__title--file');
}
}
function getBody(
i18n: LocalizerType,
modalType: AttachmentNotAvailableModalType
): string {
switch (modalType) {
case AttachmentNotAvailableModalType.LongText:
return i18n('icu:AttachmentNotAvailableModal__body--long-text');
case AttachmentNotAvailableModalType.Sticker:
return i18n('icu:AttachmentNotAvailableModal__body--sticker');
case AttachmentNotAvailableModalType.VisualMedia:
return i18n('icu:AttachmentNotAvailableModal__body--media');
case AttachmentNotAvailableModalType.VoiceMessage:
return i18n('icu:AttachmentNotAvailableModal__body--voice-message');
case AttachmentNotAvailableModalType.File:
default:
return i18n('icu:AttachmentNotAvailableModal__body--file');
}
}
export function AttachmentNotAvailableModal(props: PropsType): JSX.Element {
const { i18n, modalType, onClose } = props;
const footer = (
<Button onClick={onClose} ref={focusRef} variant={ButtonVariant.Primary}>
{i18n('icu:Confirmation--confirm')}
</Button>
);
return (
<Modal
modalName="AttachmentNotAvailableModal"
moduleClassName="AttachmentNotAvailableModal"
i18n={i18n}
onClose={onClose}
title={getTitle(i18n, modalType)}
modalFooter={footer}
padded={false}
>
<div className="module-error-modal__description">
{getBody(i18n, modalType)}
</div>
</Modal>
);
}

View file

@ -155,6 +155,7 @@ export function DebugLogWindow({
onShowDebugLog={shouldNeverBeCalled}
onUndoArchive={shouldNeverBeCalled}
openFileInFolder={shouldNeverBeCalled}
showAttachmentNotAvailableModal={shouldNeverBeCalled}
toast={toast}
containerWidthBreakpoint={null}
isInFullScreenCall={false}
@ -212,6 +213,7 @@ export function DebugLogWindow({
onShowDebugLog={shouldNeverBeCalled}
onUndoArchive={shouldNeverBeCalled}
openFileInFolder={shouldNeverBeCalled}
showAttachmentNotAvailableModal={shouldNeverBeCalled}
toast={toast}
containerWidthBreakpoint={null}
isInFullScreenCall={false}

View file

@ -62,6 +62,7 @@ const MESSAGE_DEFAULT_PROPS = {
showConversation: noop,
showEditHistoryModal: noop,
showAttachmentDownloadStillInProgressToast: shouldNeverBeCalled,
showAttachmentNotAvailableModal: shouldNeverBeCalled,
showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled,

View file

@ -21,6 +21,7 @@ import { ConfirmationDialog } from './ConfirmationDialog';
import { SignalConnectionsModal } from './SignalConnectionsModal';
import { WhatsNewModal } from './WhatsNewModal';
import type { StartCallData } from './ConfirmLeaveCallModal';
import type { AttachmentNotAvailableModalType } from './AttachmentNotAvailableModal';
// NOTE: All types should be required for this component so that the smart
// component gives you type errors when adding/removing props.
@ -30,6 +31,9 @@ export type PropsType = {
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId: string | undefined;
renderAddUserToAnotherGroup: () => JSX.Element;
// AttachmentNotAvailableModal
attachmentNotAvailableModalType: AttachmentNotAvailableModalType | undefined;
renderAttachmentNotAvailableModal: () => JSX.Element;
// CallLinkAddNameModal
callLinkAddNameModalRoomId: string | null;
renderCallLinkAddNameModal: () => JSX.Element;
@ -116,6 +120,9 @@ export type PropsType = {
export function GlobalModalContainer({
i18n,
// AttachmentNotAvailableModal
attachmentNotAvailableModalType,
renderAttachmentNotAvailableModal,
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId,
renderAddUserToAnotherGroup,
@ -327,5 +334,9 @@ export function GlobalModalContainer({
);
}
if (attachmentNotAvailableModalType) {
return renderAttachmentNotAvailableModal();
}
return null;
}

View file

@ -287,6 +287,9 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
onShowDebugLog={action('onShowDebugLog')}
onUndoArchive={action('onUndoArchive')}
openFileInFolder={action('openFileInFolder')}
showAttachmentNotAvailableModal={action(
'showAttachmentNotAvailableModal'
)}
toast={undefined}
megaphone={undefined}
containerWidthBreakpoint={containerWidthBreakpoint}

View file

@ -1654,6 +1654,7 @@ export function Preferences({
onShowDebugLog={shouldNeverBeCalled}
onUndoArchive={shouldNeverBeCalled}
openFileInFolder={shouldNeverBeCalled}
showAttachmentNotAvailableModal={shouldNeverBeCalled}
toast={toast}
containerWidthBreakpoint={WidthBreakpoint.Narrow}
isInFullScreenCall={false}

View file

@ -69,6 +69,7 @@ const MESSAGE_DEFAULT_PROPS = {
scrollToQuotedMessage: shouldNeverBeCalled,
showConversation: noop,
showAttachmentDownloadStillInProgressToast: shouldNeverBeCalled,
showAttachmentNotAvailableModal: shouldNeverBeCalled,
showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightbox: shouldNeverBeCalled,

View file

@ -238,6 +238,7 @@ export default {
openFileInFolder: action('openFileInFolder'),
onShowDebugLog: action('onShowDebugLog'),
onUndoArchive: action('onUndoArchive'),
showAttachmentNotAvailableModal: action('showAttachmentNotAvailableModal'),
i18n,
toastType: ToastType.AddingUserToGroup,
megaphoneType: MegaphoneType.UsernameOnboarding,

View file

@ -16,8 +16,7 @@ import type { AnyToast } from '../types/Toast';
import { ToastType } from '../types/Toast';
import type { AnyActionableMegaphone } from '../types/Megaphone';
import { MegaphoneType } from '../types/Megaphone';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { LINKED_DEVICES_URL } from '../types/support';
import { AttachmentNotAvailableModalType } from './AttachmentNotAvailableModal';
export type PropsType = {
hideToast: () => unknown;
@ -29,6 +28,9 @@ export type PropsType = {
conversationId: string,
options?: { wasPinned?: boolean }
) => unknown;
showAttachmentNotAvailableModal: (
type: AttachmentNotAvailableModalType
) => void;
toast?: AnyToast;
megaphone?: AnyActionableMegaphone;
centerToast?: boolean;
@ -45,6 +47,7 @@ export function renderToast({
openFileInFolder,
onShowDebugLog,
onUndoArchive,
showAttachmentNotAvailableModal,
OS,
toast,
}: PropsType): JSX.Element | null {
@ -415,7 +418,10 @@ export function renderToast({
onClose={hideToast}
toastAction={{
label: i18n('icu:attachmentNoLongerAvailable__learnMore'),
onClick: () => openLinkInWebBrowser(LINKED_DEVICES_URL),
onClick: () =>
showAttachmentNotAvailableModal(
AttachmentNotAvailableModalType.VisualMedia
),
}}
>
{i18n('icu:mediaNotAvailable')}

View file

@ -107,7 +107,8 @@ import { getColorForCallLink } from '../../util/getColorForCallLink';
import { getKeyFromCallLink } from '../../util/callLinks';
import { InAnotherCallTooltip } from './InAnotherCallTooltip';
import { formatFileSize } from '../../util/formatFileSize';
import { LINKED_DEVICES_URL } from '../../types/support';
import { AttachmentNotAvailableModalType } from '../AttachmentNotAvailableModal';
import { assertDev } from '../../util/assert';
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 16;
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
@ -376,6 +377,9 @@ export type PropsActions = {
showEditHistoryModal?: (id: string) => unknown;
showAttachmentDownloadStillInProgressToast: (count: number) => unknown;
showAttachmentNotAvailableModal: (
modalType: AttachmentNotAvailableModalType
) => void;
showExpiredIncomingTapToViewToast: () => unknown;
showExpiredOutgoingTapToViewToast: () => unknown;
showMediaNoLongerAvailableToast: () => unknown;
@ -948,6 +952,7 @@ export class Message extends React.PureComponent<Props, State> {
renderingContext,
shouldCollapseAbove,
shouldCollapseBelow,
showAttachmentNotAvailableModal,
showLightbox,
showMediaNoLongerAvailableToast,
status,
@ -969,9 +974,19 @@ export class Message extends React.PureComponent<Props, State> {
// For attachments which aren't full-frame
const withContentBelow = Boolean(text || attachmentDroppedDueToSize);
const withContentAbove = Boolean(quote) || this.#shouldRenderAuthor();
const displayImage = canDisplayImage(attachments);
const displayImage =
canDisplayImage(attachments) && !attachmentDroppedDueToSize;
if (displayImage && !imageBroken) {
// attachmentDroppedDueToSize is handled in renderAttachmentTooBig
const isAttachmentNotAvailable =
isPermanentlyUndownloadable(firstAttachment) &&
!attachmentDroppedDueToSize;
if (
displayImage &&
!imageBroken &&
!(isSticker && isAttachmentNotAvailable)
) {
const prefix = isSticker ? 'sticker' : 'attachment';
const containerClassName = classNames(
`module-message__${prefix}-container`,
@ -1057,8 +1072,26 @@ export class Message extends React.PureComponent<Props, State> {
}
const isAttachmentAudio = isAudio(attachments);
// Undownloadable audio and generic files
if (isPermanentlyUndownloadable(firstAttachment)) {
if (isAttachmentNotAvailable && (isAttachmentAudio || isSticker)) {
let attachmentType: string;
let info: string;
let modalType: AttachmentNotAvailableModalType;
if (isAttachmentAudio) {
attachmentType = 'audio';
info = i18n('icu:attachmentNotAvailable__voice');
modalType = AttachmentNotAvailableModalType.VoiceMessage;
} else if (isSticker) {
attachmentType = 'sticker';
info = i18n('icu:attachmentNotAvailable__sticker');
modalType = AttachmentNotAvailableModalType.Sticker;
} else {
assertDev(
false,
'renderAttachment(): Invalid case for permanently undownloadable attachment'
);
return null;
}
const containerClassName = classNames(
'module-message__undownloadable-attachment',
withContentAbove
@ -1069,11 +1102,11 @@ export class Message extends React.PureComponent<Props, State> {
: null,
text ? null : 'module-message__undownloadable-attachment--no-text'
);
const attachmentType = isAttachmentAudio ? 'audio' : 'generic';
const iconClassName = classNames(
'module-message__undownloadable-attachment__icon',
`module-message__undownloadable-attachment__icon--${attachmentType}`
);
return (
<div className={containerClassName}>
<div className="module-message__undownloadable-attachment__icon-container">
@ -1081,9 +1114,7 @@ export class Message extends React.PureComponent<Props, State> {
</div>
<div>
<div className="module-message__undownloadable-attachment-info">
{isAttachmentAudio
? i18n('icu:voiceMessageNotAvailable')
: i18n('icu:fileNotAvailable')}
{info}
</div>
<div className="module-message__undownloadable-attachment-learn-more-container">
<button
@ -1091,7 +1122,7 @@ export class Message extends React.PureComponent<Props, State> {
onClick={e => {
e.stopPropagation();
e.preventDefault();
openLinkInWebBrowser(LINKED_DEVICES_URL);
showAttachmentNotAvailableModal(modalType);
}}
type="button"
>
@ -1138,6 +1169,7 @@ export class Message extends React.PureComponent<Props, State> {
},
});
}
const { pending, fileName, size, contentType } = firstAttachment;
const extension = getExtensionForDisplay({ contentType, fileName });
const isDangerous = isFileDangerous(fileName || '');
@ -1152,6 +1184,12 @@ export class Message extends React.PureComponent<Props, State> {
: null,
withContentAbove
? 'module-message__generic-attachment--with-content-above'
: null,
isAttachmentNotAvailable
? 'module-message__generic-attachment--undownloadable'
: null,
isAttachmentNotAvailable && !text
? 'module-message__generic-attachment--undownloadable-no-text'
: null
)}
// There's only ever one of these, so we don't want users to tab into it
@ -1161,9 +1199,15 @@ export class Message extends React.PureComponent<Props, State> {
event.preventDefault();
if (!isDownloaded(firstAttachment)) {
kickOffAttachmentDownload({
messageId: id,
});
if (isAttachmentNotAvailable) {
showAttachmentNotAvailableModal(
AttachmentNotAvailableModalType.File
);
} else {
kickOffAttachmentDownload({
messageId: id,
});
}
} else {
this.openGenericAttachment();
}
@ -1193,18 +1237,69 @@ export class Message extends React.PureComponent<Props, State> {
<div
className={classNames(
'module-message__generic-attachment__file-name',
`module-message__generic-attachment__file-name--${direction}`
`module-message__generic-attachment__file-name--${direction}`,
isAttachmentNotAvailable
? 'module-message__generic-attachment__file-name--undownloadable'
: null
)}
>
{fileName}
</div>
<div
className={classNames(
'module-message__generic-attachment__file-size',
`module-message__generic-attachment__file-size--${direction}`
)}
>
{formatFileSize(size)}
{isAttachmentNotAvailable ? (
<div className="module-message__undownloadable-attachment-file">
<div className="module-message__undownloadable-attachment__icon-container--file">
<div className="module-message__undownloadable-attachment__icon module-message__undownloadable-attachment__icon--file module-message__undownloadable-attachment__icon--small" />
</div>
<div className="module-message__undownloadable-attachment-info--file">
{i18n('icu:attachmentNotAvailable__file')}
</div>
</div>
) : (
<div
className={classNames(
'module-message__generic-attachment__file-size',
`module-message__generic-attachment__file-size--${direction}`
)}
>
{formatFileSize(size)}
</div>
)}
</div>
</button>
);
}
public renderUndownloadableTextAttachment(): JSX.Element | null {
const { i18n, textAttachment, showAttachmentNotAvailableModal } =
this.props;
if (!textAttachment || !isPermanentlyUndownloadable(textAttachment)) {
return null;
}
return (
<button
type="button"
className="module-message__generic-attachment module-message__undownloadable-attachment-text"
tabIndex={-1}
onClick={event => {
event.stopPropagation();
event.preventDefault();
showAttachmentNotAvailableModal(
AttachmentNotAvailableModalType.LongText
);
}}
>
<div className="module-message__undownloadable-attachment-text__icon-container">
<div className="module-message__undownloadable-attachment__icon module-message__undownloadable-attachment__icon--file" />
</div>
<div>
<div className="module-message__undownloadable-attachment-info">
{i18n('icu:attachmentNotAvailable__longMessage')}
</div>
<div className="module-message__undownloadable-attachment-learn-more-container">
<div className="module-message__undownloadable-attachment-learn-more">
{i18n('icu:attachmentNoLongerAvailable__learnMore')}
</div>
</div>
</div>
</button>
@ -2586,6 +2681,7 @@ export class Message extends React.PureComponent<Props, State> {
{this.renderPayment()}
{this.renderEmbeddedContact()}
{this.renderText()}
{this.renderUndownloadableTextAttachment()}
{this.#renderAction()}
{this.#renderMetadata()}
{this.renderSendMessageButton()}
@ -2603,12 +2699,14 @@ export class Message extends React.PureComponent<Props, State> {
direction,
giftBadge,
id,
isSticker,
isTapToView,
isTapToViewExpired,
kickOffAttachmentDownload,
startConversation,
openGiftBadge,
pushPanelForConversation,
showAttachmentNotAvailableModal,
showLightbox,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
@ -2631,7 +2729,21 @@ export class Message extends React.PureComponent<Props, State> {
event.preventDefault();
event.stopPropagation();
showMediaNoLongerAvailableToast();
// This needs to be the first check because canDisplayImage is true for stickers
if (isSticker) {
showAttachmentNotAvailableModal(
AttachmentNotAvailableModalType.Sticker
);
} else if (canDisplayImage(attachments)) {
showMediaNoLongerAvailableToast();
} else if (isAudio(attachments)) {
showAttachmentNotAvailableModal(
AttachmentNotAvailableModalType.VoiceMessage
);
} else {
showAttachmentNotAvailableModal(AttachmentNotAvailableModalType.File);
}
return;
}
@ -2817,7 +2929,12 @@ export class Message extends React.PureComponent<Props, State> {
const isAttachmentPending = this.isAttachmentPending();
const width = this.getWidth();
const isEmojiOnly = this.#canRenderStickerLikeEmoji();
const isStickerLike = isSticker || isEmojiOnly;
const isStickerLike =
isEmojiOnly ||
(isSticker &&
attachments &&
attachments[0] &&
!isPermanentlyUndownloadable(attachments[0]));
// If it's a mostly-normal gray incoming text box, we don't want to darken it as much
const lighterSelect =

View file

@ -101,6 +101,7 @@ export type PropsReduxActions = Pick<
| 'showConversation'
| 'showEditHistoryModal'
| 'showAttachmentDownloadStillInProgressToast'
| 'showAttachmentNotAvailableModal'
| 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast'
| 'showLightbox'
@ -149,6 +150,7 @@ export function MessageDetail({
showConversation,
showEditHistoryModal,
showAttachmentDownloadStillInProgressToast,
showAttachmentNotAvailableModal,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showLightbox,
@ -370,6 +372,7 @@ export function MessageDetail({
showAttachmentDownloadStillInProgressToast={
showAttachmentDownloadStillInProgressToast
}
showAttachmentNotAvailableModal={showAttachmentNotAvailableModal}
showExpiredIncomingTapToViewToast={
showExpiredIncomingTapToViewToast
}

View file

@ -138,6 +138,7 @@ const defaultMessageProps: TimelineMessagesProps = {
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showAttachmentNotAvailableModal: action('showAttachmentNotAvailableModal'),
showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),

View file

@ -310,6 +310,7 @@ const actions = () => ({
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showAttachmentNotAvailableModal: action('showAttachmentNotAvailableModal'),
showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),

View file

@ -107,6 +107,7 @@ const getDefaultProps = () => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
showAttachmentNotAvailableModal: action('showAttachmentNotAvailableModal'),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
showSpoiler: action('showSpoiler'),

View file

@ -345,6 +345,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showAttachmentNotAvailableModal: action('showAttachmentNotAvailableModal'),
showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
@ -845,10 +846,24 @@ CanDeleteForEveryone.args = {
direction: 'outgoing',
};
const bigAttachment = {
contentType: stringToMIMEType('text/plain'),
fileName: 'why-i-love-birds.txt',
size: 100000000000,
width: undefined,
height: undefined,
path: undefined,
key: undefined,
id: undefined,
error: true,
wasTooBig: true,
};
export function AttachmentTooBig(): JSX.Element {
const propsSent = createProps({
conversationType: 'direct',
attachmentDroppedDueToSize: true,
attachments: [bigAttachment],
});
return <>{renderBothDirections(propsSent)}</>;
@ -858,6 +873,37 @@ export function AttachmentTooBigWithText(): JSX.Element {
const propsSent = createProps({
conversationType: 'direct',
attachmentDroppedDueToSize: true,
attachments: [bigAttachment],
text: 'Check out this file!',
});
return <>{renderBothDirections(propsSent)}</>;
}
const bigImageAttachment = {
...bigAttachment,
contentType: IMAGE_JPEG,
fileName: 'bird.jpg',
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 1000,
height: 1000,
};
export function AttachmentTooBigImage(): JSX.Element {
const propsSent = createProps({
conversationType: 'direct',
attachmentDroppedDueToSize: true,
attachments: [bigImageAttachment],
});
return <>{renderBothDirections(propsSent)}</>;
}
export function AttachmentTooBigImageWithText(): JSX.Element {
const propsSent = createProps({
conversationType: 'direct',
attachmentDroppedDueToSize: true,
attachments: [bigImageAttachment],
text: 'Check out this file!',
});
@ -2340,6 +2386,29 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element {
...textFileProps,
text: "Here's that file",
};
const stickerProps = createProps({
attachments: [
fakeAttachment({
fileName: '512x515-thumbs-up-lincoln.webp',
contentType: IMAGE_WEBP,
width: 128,
height: 128,
error: true,
}),
],
isSticker: true,
status: 'sent',
});
const longMessageProps = createProps({
text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
textAttachment: {
contentType: LONG_MESSAGE,
size: 123,
pending: false,
key: undefined,
error: true,
},
});
const outgoingAuthor = {
...imageProps.author,
@ -2352,8 +2421,10 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element {
<TimelineMessage {...gifProps} />
<TimelineMessage {...videoProps} />
<TimelineMessage {...multipleImagesProps} />
<TimelineMessage {...stickerProps} />
<TimelineMessage {...textFileProps} />
<TimelineMessage {...textFileWithCaptionProps} />
<TimelineMessage {...longMessageProps} />
<TimelineMessage {...audioProps} />
<TimelineMessage {...audioWithCaptionProps} shouldCollapseBelow />
<TimelineMessage
@ -2382,11 +2453,21 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element {
author={outgoingAuthor}
direction="outgoing"
/>
<TimelineMessage
{...stickerProps}
author={outgoingAuthor}
direction="outgoing"
/>
<TimelineMessage
{...textFileWithCaptionProps}
author={outgoingAuthor}
direction="outgoing"
/>
<TimelineMessage
{...longMessageProps}
author={outgoingAuthor}
direction="outgoing"
/>
<TimelineMessage
{...audioProps}
author={outgoingAuthor}

View file

@ -50,6 +50,7 @@ import type { LocalizerType } from '../../types/I18N';
import { linkCallRoute } from '../../util/signalRoutes';
import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
import { getMessageById } from '../../messages/getMessageById';
import type { AttachmentNotAvailableModalType } from '../../components/AttachmentNotAvailableModal';
// State
@ -93,6 +94,7 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
export type GlobalModalsStateType = ReadonlyDeep<{
addUserToAnotherGroupModalContactId?: string;
aboutContactModalContactId?: string;
attachmentNotAvailableModalType: AttachmentNotAvailableModalType | undefined;
callLinkAddNameModalRoomId: string | null;
callLinkEditModalRoomId: string | null;
callLinkPendingParticipantContactId: string | undefined;
@ -127,6 +129,10 @@ export type GlobalModalsStateType = ReadonlyDeep<{
// Actions
const SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL =
'globalModals/SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL';
const HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL =
'globalModals/HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL';
const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL';
const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL';
const HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL';
@ -193,6 +199,15 @@ export type UserNotFoundModalStateType = ReadonlyDeep<
}
>;
type HideAttachmentNotAvailableModalActionType = ReadonlyDeep<{
type: typeof HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL;
}>;
type ShowAttachmentNotAvailableModalActionType = ReadonlyDeep<{
type: typeof SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL;
payload: AttachmentNotAvailableModalType;
}>;
type HideContactModalActionType = ReadonlyDeep<{
type: typeof HIDE_CONTACT_MODAL;
}>;
@ -378,6 +393,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| CloseGV2MigrationDialogActionType
| CloseShortcutGuideModalActionType
| CloseStickerPackPreviewActionType
| HideAttachmentNotAvailableModalActionType
| HideContactModalActionType
| HideSendAnywayDialogActiontype
| HideStoriesSettingsActionType
@ -386,6 +402,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| MessageChangedActionType
| MessageDeletedActionType
| MessageExpiredActionType
| ShowAttachmentNotAvailableModalActionType
| ShowContactModalActionType
| ShowEditHistoryModalActionType
| ShowErrorModalActionType
@ -423,11 +440,13 @@ export const actions = {
closeGV2MigrationDialog,
closeShortcutGuideModal,
closeStickerPackPreview,
hideAttachmentNotAvailableModal,
hideBlockingSafetyNumberChangeDialog,
hideContactModal,
hideStoriesSettings,
hideUserNotFoundModal,
hideWhatsNewModal,
showAttachmentNotAvailableModal,
showBlockingSafetyNumberChangeDialog,
showContactModal,
showEditHistoryModal,
@ -462,6 +481,21 @@ export const useGlobalModalActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function hideAttachmentNotAvailableModal(): HideAttachmentNotAvailableModalActionType {
return {
type: HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL,
};
}
function showAttachmentNotAvailableModal(
payload: AttachmentNotAvailableModalType
): ShowAttachmentNotAvailableModalActionType {
return {
type: SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL,
payload,
};
}
function hideContactModal(): HideContactModalActionType {
return {
type: HIDE_CONTACT_MODAL,
@ -994,6 +1028,7 @@ function copyOverMessageAttributesIntoForwardMessages(
export function getEmptyState(): GlobalModalsStateType {
return {
attachmentNotAvailableModalType: undefined,
hasConfirmationModal: false,
callLinkAddNameModalRoomId: null,
callLinkEditModalRoomId: null,
@ -1083,6 +1118,20 @@ export function reducer(
};
}
if (action.type === HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL) {
return {
...state,
attachmentNotAvailableModalType: undefined,
};
}
if (action.type === SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL) {
return {
...state,
attachmentNotAvailableModalType: action.payload,
};
}
if (action.type === SHOW_CONTACT_MODAL) {
const ourId = window.ConversationController.getOurConversationIdOrThrow();
if (action.payload.contactId === ourId) {

View file

@ -22,6 +22,11 @@ export const isShowingAnyModal = createSelector(
})
);
export const getAttachmentNotAvailableModalType = createSelector(
getGlobalModalsState,
({ attachmentNotAvailableModalType }) => attachmentNotAvailableModalType
);
export const getCallLinkEditModalRoomId = createSelector(
getGlobalModalsState,
({ callLinkEditModalRoomId }) => callLinkEditModalRoomId

View file

@ -0,0 +1,38 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { AttachmentNotAvailableModal } from '../../components/AttachmentNotAvailableModal';
import { strictAssert } from '../../util/assert';
import { getAttachmentNotAvailableModalType } from '../selectors/globalModals';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
export const SmartAttachmentNotAvailableModal = memo(
function SmartAttachmentNotAvailableModal() {
const i18n = useSelector(getIntl);
const attachmentNotAvailableModalType = useSelector(
getAttachmentNotAvailableModalType
);
strictAssert(
attachmentNotAvailableModalType != null,
'attachmentNotAvailableModalType is required'
);
const { hideAttachmentNotAvailableModal } = useGlobalModalActions();
const handleClose = useCallback(() => {
hideAttachmentNotAvailableModal();
}, [hideAttachmentNotAvailableModal]);
return (
<AttachmentNotAvailableModal
i18n={i18n}
modalType={attachmentNotAvailableModalType}
onClose={handleClose}
/>
);
}
);

View file

@ -30,6 +30,7 @@ import { SmartCallLinkEditModal } from './CallLinkEditModal';
import { SmartCallLinkAddNameModal } from './CallLinkAddNameModal';
import { SmartConfirmLeaveCallModal } from './ConfirmLeaveCallModal';
import { SmartCallLinkPendingParticipantModal } from './CallLinkPendingParticipantModal';
import { SmartAttachmentNotAvailableModal } from './AttachmentNotAvailableModal';
function renderCallLinkAddNameModal(): JSX.Element {
return <SmartCallLinkAddNameModal />;
@ -99,6 +100,10 @@ function renderAboutContactModal(): JSX.Element {
return <SmartAboutContactModal />;
}
function renderAttachmentNotAvailableModal(): JSX.Element {
return <SmartAttachmentNotAvailableModal />;
}
export const SmartGlobalModalContainer = memo(
function SmartGlobalModalContainer() {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
@ -110,6 +115,7 @@ export const SmartGlobalModalContainer = memo(
const {
aboutContactModalContactId,
addUserToAnotherGroupModalContactId,
attachmentNotAvailableModalType,
callLinkAddNameModalRoomId,
callLinkEditModalRoomId,
callLinkPendingParticipantContactId,
@ -189,6 +195,7 @@ export const SmartGlobalModalContainer = memo(
return (
<GlobalModalContainer
attachmentNotAvailableModalType={attachmentNotAvailableModalType}
addUserToAnotherGroupModalContactId={
addUserToAnotherGroupModalContactId
}
@ -220,6 +227,7 @@ export const SmartGlobalModalContainer = memo(
isWhatsNewVisible={isWhatsNewVisible}
renderAboutContactModal={renderAboutContactModal}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderAttachmentNotAvailableModal={renderAttachmentNotAvailableModal}
renderCallLinkAddNameModal={renderCallLinkAddNameModal}
renderCallLinkEditModal={renderCallLinkEditModal}
renderCallLinkPendingParticipantModal={

View file

@ -59,8 +59,12 @@ export const SmartMessageDetail = memo(
showSpoiler,
startConversation,
} = useConversationsActions();
const { showContactModal, showEditHistoryModal, toggleSafetyNumberModal } =
useGlobalModalActions();
const {
showAttachmentNotAvailableModal,
showContactModal,
showEditHistoryModal,
toggleSafetyNumberModal,
} = useGlobalModalActions();
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
const { viewStory } = useStoriesActions();
@ -112,6 +116,7 @@ export const SmartMessageDetail = memo(
showAttachmentDownloadStillInProgressToast={
showAttachmentDownloadStillInProgressToast
}
showAttachmentNotAvailableModal={showAttachmentNotAvailableModal}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}

View file

@ -144,6 +144,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
useComposerActions();
const {
showAttachmentNotAvailableModal,
showContactModal,
showEditHistoryModal,
toggleMessageRequestActionsConfirmation,
@ -233,6 +234,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
showAttachmentDownloadStillInProgressToast={
showAttachmentDownloadStillInProgressToast
}
showAttachmentNotAvailableModal={showAttachmentNotAvailableModal}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}

View file

@ -56,7 +56,8 @@ export const SmartToastManager = memo(function SmartToastManager({
const { onUndoArchive } = useConversationsActions();
const { openFileInFolder, hideToast } = useToastActions();
const { toggleUsernameOnboarding } = useGlobalModalActions();
const { showAttachmentNotAvailableModal, toggleUsernameOnboarding } =
useGlobalModalActions();
let megaphone: AnyActionableMegaphone | undefined;
@ -93,6 +94,7 @@ export const SmartToastManager = memo(function SmartToastManager({
onUndoArchive={onUndoArchive}
openFileInFolder={openFileInFolder}
hideToast={hideToast}
showAttachmentNotAvailableModal={showAttachmentNotAvailableModal}
centerToast={centerToast}
containerWidthBreakpoint={containerWidthBreakpoint}
isCompositionAreaVisible={isCompositionAreaVisible}

View file

@ -0,0 +1,10 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export enum AttachmentNotAvailableModalType {
File = 'File',
LongText = 'LongText',
Sticker = 'Sticker',
VisualMedia = 'VisualMedia',
VoiceMessage = 'VoiceMessage',
}

View file

@ -5,8 +5,6 @@ export const PRODUCTION_DOWNLOAD_URL = 'https://signal.org/download/';
export const BETA_DOWNLOAD_URL = 'https://support.signal.org/beta';
export const UNSUPPORTED_OS_URL =
'https://support.signal.org/hc/articles/5109141421850';
export const LINKED_DEVICES_URL =
'https://support.signal.org/hc/en-us/articles/360007320551-Linked-Devices';
export const LINK_SIGNAL_DESKTOP =
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
export const SAFETY_NUMBER_URL =