New option: Disable automatic attachment downloads

This commit is contained in:
Scott Nonnenberg
2025-03-04 10:09:43 +10:00
committed by GitHub
parent ee63cfc277
commit f163ada463
86 changed files with 3043 additions and 1030 deletions
+76 -4
View File
@@ -67,6 +67,10 @@
"messageformat": "Sent media quality", "messageformat": "Sent media quality",
"description": "Title for the sent media quality setting" "description": "Title for the sent media quality setting"
}, },
"icu:Preferences__sent-media-quality__description": {
"messageformat": "Sending high quality media will use more data.",
"description": "Additional detail about the sent media quality setting"
},
"icu:sentMediaQualityStandard": { "icu:sentMediaQualityStandard": {
"messageformat": "Standard", "messageformat": "Standard",
"description": "Label text for standard media quality option" "description": "Label text for standard media quality option"
@@ -1550,6 +1554,14 @@
"messageformat": "Voice message not available", "messageformat": "Voice message not available",
"description": "Shown in chat timeline for messages with old audio attachments which are no longer available for download." "description": "Shown in chat timeline for messages with old audio attachments which are no longer available for download."
}, },
"icu:attachmentNotAvailable__tapToView": {
"messageformat": "View once media is not available",
"description": "Shown in chat timeline for expired View Once messages - never viewed before expiration"
},
"icu:attachmentNotAvailable__tapToViewCannotDownload": {
"messageformat": "Can't download view once media",
"description": "Shown in chat timeline for invalid View Once messages, or where the attachment cannot be downloaded otherwise"
},
"icu:AttachmentNotAvailableModal__title--file": { "icu:AttachmentNotAvailableModal__title--file": {
"messageformat": "File not available", "messageformat": "File not available",
"description": "Title for info dialog for messages with old file attachments which are no longer available for download." "description": "Title for info dialog for messages with old file attachments which are no longer available for download."
@@ -1590,6 +1602,14 @@
"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.", "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." "description": "Body text for info dialog for messages with old voice messages which are no longer available for download."
}, },
"icu:TapToViewNotAvailableModal__body--expired": {
"messageformat": "This view once media is no longer available to download. Ask {name} to send it again.",
"description": "Body text for info dialog for messages with old view once media which are no longer available for download."
},
"icu:TapToViewNotAvailableModal__body--error": {
"messageformat": "Cant download view once media. {name} will need to send it again.",
"description": "Body text for info dialog for messages with old view once media which are no longer available for download."
},
"icu:save": { "icu:save": {
"messageformat": "Save", "messageformat": "Save",
"description": "Used on save buttons" "description": "Used on save buttons"
@@ -2491,6 +2511,10 @@
"messageformat": "Failed to fetch phone number. Check your connection and try again.", "messageformat": "Failed to fetch phone number. Check your connection and try again.",
"description": "Shown if request to Signal servers to find phone number fails" "description": "Shown if request to Signal servers to find phone number fails"
}, },
"icu:Toast--download-failed": {
"messageformat": "Download failed",
"description": "Shown if user's manual request to download attachment fails"
},
"icu:ToastManager__CannotEditMessage_24": { "icu:ToastManager__CannotEditMessage_24": {
"messageformat": "Edits can only be applied within 24 hours from the time you sent this message.", "messageformat": "Edits can only be applied within 24 hours from the time you sent this message.",
"description": "Error message when you try to send an edit after message becomes too old" "description": "Error message when you try to send an edit after message becomes too old"
@@ -3237,11 +3261,19 @@
}, },
"icu:Message--tap-to-view-expired": { "icu:Message--tap-to-view-expired": {
"messageformat": "Viewed", "messageformat": "Viewed",
"description": "Text shown on messages with with individual timers, after user has viewed it" "description": "Text shown on messages with individual timers, after user has viewed it"
},
"icu:Message--tap-to-view--viewed": {
"messageformat": "Viewed",
"description": "Text shown on messages with individual timers, after user has viewed it"
}, },
"icu:Message--tap-to-view--outgoing": { "icu:Message--tap-to-view--outgoing": {
"messageformat": "Media", "messageformat": "Media",
"description": "Text shown on outgoing messages with with individual timers (inaccessible)" "description": "Text shown on outgoing messages with individual timers (inaccessible)"
},
"icu:Message--tap-to-view--media": {
"messageformat": "View Once Media",
"description": "Text shown on outgoing messages with individual timers (inaccessible)"
}, },
"icu:Message--tap-to-view--incoming--expired-toast": { "icu:Message--tap-to-view--incoming--expired-toast": {
"messageformat": "You already viewed this message.", "messageformat": "You already viewed this message.",
@@ -3253,11 +3285,23 @@
}, },
"icu:Message--tap-to-view--incoming": { "icu:Message--tap-to-view--incoming": {
"messageformat": "View Photo", "messageformat": "View Photo",
"description": "Text shown on photo messages with with individual timers, before user has viewed it" "description": "Text shown on photo messages with individual timers, before user has viewed it"
}, },
"icu:Message--tap-to-view--incoming-video": { "icu:Message--tap-to-view--incoming-video": {
"messageformat": "View Video", "messageformat": "View Video",
"description": "Text shown on video messages with with individual timers, before user has viewed it" "description": "Text shown on video messages with individual timers, before user has viewed it"
},
"icu:Message--tap-to-view--video": {
"messageformat": "View Once Video",
"description": "Text shown on video messages with individual timers, before user has viewed it"
},
"icu:Message--tap-to-view--photo": {
"messageformat": "View Once Photo",
"description": "Text shown on photo messages with individual timers, before user has viewed it"
},
"icu:Message--tap-to-view--helper-text": {
"messageformat": "Click to view",
"description": "Text shown below 'View Once Photo' or 'View Once Video' after the file has been downloaded"
}, },
"icu:Conversation--getDraftPreview--attachment": { "icu:Conversation--getDraftPreview--attachment": {
"messageformat": "(attachment)", "messageformat": "(attachment)",
@@ -6395,6 +6439,34 @@
"messageformat": "Privacy", "messageformat": "Privacy",
"description": "Button to switch the settings view" "description": "Button to switch the settings view"
}, },
"icu:Preferences__button--data-usage": {
"messageformat": "Data usage",
"description": "Button to switch the settings view"
},
"icu:Preferences__media-auto-download": {
"messageformat": "Media auto-download",
"description": "Button to switch the settings view"
},
"icu:Preferences__media-auto-download__photos": {
"messageformat": "Photos",
"description": "A category of files for media auto-download"
},
"icu:Preferences__media-auto-download__videos": {
"messageformat": "Videos",
"description": "A category of files for media auto-download"
},
"icu:Preferences__media-auto-download__audio": {
"messageformat": "Audio",
"description": "A category of files for media auto-download"
},
"icu:Preferences__media-auto-download__documents": {
"messageformat": "Documents",
"description": "A category of files for media auto-download"
},
"icu:Preferences__media-auto-download__description": {
"messageformat": "Voice messages and stickers are always auto-downloaded.",
"description": "Additional clarification for how media auto-download will behave"
},
"icu:Preferences--lastSynced": { "icu:Preferences--lastSynced": {
"messageformat": "Last import at {date} {time}", "messageformat": "Last import at {date} {time}",
"description": "Label for date and time of last sync operation" "description": "Label for date and time of last sync operation"
-1
View File
@@ -1 +0,0 @@
<svg height="56" viewBox="0 0 44 56" width="44" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="m27.4611409 10.6408637c.3342521.3574651.5388591.8376808.5388591 1.365685v25c0 1.6568543-1.3431458 3-3 3h-22c-1.65685425 0-3-1.3431457-3-3v-33.99999999c0-1.65685425 1.34314575-3 3-3l13.0265395-.00654871c.5599951.01585769 1.0585899.24950957 1.4153362.6205458.3409039.35455915 9.6542718 9.6299765 10.0192652 10.0203179z"/><filter id="b" height="170%" width="200%" x="-50%" y="-30%"><feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="4"/><feColorMatrix in="shadowBlurOuter1" result="shadowMatrixOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/><feMorphology in="SourceAlpha" operator="dilate" radius="0.5" result="shadowSpreadOuter2"/><feOffset dx="0" dy="0" in="shadowSpreadOuter2" result="shadowOffsetOuter2"/><feColorMatrix in="shadowOffsetOuter2" result="shadowMatrixOuter2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="shadowMatrixOuter2"/></feMerge></filter><path id="c" d="m16 .00654871h12v11.99999999c0-1.1045695-.8954305-2-2-2h-5c-1.6568542 0-3-1.34314574-3-2.99999999v-5c0-1.1045695-.8954305-2-2-2z"/><filter id="d" height="216.7%" width="216.7%" x="-58.3%" y="-41.7%"><feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="2"/><feColorMatrix in="shadowBlurOuter1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/></filter><filter id="e" height="175%" width="175%" x="-37.5%" y="-20.8%"><feMorphology in="SourceAlpha" operator="erode" radius="1" result="shadowSpreadInner1"/><feOffset dx="0" dy="0" in="shadowSpreadInner1" result="shadowOffsetInner1"/><feComposite in="shadowOffsetInner1" in2="SourceAlpha" k2="-1" k3="1" operator="arithmetic" result="shadowInnerInner1"/><feColorMatrix in="shadowInnerInner1" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/></filter><mask id="f" fill="#fff"><use fill="#fff" fill-rule="evenodd" xlink:href="#a"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(8 6)"><use fill="#000" filter="url(#b)" xlink:href="#a"/><use fill="#fff" fill-rule="evenodd" xlink:href="#a"/><g mask="url(#f)"><use fill="#000" filter="url(#d)" xlink:href="#c"/><use fill="#fff" fill-rule="evenodd" xlink:href="#c"/><use fill="#000" filter="url(#e)" xlink:href="#c"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="30" height="40" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.5 39.5h21a4 4 0 0 0 4-4v-23c0-.552-.224-1.052-.586-1.414l-10-10A1.994 1.994 0 0 0 17.5.5h-13a4 4 0 0 0-4 4v31a4 4 0 0 0 4 4Z" fill="color(display-p3 .9765 .9765 .9804)"/><path d="M28.914 11.086A1.994 1.994 0 0 0 27.5 10.5h-4a4 4 0 0 1-4-4v-4c0-.552-.224-1.052-.586-1.414l10 10Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 40h21a4.5 4.5 0 0 0 4.5-4.5v-23c0-.69-.28-1.316-.732-1.768l-10-10A2.494 2.494 0 0 0 17.5 0h-13A4.5 4.5 0 0 0 0 4.5v31A4.5 4.5 0 0 0 4.5 40Zm0-.5h21a4 4 0 0 0 4-4v-23c0-.552-.224-1.052-.586-1.414A1.994 1.994 0 0 0 27.5 10.5h-4a4 4 0 0 1-4-4v-4c0-.552-.224-1.052-.586-1.414A1.994 1.994 0 0 0 17.5.5h-13a4 4 0 0 0-4 4v31a4 4 0 0 0 4 4ZM19.975 2.147c.016.115.025.233.025.353v4a3.5 3.5 0 0 0 3.5 3.5h4c.12 0 .238.008.353.025l-7.878-7.878Z" fill="#000" fill-opacity=".2"/></svg>

After

Width:  |  Height:  |  Size: 905 B

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 16.875a.833.833 0 0 0 .59-.244l5.416-5.417a.833.833 0 1 0-1.179-1.178l-4.095 4.096.101-1.424V3.542a.833.833 0 1 0-1.666 0v9.166l.101 1.424-4.095-4.096a.833.833 0 0 0-1.179 1.178l5.417 5.417a.833.833 0 0 0 .589.244Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 323 B

+1
View File
@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#data__a)"><path d="M10 .938a9.063 9.063 0 1 0 0 18.125A9.063 9.063 0 0 0 10 .938ZM9.27 2.43v6.982c0 .547.188 1.078.532 1.504l4.383 5.434A7.604 7.604 0 1 1 9.27 2.43Zm6.05 13.004L11.34 10.5l1.993.125h4.246a7.58 7.58 0 0 1-2.26 4.81Zm-4.59-6.267V2.43a7.607 7.607 0 0 1 6.829 6.737h-6.83Z" fill="#000"/></g><defs><clipPath id="data__a"><path fill="#fff" d="M0 0h20v20H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 488 B

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18.317 9.48h.484a.73.73 0 0 1 .627 1.1l-1.3 2.195a.73.73 0 0 1-1.255 0l-1.301-2.195a.73.73 0 0 1 .627-1.1h.447a6.667 6.667 0 1 0-2.035 5.335.833.833 0 1 1 1.153 1.203 8.333 8.333 0 1 1 2.553-6.54Z" fill="#000"/><path d="M9.756 6.185a.734.734 0 0 1 1.285.486v6.558a.833.833 0 1 1-1.666 0v-5H8.55a.739.739 0 0 1-.739-.738c0-.387.294-.694.657-.737.605-.073.971-.22 1.287-.57Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 478 B

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.959 2.527a.833.833 0 0 1 .809-.857c.155-.004.31-.004.463 0a.833.833 0 1 1-.045 1.666 6.605 6.605 0 0 0-.37 0 .833.833 0 0 1-.857-.81Zm0 14.951a.833.833 0 0 1 .856-.81c.123.003.246.003.37 0a.833.833 0 0 1 .047 1.666c-.155.004-.31.004-.463 0a.833.833 0 0 1-.81-.856Zm8.516-8.518c.46-.013.844.35.857.81.005.155.005.31 0 .463a.833.833 0 1 1-1.666-.046 6.615 6.615 0 0 0 0-.37.833.833 0 0 1 .81-.856Zm-14.951 0c.46.013.823.396.81.856a6.616 6.616 0 0 0 0 .37.833.833 0 1 1-1.666.048 8.275 8.275 0 0 1 0-.463.833.833 0 0 1 .856-.81ZM17.1 5.635a.833.833 0 0 0-1.42.874c.066.106.127.213.186.32a.833.833 0 1 0 1.466-.793 8.305 8.305 0 0 0-.232-.401ZM4.134 13.174a.833.833 0 1 0-1.465.794 7.7 7.7 0 0 0 .232.401.833.833 0 0 0 1.419-.874 6.646 6.646 0 0 1-.186-.32Zm8.702-10.166a.833.833 0 0 1 1.13-.338c.136.074.27.151.4.232a.833.833 0 0 1-.872 1.42 6.58 6.58 0 0 0-.32-.185.833.833 0 0 1-.338-1.13ZM5.36 15.956a.833.833 0 0 1 1.147-.274c.104.064.211.126.32.185a.833.833 0 0 1-.792 1.466 8.234 8.234 0 0 1-.4-.23.833.833 0 0 1-.275-1.147Zm.273-13.054a.833.833 0 0 0 .875 1.42c.105-.066.212-.127.32-.186a.833.833 0 0 0-.794-1.465c-.135.073-.269.15-.4.231Zm7.54 12.966a.833.833 0 0 0 .793 1.465c.136-.073.27-.15.401-.232a.833.833 0 1 0-.874-1.418 6.706 6.706 0 0 1-.32.185ZM3.006 7.166a.833.833 0 0 1-.337-1.129c.073-.136.15-.27.23-.401a.833.833 0 1 1 1.421.872 6.617 6.617 0 0 0-.185.32.833.833 0 0 1-1.129.338Zm12.948 7.476a.833.833 0 0 1-.274-1.146 6.65 6.65 0 0 0 .185-.32.833.833 0 0 1 1.467.79 7.926 7.926 0 0 1-.232.402.833.833 0 0 1-1.146.274Zm-5.646-8.705c-.222 0-.418.1-.552.248-.315.35-.681.496-1.287.569a.744.744 0 0 0-.656.737c0 .407.33.738.738.738h.824v5a.833.833 0 1 0 1.667 0V6.671a.734.734 0 0 0-.734-.734Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1 +0,0 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.063 2.524a.73.73 0 0 1 .708-.75 8.17 8.17 0 0 1 .457 0 .73.73 0 1 1-.04 1.458 6.709 6.709 0 0 0-.375 0 .73.73 0 0 1-.75-.708Zm0 14.957a.73.73 0 0 1 .749-.709c.124.004.25.003.376 0a.73.73 0 1 1 .041 1.458 8.209 8.209 0 0 1-.457 0 .73.73 0 0 1-.71-.749Zm8.415-8.416a.73.73 0 0 1 .75.708c.004.153.004.305 0 .457a.73.73 0 0 1-1.458-.04 6.73 6.73 0 0 0 0-.375.73.73 0 0 1 .708-.75Zm-14.957 0a.73.73 0 0 1 .709.749c-.004.124-.003.25 0 .376a.73.73 0 0 1-1.458.041 8.172 8.172 0 0 1 0-.457.73.73 0 0 1 .749-.71ZM17.01 5.69a.73.73 0 1 0-1.24.765 7 7 0 0 1 .187.325.73.73 0 1 0 1.283-.694 8.112 8.112 0 0 0-.23-.396ZM4.043 13.224a.73.73 0 1 0-1.283.694 8.2 8.2 0 0 0 .23.396.73.73 0 1 0 1.24-.765 6.733 6.733 0 0 1-.187-.325Zm8.885-10.167a.73.73 0 0 1 .987-.295c.135.073.267.149.397.228a.73.73 0 1 1-.764 1.243 6.707 6.707 0 0 0-.325-.188.73.73 0 0 1-.296-.988ZM5.449 16.01a.73.73 0 0 1 1.003-.24c.106.066.215.129.325.189a.73.73 0 0 1-.692 1.283 8.219 8.219 0 0 1-.397-.229.73.73 0 0 1-.24-1.003Zm.239-13.019a.73.73 0 1 0 .765 1.242c.107-.066.215-.129.325-.188a.73.73 0 0 0-.694-1.283 8.164 8.164 0 0 0-.396.23Zm7.534 12.969a.73.73 0 1 0 .694 1.282c.134-.073.266-.149.396-.23a.73.73 0 1 0-.765-1.24 6.75 6.75 0 0 1-.325.187ZM3.055 7.075a.73.73 0 0 1-.295-.988 8.18 8.18 0 0 1 .229-.397.73.73 0 0 1 1.242.764 6.712 6.712 0 0 0-.188.325.73.73 0 0 1-.988.296Zm12.954 7.478a.73.73 0 0 1-.24-1.003 6.68 6.68 0 0 0 .188-.325.73.73 0 0 1 1.283.692 8.136 8.136 0 0 1-.228.397.73.73 0 0 1-1.003.24ZM9.831 6.258a.63.63 0 0 1 1.106.413v6.558a.73.73 0 1 1-1.458 0V8.125h-.824a.634.634 0 0 1-.634-.634.64.64 0 0 1 .564-.634c.304-.037.528-.094.714-.182a1.63 1.63 0 0 0 .532-.417Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.713 5.954a8.333 8.333 0 0 0 11.333 11.333l-1.239-1.238a6.667 6.667 0 0 1-8.856-8.856l-1.238-1.24Z" fill="#000"/><path d="M9.375 12.616v.613a.833.833 0 0 0 1.302.69l-1.302-1.303ZM11.041 7.8 9.588 6.349c.058-.05.114-.103.168-.163a.734.734 0 0 1 1.285.486v1.13Z" fill="#000"/><path d="M7.192 3.952 5.954 2.713a8.334 8.334 0 0 1 12.363 6.766h.484a.73.73 0 0 1 .627 1.101l-1.3 2.195a.73.73 0 0 1-1.255 0l-1.301-2.195a.73.73 0 0 1 .627-1.1h.447a6.667 6.667 0 0 0-9.454-5.528ZM2.327 2.327a.833.833 0 0 1 1.179 0L17.648 16.47a.833.833 0 0 1-1.179 1.178L2.327 3.506a.833.833 0 0 1 0-1.179Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 688 B

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.959 2.527a.833.833 0 0 1 .809-.857c.155-.004.31-.004.463 0a.833.833 0 1 1-.045 1.666 6.605 6.605 0 0 0-.37 0 .833.833 0 0 1-.857-.81Zm0 14.951a.833.833 0 0 1 .856-.81c.123.003.246.003.37 0a.833.833 0 0 1 .047 1.666c-.155.004-.31.004-.463 0a.833.833 0 0 1-.81-.856Zm8.516-8.518c.46-.013.844.35.857.81.005.155.005.31 0 .463a.833.833 0 1 1-1.666-.046 6.615 6.615 0 0 0 0-.37.833.833 0 0 1 .81-.856Zm-14.951 0c.46.013.823.396.81.856a6.616 6.616 0 0 0 0 .37.833.833 0 1 1-1.666.048 8.275 8.275 0 0 1 0-.463.833.833 0 0 1 .856-.81ZM17.1 5.635a.833.833 0 0 0-1.42.874c.066.106.127.213.186.32a.833.833 0 1 0 1.466-.793 8.305 8.305 0 0 0-.232-.401ZM4.134 13.174a.833.833 0 1 0-1.465.794 7.7 7.7 0 0 0 .232.401.833.833 0 0 0 1.419-.874 6.646 6.646 0 0 1-.186-.32Zm8.702-10.166a.833.833 0 0 1 1.13-.338c.136.074.27.151.4.232a.833.833 0 0 1-.872 1.42 6.58 6.58 0 0 0-.32-.185.833.833 0 0 1-.338-1.13ZM5.36 15.956a.833.833 0 0 1 1.147-.274c.104.064.211.126.32.185a.833.833 0 0 1-.792 1.466 8.234 8.234 0 0 1-.4-.23.833.833 0 0 1-.275-1.147Zm.273-13.054a.833.833 0 0 0 .875 1.42c.105-.066.212-.127.32-.186a.833.833 0 0 0-.794-1.465c-.135.073-.269.15-.4.231Zm7.54 12.966a.833.833 0 0 0 .793 1.465c.136-.073.27-.15.401-.232a.833.833 0 1 0-.874-1.418 6.706 6.706 0 0 1-.32.185ZM3.006 7.166a.833.833 0 0 1-.337-1.129c.073-.136.15-.27.23-.401a.833.833 0 1 1 1.421.872 6.617 6.617 0 0 0-.185.32.833.833 0 0 1-1.129.338Zm12.948 7.476a.833.833 0 0 1-.274-1.146 6.65 6.65 0 0 0 .185-.32.833.833 0 0 1 1.467.79 7.926 7.926 0 0 1-.232.402.833.833 0 0 1-1.146.274Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1 -3
View File
@@ -1,3 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.59 5.59a.833.833 0 0 0-1.18-1.18L10 8.822l-4.41-4.41A.833.833 0 0 0 4.41 5.59L8.822 10l-4.41 4.41a.833.833 0 1 0 1.178 1.18L10 11.177l4.412 4.411a.833.833 0 1 0 1.178-1.178L11.18 10l4.41-4.41Z" fill="#000"/></svg>
<path d="M18.7071 6.70711C19.0976 6.31658 19.0976 5.68342 18.7071 5.29289C18.3166 4.90237 17.6834 4.90237 17.2929 5.29289L12 10.5858L6.70711 5.29289C6.31658 4.90237 5.68342 4.90237 5.29289 5.29289C4.90237 5.68342 4.90237 6.31658 5.29289 6.70711L10.5858 12L5.29289 17.2929C4.90237 17.6834 4.90237 18.3166 5.29289 18.7071C5.68342 19.0976 6.31658 19.0976 6.70711 18.7071L12 13.4142L17.2929 18.7071C17.6834 19.0976 18.3166 19.0976 18.7071 18.7071C19.0976 18.3166 19.0976 17.6834 18.7071 17.2929L13.4142 12L18.7071 6.70711Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 301 B

+6
View File
@@ -109,6 +109,12 @@
font-style: italic; font-style: italic;
} }
@mixin font-body-small {
font-size: 12px;
line-height: 16px;
letter-spacing: 0px;
}
@mixin font-subtitle { @mixin font-subtitle {
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
+201 -266
View File
@@ -509,45 +509,6 @@ $message-padding-horizontal: 12px;
} }
} }
.module-message__container--with-tap-to-view {
min-width: 148px;
cursor: pointer;
user-select: none;
}
.module-message__container--with-tap-to-view-pending {
background-color: variables.$color-gray-15;
}
.module-message__container--with-tap-to-view-pending {
cursor: default;
}
.module-message__container--with-tap-to-view-expired {
@include mixins.light-theme {
border: 1px solid variables.$color-gray-15;
background-color: variables.$color-white;
}
@include mixins.dark-theme {
border: 1px solid variables.$color-gray-60;
background-color: variables.$color-gray-95;
}
}
.module-message__container--with-tap-to-view-error {
width: auto;
cursor: default;
@include mixins.light-theme {
background-color: variables.$color-white;
border: 1px solid variables.$color-deep-red;
}
@include mixins.dark-theme {
background-color: variables.$color-black;
border: 1px solid variables.$color-deep-red;
}
}
.module-message__container--deleted-for-everyone { .module-message__container--deleted-for-everyone {
@include mixins.light-theme { @include mixins.light-theme {
color: variables.$color-gray-90; color: variables.$color-gray-90;
@@ -619,104 +580,6 @@ $message-padding-horizontal: 12px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.module-message__tap-to-view {
margin-top: 2px;
display: flex;
flex-direction: row;
align-items: center;
}
.module-message__tap-to-view--with-content-above {
margin-top: 8px;
}
.module-message__tap-to-view--with-content-below {
margin-bottom: 8px;
}
.module-message__tap-to-view__spinner-container {
margin-inline-end: 6px;
flex-grow: 0;
flex-shrink: 0;
width: 20px;
height: 20px;
}
.module-message__tap-to-view__icon {
margin-inline-end: 6px;
flex-grow: 0;
flex-shrink: 0;
width: 20px;
height: 20px;
@include mixins.light-theme {
@include mixins.color-svg(
'../images/icons/v3/view_once/view_once.svg',
variables.$color-gray-90
);
}
@include mixins.dark-theme {
@include mixins.color-svg(
'../images/icons/v3/view_once/view_once.svg',
variables.$color-gray-05
);
}
}
.module-message__tap-to-view__icon--expired {
@include mixins.light-theme {
@include mixins.color-svg(
'../images/icons/v3/view_once/view_once-dash.svg',
variables.$color-gray-75
);
}
@include mixins.dark-theme {
@include mixins.color-svg(
'../images/icons/v3/view_once/view_once-dash.svg',
variables.$color-gray-05
);
}
}
.module-message__tap-to-view__icon--outgoing {
background-color: variables.$color-gray-05;
}
.module-message__tap-to-view__text {
@include mixins.font-body-1-bold;
color: variables.$color-gray-05;
}
.module-message__tap-to-view__text--incoming {
@include mixins.light-theme {
color: variables.$color-gray-90;
}
@include mixins.dark-theme {
color: variables.$color-gray-05;
}
}
.module-message__tap-to-view__text--incoming-expired {
@include mixins.light-theme {
color: variables.$color-gray-90;
}
@include mixins.dark-theme {
color: variables.$color-gray-05;
}
}
.module-message__tap-to-view__text--incoming-error {
@include mixins.light-theme {
color: variables.$color-gray-60;
}
@include mixins.dark-theme {
color: variables.$color-gray-25;
}
}
.module-message__tap-to-view__text--outgoing {
color: variables.$color-white;
}
.module-message__tap-to-view__text--outgoing-expired {
color: variables.$color-gray-05;
}
.module-message__attachment-container { .module-message__attachment-container {
// To ensure that images are centered if they aren't full width of bubble // To ensure that images are centered if they aren't full width of bubble
text-align: center; text-align: center;
@@ -780,16 +643,15 @@ $message-padding-horizontal: 12px;
border-color: variables.$color-white-alpha-30; border-color: variables.$color-white-alpha-30;
} }
.module-message__container--is-clickable {
cursor: pointer;
}
.module-message__undownloadable-attachment--no-text .module-message__undownloadable-attachment--no-text
+ .module-message__metadata { + .module-message__metadata {
margin-block-start: -25px; 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 { .module-message__sticker-container {
// To ensure that images are centered if they aren't full width of bubble // To ensure that images are centered if they aren't full width of bubble
text-align: center; text-align: center;
@@ -814,14 +676,16 @@ $message-padding-horizontal: 12px;
cursor: pointer; cursor: pointer;
} }
.module-message__generic-attachment { .module-message__simple-attachment {
@include mixins.button-reset; @include mixins.button-reset;
& { & {
user-select: none;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
border-radius: 4px;
} }
@include mixins.keyboard-mode { @include mixins.keyboard-mode {
@@ -831,32 +695,28 @@ $message-padding-horizontal: 12px;
} }
} }
.module-message__generic-attachment--undownloadable { .module-message__container--outgoing .module-message__simple-attachment {
min-width: 260px; @include mixins.keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px variables.$color-white;
}
}
} }
.module-message__generic-attachment--with-content-below { .module-message__simple-attachment--with-content-below {
padding-bottom: 6px; padding-bottom: 8px;
} }
.module-message__generic-attachment--with-content-above { .module-message__simple-attachment--with-content-above {
padding-top: 4px; padding-top: 4px;
} }
.module-message__generic-attachment__icon-container { .module-message__simple-attachment__icon {
position: relative; background: url('../images/generic-file.svg') no-repeat center;
user-select: none; height: 40px;
} width: 30px;
.module-message__generic-attachment__spinner-container { margin-inline-start: 3px;
padding-inline: 4px; margin-inline-end: 3px;
}
.module-message__generic-attachment__icon {
background: url('../images/file-gradient.svg') no-repeat center;
height: 44px;
width: 56px;
margin-inline: -13px -14px;
margin-bottom: -4px;
// So we can center the extension text inside this icon // So we can center the extension text inside this icon
display: flex; display: flex;
@@ -864,7 +724,7 @@ $message-padding-horizontal: 12px;
align-items: center; align-items: center;
} }
.module-message__generic-attachment__icon-dangerous-container { .module-message__simple-attachment__icon-dangerous-container {
position: absolute; position: absolute;
top: -1px; top: -1px;
@@ -877,7 +737,7 @@ $message-padding-horizontal: 12px;
background-color: variables.$color-white; background-color: variables.$color-white;
} }
.module-message__generic-attachment__icon-dangerous { .module-message__simple-attachment__icon-dangerous {
height: 16px; height: 16px;
width: 16px; width: 16px;
@@ -887,36 +747,38 @@ $message-padding-horizontal: 12px;
); );
} }
.module-message__generic-attachment__icon__extension { .module-message__simple-attachment__icon__extension {
font-size: 10px; @include mixins.font-subtitle;
line-height: 13px; text-transform: lowercase;
letter-spacing: 0.1px; user-select: none;
text-transform: uppercase;
// Along with flow layout in parent item, centers text // Along with flow layout in parent item, centers text
text-align: center; text-align: center;
width: 25px;
margin-inline: auto; margin-inline: auto;
// We don't have much room for text here, cut it off without ellipse // We shouldn't have problems with 1-3 character extensions, but clip if we do!
overflow-x: hidden;
white-space: nowrap; white-space: nowrap;
overflow-x: hidden;
text-overflow: clip; text-overflow: clip;
color: variables.$color-gray-90; color: variables.$color-gray-90;
} }
$message-attachment-padding-horizontal: 8px; // For longer extensions we manually only show the first three characters and an ellipse;
// all we do here is change the font.
.module-message__generic-attachment__text { .module-message__simple-attachment__icon__extension--more-char {
font-size: 9px;
line-height: 11px;
letter-spacing: -0.2px;
}
.module-message__simple-attachment__text {
flex-grow: 1; flex-grow: 1;
margin-inline-start: $message-attachment-padding-horizontal + 2px; // icon-container is 30px wide, 12px margin
// The width of the icon plus our 8px margin plus 1px leeway max-width: calc(100% - 42px);
max-width: calc(100% - 36px);
} }
.module-message__generic-attachment__file-name { .module-message__simple-attachment__file-name {
@include mixins.font-body-1; @include mixins.font-body-1-bold;
margin-top: 2px; margin-top: 2px;
user-select: none; user-select: none;
@@ -931,45 +793,65 @@ $message-attachment-padding-horizontal: 8px;
color: variables.$color-white; color: variables.$color-white;
} }
@include mixins.dark-theme { @include mixins.dark-theme {
color: variables.$color-gray-02; color: variables.$color-white-alpha-90;
} }
} }
.module-message__generic-attachment__file-name--incoming { .module-message__simple-attachment__file-name--incoming {
color: variables.$color-white; color: variables.$color-white;
@include mixins.light-theme { @include mixins.light-theme {
color: variables.$color-gray-90; color: variables.$color-gray-90;
} }
@include mixins.dark-theme { @include mixins.dark-theme {
color: variables.$color-gray-25; color: variables.$color-gray-05;
} }
} }
.module-message__simple-attachment__bottom-row {
display: flex;
flex-direction: row;
}
.module-message__simple-attachment__metadata-container {
flex-grow: 1;
padding-inline-start: 8px;
padding-top: 6px;
align-self: end;
}
.module-message__container--incoming .module-message__container--incoming
.module-message__generic-attachment__file-name--undownloadable { .module-message__simple-attachment__file-name--undownloadable {
color: variables.$color-black-alpha-50;
}
.module-message__generic-attachment__file-size {
@include mixins.font-body-2;
margin-top: 3px;
user-select: none;
@include mixins.light-theme { @include mixins.light-theme {
color: variables.$color-white; color: variables.$color-gray-90;
} }
@include mixins.dark-theme { @include mixins.dark-theme {
color: variables.$color-gray-02; color: variables.$color-gray-05;
} }
} }
.module-message__generic-attachment__file-size--incoming { .module-message__simple-attachment__file-size {
flex-grow: 1;
flex-shrink: 0;
@include mixins.font-body-small;
margin-top: 2px;
user-select: none;
white-space: no-wrap;
@include mixins.light-theme {
color: variables.$color-white-alpha-80;
}
@include mixins.dark-theme {
color: variables.$color-white-alpha-60;
}
}
.module-message__simple-attachment__file-size--incoming {
color: variables.$color-white; color: variables.$color-white;
@include mixins.light-theme { @include mixins.light-theme {
color: variables.$color-gray-90; color: variables.$color-gray-60;
} }
@include mixins.dark-theme { @include mixins.dark-theme {
color: variables.$color-gray-25; color: variables.$color-gray-25;
@@ -977,7 +859,7 @@ $message-attachment-padding-horizontal: 8px;
} }
.module-message__undownloadable-attachment__icon-container { .module-message__undownloadable-attachment__icon-container {
margin-inline-end: $message-attachment-padding-horizontal; margin-inline-end: 8px;
} }
.module-message__undownloadable-attachment__icon-container--file { .module-message__undownloadable-attachment__icon-container--file {
@@ -1010,6 +892,13 @@ $message-attachment-padding-horizontal: 8px;
); );
} }
&--tap-to-view {
@include mixins.color-svg(
'../images/icons/v3/view_once/view_once-slash-bold.svg',
currentColor
);
}
&--small { &--small {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -1022,7 +911,12 @@ $message-attachment-padding-horizontal: 8px;
.module-message__container--incoming .module-message__container--incoming
.module-message__undownloadable-attachment-info--file { .module-message__undownloadable-attachment-info--file {
color: variables.$color-black-alpha-90; @include mixins.light-theme {
color: variables.$color-gray-80;
}
@include mixins.dark-theme {
color: variables.$color-gray-25;
}
} }
.module-message__undownloadable-attachment { .module-message__undownloadable-attachment {
@@ -1227,23 +1121,6 @@ $message-attachment-padding-horizontal: 8px;
user-select: none; user-select: none;
} }
.module-message__author--with-tap-to-view-expired {
@include mixins.font-body-2-bold;
height: 18px;
overflow-x: hidden;
overflow-y: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@include mixins.light-theme {
color: variables.$color-gray-75;
}
@include mixins.dark-theme {
color: variables.$color-white;
}
}
.module-message__author_with_sticker { .module-message__author_with_sticker {
@include mixins.font-body-2-bold; @include mixins.font-body-2-bold;
@@ -1479,16 +1356,6 @@ $message-attachment-padding-horizontal: 8px;
color: variables.$color-gray-25; color: variables.$color-gray-25;
} }
} }
.module-message__metadata__date.module-message__metadata__date--incoming-with-tap-to-view-expired {
color: variables.$color-gray-75;
@include mixins.dark-theme {
color: variables.$color-white-alpha-80;
}
}
.module-message__metadata__date.module-message__metadata__date--outgoing-with-tap-to-view-expired {
color: variables.$color-white-alpha-80;
}
.module-message__metadata__date--with-sticker { .module-message__metadata__date--with-sticker {
@include mixins.light-theme { @include mixins.light-theme {
@@ -2115,6 +1982,34 @@ $message-attachment-padding-horizontal: 8px;
} }
} }
.module-message__tap-to-view__icon--ready {
@include mixins.color-svg-themed(
'../images/icons/v3/view_once/view_once-bold.svg',
variables.$color-gray-90,
variables.$color-gray-05
);
}
.module-message__tap-to-view__icon--outgoing {
@include mixins.color-svg(
'../images/icons/v3/view_once/view_once-dash-bold.svg',
variables.$color-white-alpha-80
);
}
.module-message__tap-to-view__icon--viewed {
@include mixins.color-svg-themed(
'../images/icons/v3/view_once/view_once-viewed-bold.svg',
variables.$color-gray-90,
variables.$color-gray-05
);
}
.module-message__tap-to-view__icon--not-available {
@include mixins.color-svg-themed(
'../images/icons/v3/view_once/view_once-slash-bold.svg',
variables.$color-gray-90,
variables.$color-gray-05
);
}
// Module: Expire Timer // Module: Expire Timer
.module-expire-timer { .module-expire-timer {
@@ -2176,16 +2071,6 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
background-color: variables.$color-gray-25; background-color: variables.$color-gray-25;
} }
} }
.module-expire-timer.module-expire-timer--incoming-with-tap-to-view-expired {
background-color: variables.$color-gray-75;
@include mixins.dark-theme {
background-color: variables.$color-white-alpha-80;
}
}
.module-expire-timer.module-expire-timer--outgoing-with-tap-to-view-expired {
background-color: variables.$color-white-alpha-80;
}
.module-expire-timer--with-sticker { .module-expire-timer--with-sticker {
@include mixins.light-theme { @include mixins.light-theme {
background-color: variables.$color-gray-60; background-color: variables.$color-gray-60;
@@ -2249,30 +2134,13 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
// Module: Embedded Contact // Module: Embedded Contact
.module-embedded-contact { .module-embedded-contact {
@include mixins.button-reset; width: 100%;
padding-top: 4px;
padding-bottom: 4px;
& { display: flex;
width: 100%; flex-direction: row;
padding: 5px; align-items: center;
display: flex;
flex-direction: row;
align-items: center;
}
@include mixins.keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px variables.$color-ultramarine;
}
}
}
.module-embedded-contact--outgoing {
@include mixins.keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px variables.$color-white;
}
}
} }
.module-embedded-contact--with-content-above { .module-embedded-contact--with-content-above {
@@ -2283,15 +2151,51 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
padding-bottom: 4px; padding-bottom: 4px;
} }
.module-embedded-contact__spinner-container { .module-embedded-contact__avatar-container {
padding-inline: 5px; height: 52px;
width: 52px;
border-radius: 26px;
@include mixins.keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px variables.$color-ultramarine;
}
}
}
.module-embedded-contact--outgoing .module-embedded-contact__avatar-container {
@include mixins.keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px variables.$color-white;
}
}
}
.module-embedded-contact__avatar-container .AttachmentStatusIcon__container {
margin: 0;
height: 52px;
width: 52px;
}
.module-embedded-contact__avatar-container
.AttachmentStatusIcon__circle-icon-container {
margin: 0;
}
.module-embedded-contact__avatar-container {
@include mixins.button-reset;
}
.module-embedded-contact__avatar-container
.AttachmentStatusIcon__circle-icon-container {
@include mixins.position-absolute-center;
} }
.module-embedded-contact__text-container { .module-embedded-contact__text-container {
flex-grow: 1; flex-grow: 1;
margin-inline-start: 8px; margin-inline-start: 12px;
max-width: calc(100% - 58px); max-width: calc(100% - 64px);
} }
.module-embedded-contact__contact-name { .module-embedded-contact__contact-name {
@@ -2357,7 +2261,35 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
} }
.module-contact-detail__avatar { .module-contact-detail__avatar {
margin-bottom: 4px; @include mixins.button-reset;
& {
cursor: default;
position: relative;
height: 80px;
width: 80px;
margin-bottom: 10px;
border-radius: 40px;
}
@include mixins.light-theme {
background-color: variables.$color-gray-05;
}
@include mixins.dark-theme {
background-color: variables.$color-gray-80;
}
}
.module-contact-detail__avatar--clickable {
cursor: pointer;
}
.module-contact-detail__avatar .AttachmentStatusIcon__container {
height: 80px;
width: 80px;
margin: 0;
}
.module-contact-detail__avatar .AttachmentStatusIcon__circle-icon-container {
@include mixins.position-absolute-center;
} }
.module-contact-detail__contact-name { .module-contact-detail__contact-name {
@@ -2677,7 +2609,10 @@ button.ConversationDetails__action-button {
width: 48px; width: 48px;
height: 48px; height: 48px;
@include mixins.color-svg('../images/file.svg', variables.$color-gray-45); @include mixins.color-svg(
'../images/generic-file.svg',
variables.$color-gray-45
);
} }
.module-document-list-item__metadata { .module-document-list-item__metadata {
@@ -3459,7 +3394,7 @@ button.module-image__border-overlay:focus {
.module-staged-generic-attachment__icon { .module-staged-generic-attachment__icon {
margin-top: 30px; margin-top: 30px;
background: url('../images/file-gradient.svg') no-repeat center; background: url('../images/generic-file.svg') no-repeat center;
height: 44px; height: 44px;
width: 56px; width: 56px;
margin-inline: 32px; margin-inline: 32px;
@@ -0,0 +1,164 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
@use '../variables';
.AttachmentStatusIcon__container {
position: relative;
width: 36px;
height: 40px;
margin-top: 2px;
margin-bottom: 2px;
margin-inline-end: 12px;
}
.AttachmentStatusIcon__circle-icon-container {
height: 36px;
width: 36px;
margin-top: 2px;
margin-bottom: 2px;
border-radius: 18px;
position: relative;
@include mixins.light-theme {
background-color: variables.$color-white-alpha-20;
&:hover {
background-color: variables.$color-white-alpha-30;
}
&:active {
background-color: variables.$color-white-alpha-40;
}
}
@include mixins.dark-theme {
background-color: variables.$color-white-alpha-20;
&:hover {
background-color: variables.$color-white-alpha-30;
}
&:active {
background-color: variables.$color-white-alpha-40;
}
}
}
.AttachmentStatusIcon__circle-icon-container--incoming {
@include mixins.light-theme {
background-color: variables.$color-white-alpha-80;
&:hover {
background-color: variables.$color-white-alpha-60;
}
&:active {
background-color: variables.$color-white-alpha-40;
}
}
@include mixins.dark-theme {
background-color: variables.$color-white-alpha-20;
}
}
.AttachmentStatusIcon__circle-icon-container--disabled {
@include mixins.light-theme {
background-color: variables.$color-white-alpha-10;
&:hover {
background-color: variables.$color-white-alpha-10;
}
&:active {
background-color: variables.$color-white-alpha-10;
}
}
@include mixins.dark-theme {
background-color: variables.$color-white-alpha-10;
&:hover {
background-color: variables.$color-white-alpha-10;
}
&:active {
background-color: variables.$color-white-alpha-10;
}
}
}
.AttachmentStatusIcon__circle-icon-container--incoming.AttachmentStatusIcon__circle-icon-container--disabled {
@include mixins.light-theme {
background-color: variables.$color-white-alpha-60;
&:hover {
background-color: variables.$color-white-alpha-60;
}
&:active {
background-color: variables.$color-white-alpha-60;
}
}
@include mixins.dark-theme {
background-color: variables.$color-white-alpha-10;
&:hover {
background-color: variables.$color-white-alpha-10;
}
&:active {
background-color: variables.$color-white-alpha-10;
}
}
}
.AttachmentStatusIcon__circle-icon {
@include mixins.position-absolute-center;
& {
height: 20px;
width: 20px;
}
}
.AttachmentStatusIcon__circle-icon--x {
@include mixins.color-svg-themed(
'../images/icons/v3/x/x-bold.svg',
variables.$color-white,
variables.$color-white-alpha-90
);
}
.AttachmentStatusIcon__circle-icon--arrow-down {
@include mixins.color-svg-themed(
'../images/icons/v3/arrow/arrow-down-bold.svg',
variables.$color-white,
variables.$color-white-alpha-90
);
}
.AttachmentStatusIcon__circle-icon--incoming {
@include mixins.light-theme {
background-color: variables.$color-gray-90;
}
@include mixins.dark-theme {
background-color: variables.$color-white-alpha-90;
}
}
.AttachmentStatusIcon__progress-container {
.ProgressCircle .ProgressCircle__background {
@include mixins.light-theme {
stroke: none;
fill: none;
}
@include mixins.dark-theme {
stroke: none;
fill: none;
}
}
.ProgressCircle .ProgressCircle__fill {
@include mixins.light-theme {
stroke: variables.$color-white;
}
@include mixins.dark-theme {
stroke: variables.$color-white-alpha-90;
}
}
}
.AttachmentStatusIcon__progress-container--incoming {
.ProgressCircle .ProgressCircle__fill {
@include mixins.light-theme {
stroke: variables.$color-gray-90;
}
@include mixins.dark-theme {
stroke: variables.$color-white-alpha-90;
}
}
}
+82 -11
View File
@@ -7,6 +7,7 @@
.PlaybackButton { .PlaybackButton {
@include mixins.button-reset; @include mixins.button-reset;
position: relative;
& { & {
flex-shrink: 0; flex-shrink: 0;
@@ -24,15 +25,20 @@
@mixin audio-icon($name, $icon, $color) { @mixin audio-icon($name, $icon, $color) {
&.PlaybackButton--#{$name}::before { &.PlaybackButton--#{$name}::before {
@include mixins.position-absolute-center;
@include mixins.color-svg('../images/icons/#{$icon}.svg', $color, false); @include mixins.color-svg('../images/icons/#{$icon}.svg', $color, false);
& {
height: 20px;
width: 20px;
}
} }
} }
@mixin all-audio-icons($color) { @mixin all-audio-icons($color) {
@include audio-icon(play, v3/play/play-fill, $color); @include audio-icon(play, v3/play/play-fill, $color);
@include audio-icon(pause, v3/pause/pause-fill, $color); @include audio-icon(pause, v3/pause/pause-fill, $color);
@include audio-icon(download, v3/arrow/arrow-down, $color); @include audio-icon(not-downloaded, v3/arrow/arrow-down, $color);
@include audio-icon(pending, v2/audio-spinner-arc-22, $color); @include audio-icon(downloading, v3/x/x-bold, $color);
} }
&--variant-message { &--variant-message {
@@ -43,6 +49,8 @@
&--variant-mini { &--variant-mini {
&::before { &::before {
-webkit-mask-size: 100% !important; -webkit-mask-size: 100% !important;
width: 8px !important;
height: 8px !important;
} }
width: 14px; width: 14px;
height: 14px; height: 14px;
@@ -50,41 +58,104 @@
&--variant-draft { &--variant-draft {
&::before { &::before {
-webkit-mask-size: 100% !important; -webkit-mask-size: 100% !important;
width: 10px !important;
height: 10px !important;
} }
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
&--pending { &--computing {
cursor: auto; cursor: auto;
} }
&__SpinnerV2-container {
&--pending::before { @include mixins.position-absolute-center;
animation: rotate 1000ms linear infinite; }
.ProgressCircle {
@include mixins.position-absolute-center;
.ProgressCircle__background {
stroke: none;
}
}
@include mixins.dark-theme {
.ProgressCircle .ProgressCircle__background {
stroke: none;
}
} }
@include mixins.light-theme { @include mixins.light-theme {
.ProgressCircle .ProgressCircle__fill {
stroke: variables.$color-white;
}
.SpinnerV2 .SpinnerV2__Path {
stroke: variables.$color-white;
}
@include all-audio-icons(variables.$color-gray-90);
&--context-incoming { &--context-incoming {
&.PlaybackButton--variant-message { &.PlaybackButton--variant-message {
background: variables.$color-white; background: variables.$color-white-alpha-80;
&:hover {
background: variables.$color-white-alpha-60;
}
&:active {
background: variables.$color-white-alpha-40;
}
}
.ProgressCircle .ProgressCircle__fill {
stroke: variables.$color-gray-90;
}
.SpinnerV2 .SpinnerV2__Path {
stroke: variables.$color-gray-90;
} }
} }
@include all-audio-icons(variables.$color-gray-60);
} }
@include mixins.dark-theme { @include mixins.dark-theme {
.ProgressCircle .ProgressCircle__fill {
stroke: variables.$color-white-alpha-90;
}
.SpinnerV2 .SpinnerV2__Path {
stroke: variables.$color-white-alpha-90;
}
@include all-audio-icons(variables.$color-white-alpha-90);
&--context-incoming { &--context-incoming {
&.PlaybackButton--variant-message { &.PlaybackButton--variant-message {
background: variables.$color-gray-60; background: variables.$color-white-alpha-20;
&:hover {
background: variables.$color-white-alpha-30;
}
&:active {
background: variables.$color-white-alpha-40;
}
}
.ProgressCircle .ProgressCircle__fill {
stroke: variables.$color-white-alpha-90;
}
.SpinnerV2 .SpinnerV2__Path {
stroke: variables.$color-white-alpha-90;
} }
} }
@include all-audio-icons(variables.$color-gray-15);
} }
&--context-outgoing { &--context-outgoing {
&.PlaybackButton--variant-message { &.PlaybackButton--variant-message {
background: variables.$color-white-alpha-20; background: variables.$color-white-alpha-20;
&:hover {
background: variables.$color-white-alpha-30;
}
&:active {
background: variables.$color-white-alpha-40;
}
} }
@include all-audio-icons(variables.$color-white); @include all-audio-icons(variables.$color-white);
} }
@include mixins.dark-theme {
&--context-outgoing {
@include all-audio-icons(variables.$color-white-alpha-90);
}
}
} }
+18
View File
@@ -107,6 +107,10 @@
&--privacy { &--privacy {
@include preferences-icon('../images/icons/v3/lock/lock.svg'); @include preferences-icon('../images/icons/v3/lock/lock.svg');
} }
&--data-usage {
@include preferences-icon('../images/icons/v3/data/data.svg');
}
} }
&__settings-pane { &__settings-pane {
@@ -230,6 +234,16 @@
padding-inline: 24px; padding-inline: 24px;
} }
&__option-name {
@include mixins.font-body-1;
@include mixins.light-theme {
color: variables.$color-gray-90;
}
@include mixins.dark-theme {
color: variables.$color-gray-05;
}
}
&__description { &__description {
@include mixins.font-subtitle; @include mixins.font-subtitle;
@include mixins.light-theme { @include mixins.light-theme {
@@ -242,6 +256,10 @@
&--error { &--error {
color: variables.$color-accent-red !important; color: variables.$color-accent-red !important;
} }
&--medium {
@include mixins.font-body-2;
}
} }
&__select { &__select {
@@ -0,0 +1,18 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
@use '../variables';
.TapToViewNotAvailableModal__width-container {
max-width: 440px;
}
.TapToViewNotAvailableModal__body {
padding-block: 16px 0;
padding-inline: 16px;
}
.TapToViewNotAvailableModal .module-Button {
padding-inline: 24px;
}
+2
View File
@@ -26,6 +26,7 @@
@use 'components/App.scss'; @use 'components/App.scss';
@use 'components/AttachmentDetailPill.scss'; @use 'components/AttachmentDetailPill.scss';
@use 'components/AttachmentNotAvailableModal.scss'; @use 'components/AttachmentNotAvailableModal.scss';
@use 'components/AttachmentStatusIcon.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';
@@ -179,6 +180,7 @@
@use 'components/StoryViewsNRepliesModal.scss'; @use 'components/StoryViewsNRepliesModal.scss';
@use 'components/SystemMessage.scss'; @use 'components/SystemMessage.scss';
@use 'components/Tabs.scss'; @use 'components/Tabs.scss';
@use 'components/TapToViewNotAvailableModal.scss';
@use 'components/TextAttachment.scss'; @use 'components/TextAttachment.scss';
@use 'components/TimelineDateHeader.scss'; @use 'components/TimelineDateHeader.scss';
@use 'components/TimelineFloatingHeader.scss'; @use 'components/TimelineFloatingHeader.scss';
+13 -4
View File
@@ -67,6 +67,7 @@ const MESSAGE_DEFAULT_PROPS = {
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled, showLightboxForViewOnceMedia: shouldNeverBeCalled,
showMediaNoLongerAvailableToast: shouldNeverBeCalled, showMediaNoLongerAvailableToast: shouldNeverBeCalled,
showTapToViewNotAvailableModal: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled, startConversation: shouldNeverBeCalled,
textDirection: TextDirection.Default, textDirection: TextDirection.Default,
viewStory: shouldNeverBeCalled, viewStory: shouldNeverBeCalled,
@@ -128,8 +129,12 @@ export function EditHistoryMessagesModal({
isEditedMessage isEditedMessage
isSpoilerExpanded={revealedSpoilersById[currentMessageId] || {}} isSpoilerExpanded={revealedSpoilersById[currentMessageId] || {}}
key={currentMessage.timestamp} key={currentMessage.timestamp}
kickOffAttachmentDownload={kickOffAttachmentDownload} kickOffAttachmentDownload={() =>
cancelAttachmentDownload={cancelAttachmentDownload} kickOffAttachmentDownload({ messageId: currentMessage.id })
}
cancelAttachmentDownload={() =>
cancelAttachmentDownload({ messageId: currentMessage.id })
}
messageExpanded={(messageId, displayLimit) => { messageExpanded={(messageId, displayLimit) => {
const update = { const update = {
...displayLimitById, ...displayLimitById,
@@ -192,8 +197,12 @@ export function EditHistoryMessagesModal({
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
isSpoilerExpanded={revealedSpoilersById[syntheticId] || {}} isSpoilerExpanded={revealedSpoilersById[syntheticId] || {}}
kickOffAttachmentDownload={kickOffAttachmentDownload} kickOffAttachmentDownload={() =>
cancelAttachmentDownload={cancelAttachmentDownload} kickOffAttachmentDownload({ messageId: currentMessage.id })
}
cancelAttachmentDownload={() =>
cancelAttachmentDownload({ messageId: currentMessage.id })
}
messageExpanded={(messageId, displayLimit) => { messageExpanded={(messageId, displayLimit) => {
const update = { const update = {
...displayLimitById, ...displayLimitById,
+20
View File
@@ -23,6 +23,10 @@ import { WhatsNewModal } from './WhatsNewModal';
import { MediaPermissionsModal } from './MediaPermissionsModal'; import { MediaPermissionsModal } from './MediaPermissionsModal';
import type { StartCallData } from './ConfirmLeaveCallModal'; import type { StartCallData } from './ConfirmLeaveCallModal';
import type { AttachmentNotAvailableModalType } from './AttachmentNotAvailableModal'; import type { AttachmentNotAvailableModalType } from './AttachmentNotAvailableModal';
import {
TapToViewNotAvailableModal,
type DataPropsType as TapToViewNotAvailablePropsType,
} from './TapToViewNotAvailableModal';
// NOTE: All types should be required for this component so that the smart // NOTE: All types should be required for this component so that the smart
// component gives you type errors when adding/removing props. // component gives you type errors when adding/removing props.
@@ -117,6 +121,9 @@ export type PropsType = {
| SafetyNumberChangedBlockingDataType | SafetyNumberChangedBlockingDataType
| undefined; | undefined;
renderSendAnywayDialog: () => JSX.Element; renderSendAnywayDialog: () => JSX.Element;
// TapToViewNotAvailableModal
tapToViewNotAvailableModalProps: TapToViewNotAvailablePropsType | undefined;
hideTapToViewNotAvailableModal: () => void;
// UserNotFoundModal // UserNotFoundModal
hideUserNotFoundModal: () => unknown; hideUserNotFoundModal: () => unknown;
userNotFoundModalState: UserNotFoundModalStateType | undefined; userNotFoundModalState: UserNotFoundModalStateType | undefined;
@@ -201,6 +208,9 @@ export function GlobalModalContainer({
hasSafetyNumberChangeModal, hasSafetyNumberChangeModal,
safetyNumberChangedBlockingData, safetyNumberChangedBlockingData,
renderSendAnywayDialog, renderSendAnywayDialog,
// TapToViewNotAvailableModal
tapToViewNotAvailableModalProps,
hideTapToViewNotAvailableModal,
// UserNotFoundModal // UserNotFoundModal
hideUserNotFoundModal, hideUserNotFoundModal,
userNotFoundModalState, userNotFoundModalState,
@@ -364,5 +374,15 @@ export function GlobalModalContainer({
return renderAttachmentNotAvailableModal(); return renderAttachmentNotAvailableModal();
} }
if (tapToViewNotAvailableModalProps) {
return (
<TapToViewNotAvailableModal
i18n={i18n}
onClose={hideTapToViewNotAvailableModal}
{...tapToViewNotAvailableModalProps}
/>
);
}
return null; return null;
} }
+2 -2
View File
@@ -64,7 +64,7 @@ export function MiniPlayer({
}, [state, onPause, onPlay]); }, [state, onPause, onPlay]);
let label: string | undefined; let label: string | undefined;
let mod: 'play' | 'pause' | 'pending'; let mod: 'play' | 'pause' | 'computing';
switch (state) { switch (state) {
case PlayerState.playing: case PlayerState.playing:
label = i18n('icu:MessageAudio--pause'); label = i18n('icu:MessageAudio--pause');
@@ -76,7 +76,7 @@ export function MiniPlayer({
break; break;
case PlayerState.loading: case PlayerState.loading:
label = i18n('icu:MessageAudio--pending'); label = i18n('icu:MessageAudio--pending');
mod = 'pending'; mod = 'computing';
break; break;
default: default:
throw new TypeError(`Missing case ${state}`); throw new TypeError(`Missing case ${state}`);
+17 -3
View File
@@ -1,12 +1,14 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { useContext } from 'react';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react'; import type { Meta } from '@storybook/react';
import type { ButtonProps } from './PlaybackButton'; import type { ButtonProps } from './PlaybackButton';
import { PlaybackButton } from './PlaybackButton'; import { PlaybackButton } from './PlaybackButton';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
import { ThemeType } from '../types/Util';
export default { export default {
title: 'components/PlaybackButton', title: 'components/PlaybackButton',
@@ -20,6 +22,9 @@ const rowStyles: CSSProperties = {
}; };
export function Default(): JSX.Element { export function Default(): JSX.Element {
const theme = useContext(StorybookThemeContext);
const themeIncomingColor = theme === ThemeType.light ? '#e9e9e9' : '#3b3b3b';
return ( return (
<> <>
{(['message', 'draft', 'mini'] as const).map(variant => ( {(['message', 'draft', 'mini'] as const).map(variant => (
@@ -28,10 +33,19 @@ export function Default(): JSX.Element {
<div <div
style={{ style={{
...rowStyles, ...rowStyles,
background: context === 'outgoing' ? '#2c6bed' : undefined, background:
context === 'outgoing' ? '#2c6bed' : themeIncomingColor,
}} }}
> >
{(['play', 'download', 'pending', 'pause'] as const).map(mod => ( {(
[
'play',
'pause',
'not-downloaded',
'downloading',
'computing',
] as const
).map(mod => (
<PlaybackButton <PlaybackButton
key={`${variant}_${context}_${mod}`} key={`${variant}_${context}_${mod}`}
variant={variant} variant={variant}
+49 -3
View File
@@ -5,6 +5,8 @@ import { animated, useSpring } from '@react-spring/web';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useReducedMotion } from '../hooks/useReducedMotion'; import { useReducedMotion } from '../hooks/useReducedMotion';
import { ProgressCircle } from './ProgressCircle';
import { SpinnerV2 } from './SpinnerV2';
const SPRING_CONFIG = { const SPRING_CONFIG = {
mass: 0.5, mass: 0.5,
@@ -16,7 +18,8 @@ const SPRING_CONFIG = {
export type ButtonProps = { export type ButtonProps = {
context?: 'incoming' | 'outgoing'; context?: 'incoming' | 'outgoing';
variant: 'message' | 'mini' | 'draft'; variant: 'message' | 'mini' | 'draft';
mod: 'play' | 'pause' | 'download' | 'pending'; mod: 'play' | 'pause' | 'not-downloaded' | 'downloading' | 'computing';
downloadFraction?: number;
label: string; label: string;
visible?: boolean; visible?: boolean;
onClick: () => void; onClick: () => void;
@@ -27,7 +30,22 @@ export type ButtonProps = {
/** Handles animations, key events, and stopping event propagation */ /** Handles animations, key events, and stopping event propagation */
export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>( export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
function ButtonInner(props, ref) { function ButtonInner(props, ref) {
const { mod, label, variant, onClick, context, visible = true } = props; const {
context,
downloadFraction,
label,
mod,
onClick,
variant,
visible = true,
} = props;
let size = 36;
if (variant === 'mini') {
size = 14;
} else if (variant === 'draft') {
size = 18;
}
const reducedMotion = useReducedMotion(); const reducedMotion = useReducedMotion();
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME // eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
const [animProps] = useSpring( const [animProps] = useSpring(
@@ -64,6 +82,32 @@ export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
[onClick] [onClick]
); );
let content: JSX.Element | null = null;
const strokeWidth = variant === 'message' ? 2 : 1;
if (mod === 'downloading' && downloadFraction) {
content = (
<ProgressCircle
fractionComplete={downloadFraction}
width={size}
strokeWidth={strokeWidth}
/>
);
} else if (
mod === 'computing' ||
(mod === 'downloading' && !downloadFraction)
) {
content = (
<div className="PlaybackButton__SpinnerV2-container">
<SpinnerV2
className="PlaybackButton__SpinnerV2"
size={size}
strokeWidth={strokeWidth * 2}
marginRatio={1}
/>
</div>
);
}
const buttonComponent = ( const buttonComponent = (
<button <button
type="button" type="button"
@@ -78,7 +122,9 @@ export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
onKeyDown={onButtonKeyDown} onKeyDown={onButtonKeyDown}
tabIndex={0} tabIndex={0}
aria-label={label} aria-label={label}
/> >
{content}
</button>
); );
if (variant === 'message') { if (variant === 'message') {
+7
View File
@@ -50,6 +50,12 @@ export default {
args: { args: {
i18n, i18n,
autoDownloadAttachment: {
photos: true,
videos: false,
audio: false,
documents: false,
},
availableCameras: [ availableCameras: [
{ {
deviceId: deviceId:
@@ -133,6 +139,7 @@ export default {
makeSyncRequest: action('makeSyncRequest'), makeSyncRequest: action('makeSyncRequest'),
onAudioNotificationsChange: action('onAudioNotificationsChange'), onAudioNotificationsChange: action('onAudioNotificationsChange'),
onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'), onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'),
onAutoDownloadAttachmentChange: action('onAutoDownloadAttachmentChange'),
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'), onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
onAutoLaunchChange: action('onAutoLaunchChange'), onAutoLaunchChange: action('onAutoLaunchChange'),
onCallNotificationsChange: action('onCallNotificationsChange'), onCallNotificationsChange: action('onCallNotificationsChange'),
+125 -19
View File
@@ -17,6 +17,7 @@ import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MediaDeviceSettings } from '../types/Calling'; import type { MediaDeviceSettings } from '../types/Calling';
import type { import type {
AutoDownloadAttachmentType,
NotificationSettingType, NotificationSettingType,
SentMediaQualitySettingType, SentMediaQualitySettingType,
ZoomFactorType, ZoomFactorType,
@@ -73,6 +74,7 @@ type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
export type PropsDataType = { export type PropsDataType = {
// Settings // Settings
autoDownloadAttachment: AutoDownloadAttachmentType;
blockedCount: number; blockedCount: number;
customColors: Record<string, CustomColorType>; customColors: Record<string, CustomColorType>;
defaultConversationColor: DefaultConversationColorType; defaultConversationColor: DefaultConversationColorType;
@@ -162,6 +164,9 @@ type PropsFunctionType = {
// Change handlers // Change handlers
onAudioNotificationsChange: CheckboxChangeHandlerType; onAudioNotificationsChange: CheckboxChangeHandlerType;
onAutoConvertEmojiChange: CheckboxChangeHandlerType; onAutoConvertEmojiChange: CheckboxChangeHandlerType;
onAutoDownloadAttachmentChange: (
setting: AutoDownloadAttachmentType
) => unknown;
onAutoDownloadUpdateChange: CheckboxChangeHandlerType; onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
onAutoLaunchChange: CheckboxChangeHandlerType; onAutoLaunchChange: CheckboxChangeHandlerType;
onCallNotificationsChange: CheckboxChangeHandlerType; onCallNotificationsChange: CheckboxChangeHandlerType;
@@ -209,6 +214,7 @@ enum Page {
Calls = 'Calls', Calls = 'Calls',
Notifications = 'Notifications', Notifications = 'Notifications',
Privacy = 'Privacy', Privacy = 'Privacy',
DataUsage = 'DataUsage',
// Sub pages // Sub pages
ChatColor = 'ChatColor', ChatColor = 'ChatColor',
@@ -245,6 +251,7 @@ const DEFAULT_ZOOM_FACTORS = [
export function Preferences({ export function Preferences({
addCustomColor, addCustomColor,
autoDownloadAttachment,
availableCameras, availableCameras,
availableLocales, availableLocales,
availableMicrophones, availableMicrophones,
@@ -295,6 +302,7 @@ export function Preferences({
notificationContent, notificationContent,
onAudioNotificationsChange, onAudioNotificationsChange,
onAutoConvertEmojiChange, onAutoConvertEmojiChange,
onAutoDownloadAttachmentChange,
onAutoDownloadUpdateChange, onAutoDownloadUpdateChange,
onAutoLaunchChange, onAutoLaunchChange,
onCallNotificationsChange, onCallNotificationsChange,
@@ -884,25 +892,6 @@ export function Preferences({
name="autoConvertEmoji" name="autoConvertEmoji"
onChange={onAutoConvertEmojiChange} onChange={onAutoConvertEmojiChange}
/> />
<Control
left={i18n('icu:Preferences__sent-media-quality')}
right={
<Select
onChange={onSentMediaQualityChange}
options={[
{
text: i18n('icu:sentMediaQualityStandard'),
value: 'standard',
},
{
text: i18n('icu:sentMediaQualityHigh'),
value: 'high',
},
]}
value={sentMediaQualitySetting}
/>
}
/>
</SettingsRow> </SettingsRow>
{isSyncSupported && ( {isSyncSupported && (
<SettingsRow> <SettingsRow>
@@ -1404,6 +1393,111 @@ export function Preferences({
) : null} ) : null}
</> </>
); );
} else if (page === Page.DataUsage) {
settings = (
<>
<div className="Preferences__title">
<div className="Preferences__title--header">
{i18n('icu:Preferences__button--data-usage')}
</div>
</div>
<SettingsRow title={i18n('icu:Preferences__media-auto-download')}>
<Checkbox
checked={autoDownloadAttachment.photos !== false}
label={i18n('icu:Preferences__media-auto-download__photos')}
moduleClassName="Preferences__checkbox"
name="autoLaunch"
onChange={(newValue: boolean) =>
onAutoDownloadAttachmentChange({
...autoDownloadAttachment,
photos: newValue,
})
}
/>
<Checkbox
checked={autoDownloadAttachment.videos !== false}
label={i18n('icu:Preferences__media-auto-download__videos')}
moduleClassName="Preferences__checkbox"
name="autoLaunch"
onChange={(newValue: boolean) =>
onAutoDownloadAttachmentChange({
...autoDownloadAttachment,
videos: newValue,
})
}
/>
<Checkbox
checked={autoDownloadAttachment.audio !== false}
label={i18n('icu:Preferences__media-auto-download__audio')}
moduleClassName="Preferences__checkbox"
name="autoLaunch"
onChange={(newValue: boolean) =>
onAutoDownloadAttachmentChange({
...autoDownloadAttachment,
audio: newValue,
})
}
/>
<Checkbox
checked={autoDownloadAttachment.documents !== false}
label={i18n('icu:Preferences__media-auto-download__documents')}
moduleClassName="Preferences__checkbox"
name="autoLaunch"
onChange={(newValue: boolean) =>
onAutoDownloadAttachmentChange({
...autoDownloadAttachment,
documents: newValue,
})
}
/>
<div className="Preferences__padding">
<div
className={classNames(
'Preferences__description',
'Preferences__description--medium'
)}
>
{i18n('icu:Preferences__media-auto-download__description')}
</div>
</div>
</SettingsRow>
<SettingsRow>
<Control
left={
<>
<div className="Preferences__option-name">
{i18n('icu:Preferences__sent-media-quality')}
</div>
<div
className={classNames(
'Preferences__description',
'Preferences__description--medium'
)}
>
{i18n('icu:Preferences__sent-media-quality__description')}
</div>
</>
}
right={
<Select
onChange={onSentMediaQualityChange}
options={[
{
text: i18n('icu:sentMediaQualityStandard'),
value: 'standard',
},
{
text: i18n('icu:sentMediaQualityHigh'),
value: 'high',
},
]}
value={sentMediaQualitySetting}
/>
}
/>
</SettingsRow>
</>
);
} else if (page === Page.ChatColor) { } else if (page === Page.ChatColor) {
settings = ( settings = (
<> <>
@@ -1649,6 +1743,18 @@ export function Preferences({
> >
{i18n('icu:Preferences__button--privacy')} {i18n('icu:Preferences__button--privacy')}
</button> </button>
<button
type="button"
className={classNames({
Preferences__button: true,
'Preferences__button--data-usage': true,
'Preferences__button--selected': page === Page.DataUsage,
})}
onClick={() => setPage(Page.DataUsage)}
>
{i18n('icu:Preferences__button--data-usage')}
</button>
</div> </div>
<div className="Preferences__settings-pane" ref={settingsPaneRef}> <div className="Preferences__settings-pane" ref={settingsPaneRef}>
{settings} {settings}
+7 -1
View File
@@ -14,8 +14,14 @@ export function ProgressCircle({
}): JSX.Element { }): JSX.Element {
const radius = width / 2 - strokeWidth / 2; const radius = width / 2 - strokeWidth / 2;
const circumference = radius * 2 * Math.PI; const circumference = radius * 2 * Math.PI;
const widthInPixels = `${width}px`;
return ( return (
<svg className="ProgressCircle" width={width} height={width}> <svg
className="ProgressCircle"
width={widthInPixels}
height={widthInPixels}
>
<circle <circle
className="ProgressCircle__background" className="ProgressCircle__background"
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
+3
View File
@@ -28,6 +28,9 @@ export type Props = {
svgSize: SpinnerSvgSize; svgSize: SpinnerSvgSize;
}; };
/**
* @deprecated This has been superceded by the more customizable SpinnerV2 component.
*/
export function Spinner({ export function Spinner({
ariaLabel, ariaLabel,
direction, direction,
+23 -7
View File
@@ -2,27 +2,43 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
import type { Props } from './SpinnerV2';
import { SpinnerV2 } from './SpinnerV2'; import { SpinnerV2 } from './SpinnerV2';
import type { ComponentMeta } from '../storybook/types'; import type { ComponentMeta } from '../storybook/types';
import type { Props } from './SpinnerV2';
export default { export default {
title: 'Components/SpinnerV2', title: 'Components/SpinnerV2',
component: SpinnerV2, component: SpinnerV2,
argTypes: { argTypes: {
size: { control: { type: 'number' } }, size: { control: { type: 'number' } },
strokeWidth: { control: { type: 'number' } },
marginRatio: { control: { type: 'number' } },
}, },
args: { args: { size: 36, strokeWidth: 2, className: undefined, marginRatio: 0.8 },
className: undefined,
size: 16,
strokeWidth: 3,
},
} satisfies ComponentMeta<Props>; } satisfies ComponentMeta<Props>;
export function Normal(args: Props): JSX.Element { export function Default(args: Props): JSX.Element {
return <SpinnerV2 {...args} />; return <SpinnerV2 {...args} />;
} }
export function Thin(args: Props): JSX.Element {
return <SpinnerV2 {...args} strokeWidth={1} />;
}
export function Thick(args: Props): JSX.Element {
return <SpinnerV2 {...args} strokeWidth={6} />;
}
export function NoMargin(args: Props): JSX.Element {
return <SpinnerV2 {...args} marginRatio={1} strokeWidth={6} />;
}
export function BigMargin(args: Props): JSX.Element {
return <SpinnerV2 {...args} marginRatio={0.5} strokeWidth={6} />;
}
export function Styled(args: Props): JSX.Element { export function Styled(args: Props): JSX.Element {
return ( return (
<div> <div>
+4 -1
View File
@@ -6,15 +6,18 @@ import classNames from 'classnames';
export type Props = { export type Props = {
className?: string; className?: string;
marginRatio?: number;
size: number; size: number;
strokeWidth: number; strokeWidth: number;
}; };
export function SpinnerV2({ export function SpinnerV2({
className, className,
marginRatio,
size, size,
strokeWidth, strokeWidth,
}: Props): JSX.Element { }: Props): JSX.Element {
const radius = Math.min(size - strokeWidth / 2, size * (marginRatio ?? 0.8));
return ( return (
<svg <svg
className={classNames('SpinnerV2', className)} className={classNames('SpinnerV2', className)}
@@ -28,7 +31,7 @@ export function SpinnerV2({
className="SpinnerV2__Path" className="SpinnerV2__Path"
cx={size} cx={size}
cy={size} cy={size}
r={size * 0.8} r={radius}
fill="none" fill="none"
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
/> />
@@ -75,6 +75,7 @@ const MESSAGE_DEFAULT_PROPS = {
showLightbox: shouldNeverBeCalled, showLightbox: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled, showLightboxForViewOnceMedia: shouldNeverBeCalled,
showMediaNoLongerAvailableToast: shouldNeverBeCalled, showMediaNoLongerAvailableToast: shouldNeverBeCalled,
showTapToViewNotAvailableModal: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled, startConversation: shouldNeverBeCalled,
theme: ThemeType.dark, theme: ThemeType.dark,
viewStory: shouldNeverBeCalled, viewStory: shouldNeverBeCalled,
@@ -0,0 +1,46 @@
// 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 './TapToViewNotAvailableModal';
import {
TapToViewNotAvailableModal,
TapToViewNotAvailableType,
} from './TapToViewNotAvailableModal';
import type { ComponentMeta } from '../storybook/types';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/TapToViewNotAvailableModal',
component: TapToViewNotAvailableModal,
args: {
type: TapToViewNotAvailableType.Error,
parameters: {
name: 'FirstName',
},
i18n,
onClose: action('onClose'),
},
} satisfies ComponentMeta<PropsType>;
export function Error(args: PropsType): JSX.Element {
return (
<TapToViewNotAvailableModal
{...args}
type={TapToViewNotAvailableType.Error}
/>
);
}
export function Expired(args: PropsType): JSX.Element {
return (
<TapToViewNotAvailableModal
{...args}
type={TapToViewNotAvailableType.Expired}
/>
);
}
@@ -0,0 +1,61 @@
// 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 DataPropsType = {
parameters: {
name: string;
};
type: TapToViewNotAvailableType;
};
export type HousekeepingPropsType = {
i18n: LocalizerType;
onClose: () => void;
};
export type PropsType = DataPropsType & HousekeepingPropsType;
export enum TapToViewNotAvailableType {
Error = 'Error',
Expired = 'Expired',
}
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}
export function TapToViewNotAvailableModal(props: PropsType): JSX.Element {
const { i18n, onClose, parameters, type } = props;
const footer = (
<Button onClick={onClose} ref={focusRef} variant={ButtonVariant.Primary}>
{i18n('icu:Confirmation--confirm')}
</Button>
);
const bodyText =
type === TapToViewNotAvailableType.Expired
? i18n('icu:TapToViewNotAvailableModal__body--expired', parameters)
: i18n('icu:TapToViewNotAvailableModal__body--error', parameters);
return (
<Modal
modalName="TapToViewNotAvailableModal"
moduleClassName="TapToViewNotAvailableModal"
i18n={i18n}
onClose={onClose}
modalFooter={footer}
padded={false}
>
<div className="module-error-modal__description">{bodyText}</div>
</Modal>
);
}
+7
View File
@@ -27,6 +27,13 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.AlreadyGroupMember }; return { toastType: ToastType.AlreadyGroupMember };
case ToastType.AlreadyRequestedToJoin: case ToastType.AlreadyRequestedToJoin:
return { toastType: ToastType.AlreadyRequestedToJoin }; return { toastType: ToastType.AlreadyRequestedToJoin };
case ToastType.AttachmentDownloadFailed:
return {
toastType: ToastType.AttachmentDownloadFailed,
parameters: {
messageId: 'fake-message-id',
},
};
case ToastType.AttachmentDownloadStillInProgress: case ToastType.AttachmentDownloadStillInProgress:
return { return {
toastType: ToastType.AttachmentDownloadStillInProgress, toastType: ToastType.AttachmentDownloadStillInProgress,
+6
View File
@@ -93,6 +93,12 @@ export function renderToast({
); );
} }
if (toastType === ToastType.AttachmentDownloadFailed) {
return (
<Toast onClose={hideToast}>{i18n('icu:Toast--download-failed')}</Toast>
);
}
if (toastType === ToastType.AttachmentDownloadStillInProgress) { if (toastType === ToastType.AttachmentDownloadStillInProgress) {
return ( return (
<Toast onClose={hideToast}> <Toast onClose={hideToast}>
@@ -11,6 +11,7 @@ import type { AttachmentForUIType } from '../../types/Attachment';
import type { LocalizerType } from '../../types/I18N'; import type { LocalizerType } from '../../types/I18N';
import { Spinner } from '../Spinner'; import { Spinner } from '../Spinner';
import { isKeyboardActivation } from '../../hooks/useKeyboardShortcuts'; import { isKeyboardActivation } from '../../hooks/useKeyboardShortcuts';
import { roundFractionForProgressBar } from '../../util/numbers';
export type PropsType = { export type PropsType = {
attachments: ReadonlyArray<AttachmentForUIType>; attachments: ReadonlyArray<AttachmentForUIType>;
@@ -130,7 +131,9 @@ export function AttachmentDetailPill({
</div> </div>
); );
} else if (totalDownloadedSize > 0) { } else if (totalDownloadedSize > 0) {
const downloadFraction = totalDownloadedSize / totalSize; const downloadFraction = roundFractionForProgressBar(
totalDownloadedSize / totalSize
);
ariaLabel = i18n('icu:cancelDownload'); ariaLabel = i18n('icu:cancelDownload');
onClick = cancelDownloadClick; onClick = cancelDownloadClick;
@@ -0,0 +1,112 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useRef, useState } from 'react';
import type { Meta } from '@storybook/react';
import type { PropsType } from './AttachmentStatusIcon';
import { AttachmentStatusIcon } from './AttachmentStatusIcon';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
export default {
title: 'Components/Conversation/AttachmentStatusIcon',
argTypes: {
isIncoming: { control: { type: 'select' }, options: [true, false] },
},
args: {
attachment: fakeAttachment(),
isIncoming: false,
renderAttachmentDownloaded: () => {
return <div>🔥🔥</div>;
},
},
} satisfies Meta<PropsType>;
export function Default(args: PropsType): JSX.Element {
return (
<div style={{ backgroundColor: 'gray' }}>
<AttachmentStatusIcon {...args} />
</div>
);
}
export function NoAttachment(args: PropsType): JSX.Element {
return (
<div style={{ backgroundColor: 'gray' }}>
<AttachmentStatusIcon {...args} attachment={undefined} />
</div>
);
}
export function NeedsDownload(args: PropsType): JSX.Element {
return (
<div style={{ backgroundColor: 'gray' }}>
<AttachmentStatusIcon
{...args}
attachment={fakeAttachment({ path: undefined })}
/>
</div>
);
}
export function Downloading(args: PropsType): JSX.Element {
return (
<div style={{ backgroundColor: 'gray' }}>
<AttachmentStatusIcon
{...args}
attachment={fakeAttachment({
path: undefined,
pending: true,
size: 1000000,
totalDownloaded: 750000,
})}
/>
</div>
);
}
export function Interactive(args: PropsType): JSX.Element {
const size = 10000000;
const [attachment, setAttachment] = useState(
fakeAttachment({ path: undefined, size })
);
const intervalRef = useRef<NodeJS.Timeout | undefined>();
const cancelAttachmentDownload = useCallback(() => {
const newAttachment = { ...attachment, pending: false };
setAttachment(newAttachment);
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = undefined;
}
}, [attachment, setAttachment]);
const kickOffAttachmentDownload = useCallback(() => {
let { totalDownloaded } = attachment;
totalDownloaded = (totalDownloaded ?? 0) + size / 20;
const newAttachment = { ...attachment, totalDownloaded, pending: true };
setAttachment(newAttachment);
intervalRef.current = setInterval(() => {
totalDownloaded = (totalDownloaded ?? 0) + size / 20;
setAttachment({ ...newAttachment, totalDownloaded });
if (totalDownloaded >= size && intervalRef.current) {
setAttachment({ ...newAttachment, pending: false, path: 'something ' });
clearInterval(intervalRef.current);
intervalRef.current = undefined;
}
}, 300);
}, [attachment, setAttachment]);
return (
<div style={{ backgroundColor: 'gray' }}>
<button type="button" onClick={kickOffAttachmentDownload}>
start download
</button>
<button type="button" onClick={cancelAttachmentDownload}>
stop download
</button>
<AttachmentStatusIcon {...args} attachment={attachment} />
</div>
);
}
@@ -0,0 +1,169 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useRef, useState } from 'react';
import classNames from 'classnames';
import { ProgressCircle } from '../ProgressCircle';
import { usePrevious } from '../../hooks/usePrevious';
import type { AttachmentForUIType } from '../../types/Attachment';
import { roundFractionForProgressBar } from '../../util/numbers';
const TRANSITION_DELAY = 200;
export type PropsType = {
attachment: AttachmentForUIType | undefined;
isAttachmentNotAvailable: boolean;
isIncoming: boolean;
renderAttachmentDownloaded: () => JSX.Element;
};
enum IconState {
NeedsDownload = 'NeedsDownload',
Downloading = 'Downloading',
Downloaded = 'Downloaded',
}
export function AttachmentStatusIcon({
attachment,
isAttachmentNotAvailable,
isIncoming,
renderAttachmentDownloaded,
}: PropsType): JSX.Element | null {
const [isWaiting, setIsWaiting] = useState<boolean>(false);
let state: IconState = IconState.Downloaded;
if (attachment && isAttachmentNotAvailable) {
state = IconState.Downloaded;
} else if (attachment && !attachment.path && !attachment.pending) {
state = IconState.NeedsDownload;
} else if (attachment && !attachment.path && attachment.pending) {
state = IconState.Downloading;
}
const timerRef = useRef<NodeJS.Timeout | undefined>();
const previousState = usePrevious(state, state);
// We need useLayoutEffect; otherwise we might get a flash of the wrong visual state.
// We do calculations here which change the UI!
React.useLayoutEffect(() => {
if (state === previousState) {
return;
}
if (
previousState === IconState.NeedsDownload &&
state === IconState.Downloading
) {
setIsWaiting(true);
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
timerRef.current = undefined;
setIsWaiting(false);
}, TRANSITION_DELAY);
} else if (
previousState === IconState.Downloading &&
state === IconState.Downloaded
) {
setIsWaiting(true);
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
timerRef.current = undefined;
setIsWaiting(false);
}, TRANSITION_DELAY);
}
}, [previousState, state]);
if (attachment && state === IconState.NeedsDownload) {
return (
<div className="AttachmentStatusIcon__container">
<div
className={classNames(
'AttachmentStatusIcon__circle-icon-container',
isIncoming
? 'AttachmentStatusIcon__circle-icon-container--incoming'
: undefined
)}
>
<div
className={classNames(
'AttachmentStatusIcon__circle-icon',
isIncoming
? 'AttachmentStatusIcon__circle-icon--incoming'
: undefined,
'AttachmentStatusIcon__circle-icon--arrow-down'
)}
/>
</div>
</div>
);
}
if (
attachment &&
(state === IconState.Downloading ||
(state === IconState.Downloaded && isWaiting))
) {
const { size, totalDownloaded } = attachment;
let downloadFraction =
size && totalDownloaded
? roundFractionForProgressBar(totalDownloaded / size)
: undefined;
if (state === IconState.Downloading && isWaiting) {
downloadFraction = undefined;
}
if (state === IconState.Downloaded && isWaiting) {
downloadFraction = 1;
}
return (
<div className="AttachmentStatusIcon__container">
<div
className={classNames(
'AttachmentStatusIcon__circle-icon-container',
isIncoming
? 'AttachmentStatusIcon__circle-icon-container--incoming'
: undefined
)}
>
{downloadFraction ? (
<div
className={classNames(
'AttachmentStatusIcon__progress-container',
isIncoming
? 'AttachmentStatusIcon__progress-container--incoming'
: undefined
)}
>
<ProgressCircle
fractionComplete={downloadFraction}
width={36}
strokeWidth={2}
/>
</div>
) : undefined}
<div
className={classNames(
'AttachmentStatusIcon__circle-icon',
isIncoming
? 'AttachmentStatusIcon__circle-icon--incoming'
: undefined,
'AttachmentStatusIcon__circle-icon--x'
)}
/>
</div>
</div>
);
}
return (
<div className="AttachmentStatusIcon__container">
{renderAttachmentDownloaded()}
</div>
);
}
@@ -19,9 +19,12 @@ export default {
} satisfies Meta<Props>; } satisfies Meta<Props>;
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
cancelAttachmentDownload: action('cancelAttachmentDownload'),
contact: overrideProps.contact || {}, contact: overrideProps.contact || {},
hasSignalAccount: overrideProps.hasSignalAccount || false, hasSignalAccount: overrideProps.hasSignalAccount || false,
i18n, i18n,
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
messageId: 'fake-message-id',
onSendMessage: action('onSendMessage'), onSendMessage: action('onSendMessage'),
}); });
@@ -135,6 +138,97 @@ export function FullyFilledOut(): JSX.Element {
return <ContactDetail {...props} />; return <ContactDetail {...props} />;
} }
export function FullyFilledOutNotDownloaded(): JSX.Element {
const props = createProps({
contact: fullContact,
hasSignalAccount: true,
});
const propsWithUpdatedAvatar = {
...props,
contact: {
...props.contact,
avatar: {
avatar: fakeAttachment({
path: undefined,
contentType: IMAGE_GIF,
}),
isProfile: true,
},
},
};
return <ContactDetail {...propsWithUpdatedAvatar} />;
}
export function FullyFilledOutDownloading(): JSX.Element {
const props = createProps({
contact: fullContact,
hasSignalAccount: true,
});
const propsWithUpdatedAvatar = {
...props,
contact: {
...props.contact,
avatar: {
avatar: fakeAttachment({
path: undefined,
contentType: IMAGE_GIF,
pending: true,
size: 10000000,
totalDownloaded: 500000,
}),
isProfile: true,
},
},
};
return <ContactDetail {...propsWithUpdatedAvatar} />;
}
export function FullyFilledOutTransientError(): JSX.Element {
const props = createProps({
contact: fullContact,
hasSignalAccount: true,
});
const propsWithUpdatedAvatar = {
...props,
contact: {
...props.contact,
avatar: {
avatar: fakeAttachment({
error: true,
iv: 'something',
key: 'something',
digest: 'something',
cdnKey: 'something',
cdnNumber: 2,
path: undefined,
contentType: IMAGE_GIF,
}),
isProfile: true,
},
},
};
return <ContactDetail {...propsWithUpdatedAvatar} />;
}
export function FullyFilledOutPermanentError(): JSX.Element {
const props = createProps({
contact: fullContact,
hasSignalAccount: true,
});
const propsWithUpdatedAvatar = {
...props,
contact: {
...props.contact,
avatar: {
avatar: fakeAttachment({
error: true,
path: undefined,
contentType: IMAGE_GIF,
}),
isProfile: true,
},
},
};
return <ContactDetail {...propsWithUpdatedAvatar} />;
}
export function OnlyEmail(): JSX.Element { export function OnlyEmail(): JSX.Element {
const props = createProps({ const props = createProps({
contact: { contact: {
+62 -13
View File
@@ -1,30 +1,35 @@
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { useCallback } from 'react';
import classNames from 'classnames';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import { AddressType, ContactFormType } from '../../types/EmbeddedContact';
import { missingCaseError } from '../../util/missingCaseError';
import { isPermanentlyUndownloadable } from '../../types/Attachment';
import {
renderAvatar,
renderContactShorthand,
renderName,
} from './contactUtil';
import type { import type {
EmbeddedContactType, EmbeddedContactType,
Email, Email,
Phone, Phone,
PostalAddress, PostalAddress,
} from '../../types/EmbeddedContact'; } from '../../types/EmbeddedContact';
import { AddressType, ContactFormType } from '../../types/EmbeddedContact';
import { missingCaseError } from '../../util/missingCaseError';
import {
renderAvatar,
renderContactShorthand,
renderName,
} from './contactUtil';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
export type Props = { export type Props = {
cancelAttachmentDownload: (options: { messageId: string }) => void;
contact: ReadonlyDeep<EmbeddedContactType>; contact: ReadonlyDeep<EmbeddedContactType>;
hasSignalAccount: boolean; hasSignalAccount: boolean;
i18n: LocalizerType; i18n: LocalizerType;
kickOffAttachmentDownload: (options: { messageId: string }) => void;
messageId: string;
onSendMessage: () => void; onSendMessage: () => void;
}; };
@@ -75,9 +80,12 @@ function getLabelForAddress(
} }
export function ContactDetail({ export function ContactDetail({
cancelAttachmentDownload,
contact, contact,
hasSignalAccount, hasSignalAccount,
i18n, i18n,
kickOffAttachmentDownload,
messageId,
onSendMessage, onSendMessage,
}: Props): JSX.Element { }: Props): JSX.Element {
// We don't want the overall click handler for this element to fire, so we stop // We don't want the overall click handler for this element to fire, so we stop
@@ -90,11 +98,52 @@ export function ContactDetail({
const isIncoming = false; const isIncoming = false;
const module = 'contact-detail'; const module = 'contact-detail';
const maybeDownload = useCallback(() => {
const attachment = contact.avatar?.avatar;
if (!attachment) {
return;
}
if (isPermanentlyUndownloadable(attachment)) {
return;
}
if (attachment.pending) {
cancelAttachmentDownload({ messageId });
return;
}
if (!attachment.path) {
kickOffAttachmentDownload({ messageId });
}
}, [cancelAttachmentDownload, contact, kickOffAttachmentDownload, messageId]);
const attachment = contact.avatar?.avatar;
const isClickable =
attachment &&
!isPermanentlyUndownloadable(attachment) &&
(attachment.pending || !attachment.path);
return ( return (
<div className="module-contact-detail"> <div className="module-contact-detail">
<div className="module-contact-detail__avatar"> <button
{renderAvatar({ contact, i18n, size: 80 })} className={classNames(
</div> 'module-contact-detail__avatar',
isClickable ? 'module-contact-detail__avatar--clickable' : undefined
)}
type="button"
onClick={(event: React.MouseEvent) => {
event?.stopPropagation();
event.preventDefault();
maybeDownload();
}}
onKeyDown={(event: React.KeyboardEvent) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
event?.stopPropagation();
event.preventDefault();
maybeDownload();
}}
>
{renderAvatar({ contact, direction: 'incoming', i18n, size: 80 })}
</button>
{renderName({ contact, isIncoming, module })} {renderName({ contact, isIncoming, module })}
{renderContactShorthand({ contact, isIncoming, module })} {renderContactShorthand({ contact, isIncoming, module })}
+31 -27
View File
@@ -18,10 +18,10 @@ export type Props = {
contact: ReadonlyDeep<EmbeddedContactType>; contact: ReadonlyDeep<EmbeddedContactType>;
i18n: LocalizerType; i18n: LocalizerType;
isIncoming: boolean; isIncoming: boolean;
onClick?: () => void;
tabIndex: number;
withContentAbove: boolean; withContentAbove: boolean;
withContentBelow: boolean; withContentBelow: boolean;
tabIndex: number;
onClick?: () => void;
}; };
export function EmbeddedContact(props: Props): JSX.Element { export function EmbeddedContact(props: Props): JSX.Element {
@@ -38,41 +38,45 @@ export function EmbeddedContact(props: Props): JSX.Element {
const direction = isIncoming ? 'incoming' : 'outgoing'; const direction = isIncoming ? 'incoming' : 'outgoing';
return ( return (
<button <div
type="button"
className={classNames( className={classNames(
'module-embedded-contact', 'module-embedded-contact',
`module-embedded-contact--${direction}`, `module-embedded-contact--${direction}`,
withContentAbove ? 'module-embedded-contact--with-content-above' : null, withContentAbove ? 'module-embedded-contact--with-content-above' : null,
withContentBelow ? 'module-embedded-contact--with-content-below' : null withContentBelow ? 'module-embedded-contact--with-content-below' : null
)} )}
onKeyDown={(event: React.KeyboardEvent) => {
if (event.key !== 'Enter' && event.key !== 'Space') {
return;
}
if (onClick) {
event.stopPropagation();
event.preventDefault();
onClick();
}
}}
onClick={(event: React.MouseEvent) => {
if (onClick) {
event.stopPropagation();
event.preventDefault();
onClick();
}
}}
tabIndex={tabIndex}
> >
{renderAvatar({ contact, i18n, size: 52, direction })} <button
type="button"
className="module-embedded-contact__avatar-container"
onKeyDown={(event: React.KeyboardEvent) => {
if (event.key !== 'Enter' && event.key !== 'Space') {
return;
}
if (onClick) {
event.stopPropagation();
event.preventDefault();
onClick();
}
}}
onClick={(event: React.MouseEvent) => {
if (onClick) {
event.stopPropagation();
event.preventDefault();
onClick();
}
}}
tabIndex={tabIndex}
>
{renderAvatar({ contact, direction, i18n, size: 52 })}
</button>
<div className="module-embedded-contact__text-container"> <div className="module-embedded-contact__text-container">
{renderName({ contact, isIncoming, module })} {renderName({ contact, isIncoming, module })}
{renderContactShorthand({ contact, isIncoming, module })} {renderContactShorthand({ contact, isIncoming, module })}
</div> </div>
</button> </div>
); );
} }
@@ -17,7 +17,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
overrideProps.expirationTimestamp || Date.now() + 30 * 1000, overrideProps.expirationTimestamp || Date.now() + 30 * 1000,
withImageNoCaption: overrideProps.withImageNoCaption || false, withImageNoCaption: overrideProps.withImageNoCaption || false,
withSticker: overrideProps.withSticker || false, withSticker: overrideProps.withSticker || false,
withTapToViewExpired: overrideProps.withTapToViewExpired || false,
}); });
export const _30Seconds = (): JSX.Element => { export const _30Seconds = (): JSX.Element => {
@@ -60,14 +59,6 @@ export function Sticker(): JSX.Element {
return <ExpireTimer {...props} />; return <ExpireTimer {...props} />;
} }
export function TapToViewExpired(): JSX.Element {
const props = createProps({
withTapToViewExpired: true,
});
return <ExpireTimer {...props} />;
}
export function ImageNoCaption(): JSX.Element { export function ImageNoCaption(): JSX.Element {
const props = createProps({ const props = createProps({
withImageNoCaption: true, withImageNoCaption: true,
@@ -13,7 +13,6 @@ export type Props = {
isOutlineOnlyBubble?: boolean; isOutlineOnlyBubble?: boolean;
withImageNoCaption?: boolean; withImageNoCaption?: boolean;
withSticker?: boolean; withSticker?: boolean;
withTapToViewExpired?: boolean;
}; };
export function ExpireTimer({ export function ExpireTimer({
@@ -23,7 +22,6 @@ export function ExpireTimer({
isOutlineOnlyBubble, isOutlineOnlyBubble,
withImageNoCaption, withImageNoCaption,
withSticker, withSticker,
withTapToViewExpired,
}: Props): JSX.Element { }: Props): JSX.Element {
const [, forceUpdate] = useReducer(() => ({}), {}); const [, forceUpdate] = useReducer(() => ({}), {});
@@ -45,9 +43,6 @@ export function ExpireTimer({
`module-expire-timer--${bucket}`, `module-expire-timer--${bucket}`,
direction ? `module-expire-timer--${direction}` : null, direction ? `module-expire-timer--${direction}` : null,
isOutlineOnlyBubble ? 'module-expire-timer--outline-only-bubble' : null, isOutlineOnlyBubble ? 'module-expire-timer--outline-only-bubble' : null,
withTapToViewExpired
? `module-expire-timer--${direction}-with-tap-to-view-expired`
: null,
direction && withImageNoCaption direction && withImageNoCaption
? 'module-expire-timer--with-image-no-caption' ? 'module-expire-timer--with-image-no-caption'
: null, : null,
+4 -1
View File
@@ -20,6 +20,7 @@ import {
} from '../../types/Attachment'; } from '../../types/Attachment';
import { ProgressCircle } from '../ProgressCircle'; import { ProgressCircle } from '../ProgressCircle';
import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler'; import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler';
import { roundFractionForProgressBar } from '../../util/numbers';
export enum CurveType { export enum CurveType {
None = 0, None = 0,
@@ -345,7 +346,9 @@ export function getSpinner({
!isIncremental(attachment) && !isIncremental(attachment) &&
attachment.size && attachment.size &&
attachment.totalDownloaded attachment.totalDownloaded
? attachment.totalDownloaded / attachment.size ? roundFractionForProgressBar(
attachment.totalDownloaded / attachment.size
)
: undefined; : undefined;
if (downloadFraction) { if (downloadFraction) {
File diff suppressed because it is too large Load Diff
+30 -5
View File
@@ -23,6 +23,8 @@ import { WaveformScrubber } from './WaveformScrubber';
import { useComputePeaks } from '../../hooks/useComputePeaks'; import { useComputePeaks } from '../../hooks/useComputePeaks';
import { durationToPlaybackText } from '../../util/durationToPlaybackText'; import { durationToPlaybackText } from '../../util/durationToPlaybackText';
import { shouldNeverBeCalled } from '../../util/shouldNeverBeCalled'; import { shouldNeverBeCalled } from '../../util/shouldNeverBeCalled';
import { formatFileSize } from '../../util/formatFileSize';
import { roundFractionForProgressBar } from '../../util/numbers';
export type OwnProps = Readonly<{ export type OwnProps = Readonly<{
active: active:
@@ -47,6 +49,7 @@ export type OwnProps = Readonly<{
status?: MessageStatusType; status?: MessageStatusType;
textPending?: boolean; textPending?: boolean;
timestamp: number; timestamp: number;
cancelAttachmentDownload(): void;
kickOffAttachmentDownload(): void; kickOffAttachmentDownload(): void;
onCorrupted(): void; onCorrupted(): void;
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>; computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
@@ -154,6 +157,7 @@ export function MessageAudio(props: Props): JSX.Element {
textPending, textPending,
timestamp, timestamp,
cancelAttachmentDownload,
kickOffAttachmentDownload, kickOffAttachmentDownload,
onCorrupted, onCorrupted,
setPlaybackRate, setPlaybackRate,
@@ -275,23 +279,41 @@ export function MessageAudio(props: Props): JSX.Element {
); );
let button: React.ReactElement; let button: React.ReactElement;
if (state === State.Pending || state === State.Computing) { if (state === State.Computing) {
// Not really a button, but who cares? // Not really a button, but who cares?
button = ( button = (
<PlaybackButton <PlaybackButton
variant="message" variant="message"
mod="pending" mod="computing"
onClick={noop} onClick={noop}
label={i18n('icu:MessageAudio--pending')} label={i18n('icu:MessageAudio--pending')}
context={direction} context={direction}
/> />
); );
} else if (state === State.Pending) {
// Not really a button, but who cares?
const downloadFraction =
attachment.size && attachment.totalDownloaded
? roundFractionForProgressBar(
attachment.totalDownloaded / attachment.size
)
: undefined;
button = (
<PlaybackButton
variant="message"
mod="downloading"
downloadFraction={downloadFraction}
onClick={cancelAttachmentDownload}
label={i18n('icu:MessageAudio--pending')}
context={direction}
/>
);
} else if (state === State.NotDownloaded) { } else if (state === State.NotDownloaded) {
button = ( button = (
<PlaybackButton <PlaybackButton
ref={buttonRef} ref={buttonRef}
variant="message" variant="message"
mod="download" mod="not-downloaded"
label={i18n('icu:MessageAudio--download')} label={i18n('icu:MessageAudio--download')}
onClick={kickOffAttachmentDownload} onClick={kickOffAttachmentDownload}
context={direction} context={direction}
@@ -316,6 +338,10 @@ export function MessageAudio(props: Props): JSX.Element {
} }
const countDown = Math.max(0, duration - (active?.currentTime ?? 0)); const countDown = Math.max(0, duration - (active?.currentTime ?? 0));
const fileSizeOrDuration =
state === State.NotDownloaded || state === State.Pending || duration < 1
? formatFileSize(attachment.size)
: durationToPlaybackText(countDown);
const metadata = ( const metadata = (
<div className={`${CSS_BASE}__metadata`}> <div className={`${CSS_BASE}__metadata`}>
@@ -326,7 +352,7 @@ export function MessageAudio(props: Props): JSX.Element {
`${CSS_BASE}__countdown--${played ? 'played' : 'unplayed'}` `${CSS_BASE}__countdown--${played ? 'played' : 'unplayed'}`
)} )}
> >
{durationToPlaybackText(countDown)} {fileSizeOrDuration}
</div> </div>
<div className={`${CSS_BASE}__controls`}> <div className={`${CSS_BASE}__controls`}>
@@ -360,7 +386,6 @@ export function MessageAudio(props: Props): JSX.Element {
id={id} id={id}
isShowingImage={false} isShowingImage={false}
isSticker={false} isSticker={false}
isTapToViewExpired={false}
pushPanelForConversation={pushPanelForConversation} pushPanelForConversation={pushPanelForConversation}
retryMessageSend={shouldNeverBeCalled} retryMessageSend={shouldNeverBeCalled}
status={status} status={status}
@@ -108,6 +108,7 @@ export type PropsReduxActions = Pick<
| 'showLightboxForViewOnceMedia' | 'showLightboxForViewOnceMedia'
| 'showMediaNoLongerAvailableToast' | 'showMediaNoLongerAvailableToast'
| 'showSpoiler' | 'showSpoiler'
| 'showTapToViewNotAvailableModal'
| 'startConversation' | 'startConversation'
| 'viewStory' | 'viewStory'
> & { > & {
@@ -157,6 +158,7 @@ export function MessageDetail({
showLightboxForViewOnceMedia, showLightboxForViewOnceMedia,
showMediaNoLongerAvailableToast, showMediaNoLongerAvailableToast,
showSpoiler, showSpoiler,
showTapToViewNotAvailableModal,
startConversation, startConversation,
theme, theme,
toggleSafetyNumberModal, toggleSafetyNumberModal,
@@ -373,6 +375,7 @@ export function MessageDetail({
showAttachmentDownloadStillInProgressToast showAttachmentDownloadStillInProgressToast
} }
showAttachmentNotAvailableModal={showAttachmentNotAvailableModal} showAttachmentNotAvailableModal={showAttachmentNotAvailableModal}
showTapToViewNotAvailableModal={showTapToViewNotAvailableModal}
showExpiredIncomingTapToViewToast={ showExpiredIncomingTapToViewToast={
showExpiredIncomingTapToViewToast showExpiredIncomingTapToViewToast
} }
@@ -32,7 +32,6 @@ type PropsType = {
isOutlineOnlyBubble?: boolean; isOutlineOnlyBubble?: boolean;
isShowingImage: boolean; isShowingImage: boolean;
isSticker?: boolean; isSticker?: boolean;
isTapToViewExpired?: boolean;
onWidthMeasured?: (width: number) => unknown; onWidthMeasured?: (width: number) => unknown;
pushPanelForConversation: PushPanelForConversationActionType; pushPanelForConversation: PushPanelForConversationActionType;
retryMessageSend: (messageId: string) => unknown; retryMessageSend: (messageId: string) => unknown;
@@ -62,7 +61,6 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
isInline, isInline,
isShowingImage, isShowingImage,
isSticker, isSticker,
isTapToViewExpired,
onWidthMeasured, onWidthMeasured,
pushPanelForConversation, pushPanelForConversation,
retryMessageSend, retryMessageSend,
@@ -160,7 +158,6 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
timestamp={timestamp} timestamp={timestamp}
withImageNoCaption={withImageNoCaption} withImageNoCaption={withImageNoCaption}
withSticker={isSticker} withSticker={isSticker}
withTapToViewExpired={isTapToViewExpired}
/> />
); );
} }
@@ -226,7 +223,6 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
expirationTimestamp={expirationTimestamp} expirationTimestamp={expirationTimestamp}
withImageNoCaption={withImageNoCaption} withImageNoCaption={withImageNoCaption}
withSticker={isSticker} withSticker={isSticker}
withTapToViewExpired={isTapToViewExpired}
/> />
) : null} ) : null}
{textPending ? ( {textPending ? (
@@ -251,9 +247,6 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
: null, : null,
isOutlineOnlyBubble isOutlineOnlyBubble
? 'module-message__metadata__status-icon--outline-only-bubble' ? 'module-message__metadata__status-icon--outline-only-bubble'
: null,
isTapToViewExpired
? 'module-message__metadata__status-icon--with-tap-to-view-expired'
: null : null
)} )}
/> />
@@ -32,7 +32,6 @@ export function MessageTimestamp({
timestamp, timestamp,
withImageNoCaption, withImageNoCaption,
withSticker, withSticker,
withTapToViewExpired,
}: Readonly<Props>): ReactElement { }: Readonly<Props>): ReactElement {
const now = useNowThatUpdatesEveryMinute(); const now = useNowThatUpdatesEveryMinute();
const moduleName = module || 'module-timestamp'; const moduleName = module || 'module-timestamp';
@@ -42,9 +41,6 @@ export function MessageTimestamp({
className={classNames( className={classNames(
moduleName, moduleName,
direction ? `${moduleName}--${direction}` : null, direction ? `${moduleName}--${direction}` : null,
withTapToViewExpired && direction
? `${moduleName}--${direction}-with-tap-to-view-expired`
: null,
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null, withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
withSticker ? `${moduleName}--with-sticker` : null, withSticker ? `${moduleName}--with-sticker` : null,
isOutlineOnlyBubble ? `${moduleName}--outline-only-bubble` : null isOutlineOnlyBubble ? `${moduleName}--outline-only-bubble` : null
@@ -146,6 +146,7 @@ const defaultMessageProps: TimelineMessagesProps = {
'showExpiredOutgoingTapToViewToast' 'showExpiredOutgoingTapToViewToast'
), ),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
showTapToViewNotAvailableModal: action('showTapToViewNotAvailableModal'),
toggleDeleteMessagesModal: action('default--toggleDeleteMessagesModal'), toggleDeleteMessagesModal: action('default--toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('default--toggleForwardMessagesModal'), toggleForwardMessagesModal: action('default--toggleForwardMessagesModal'),
showLightbox: action('default--showLightbox'), showLightbox: action('default--showLightbox'),
@@ -267,6 +268,39 @@ ImageAttachment.args = {
}, },
}; };
export const ImageAttachmentUndownloaded = Template.bind({});
ImageAttachmentUndownloaded.args = {
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
size: 100000,
},
},
};
export const ImageAttachmentDownloading = Template.bind({});
ImageAttachmentDownloading.args = {
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
pending: true,
size: 100000,
totalDownloaded: 75000,
},
},
};
export const ImageAttachmentNoThumbnail = Template.bind({}); export const ImageAttachmentNoThumbnail = Template.bind({});
ImageAttachmentNoThumbnail.args = { ImageAttachmentNoThumbnail.args = {
rawAttachment: { rawAttachment: {
@@ -322,6 +356,39 @@ VideoAttachment.args = {
}, },
}; };
export const VideoAttachmentUndownloaded = Template.bind({});
VideoAttachmentUndownloaded.args = {
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
size: 100000,
},
},
};
export const VideoAttachmentDownloading = Template.bind({});
VideoAttachmentDownloading.args = {
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
pending: true,
size: 100000,
totalDownloaded: 75000,
},
},
};
export const VideoAttachmentNoThumbnail = Template.bind({}); export const VideoAttachmentNoThumbnail = Template.bind({});
VideoAttachmentNoThumbnail.args = { VideoAttachmentNoThumbnail.args = {
rawAttachment: { rawAttachment: {
@@ -318,6 +318,7 @@ const actions = () => ({
'showExpiredOutgoingTapToViewToast' 'showExpiredOutgoingTapToViewToast'
), ),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
showTapToViewNotAvailableModal: action('showTapToViewNotAvailableModal'),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'), toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'), toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
@@ -109,6 +109,7 @@ const getDefaultProps = () => ({
), ),
showAttachmentNotAvailableModal: action('showAttachmentNotAvailableModal'), showAttachmentNotAvailableModal: action('showAttachmentNotAvailableModal'),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
showTapToViewNotAvailableModal: action('showTapToViewNotAvailableModal'),
scrollToQuotedMessage: action('scrollToQuotedMessage'), scrollToQuotedMessage: action('scrollToQuotedMessage'),
showSpoiler: action('showSpoiler'), showSpoiler: action('showSpoiler'),
startConversation: action('startConversation'), startConversation: action('startConversation'),
@@ -353,6 +353,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
'showExpiredOutgoingTapToViewToast' 'showExpiredOutgoingTapToViewToast'
), ),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
showTapToViewNotAvailableModal: action('showTapToViewNotAvailableModal'),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'), toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'), toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
showLightbox: action('showLightbox'), showLightbox: action('showLightbox'),
@@ -374,7 +375,7 @@ const renderMany = (propsArray: ReadonlyArray<Props>) => (
<> <>
{propsArray.map((message, index) => ( {propsArray.map((message, index) => (
<TimelineMessage <TimelineMessage
key={message.text} key={`${message.text}_${index}_${message.direction}`}
{...message} {...message}
shouldCollapseAbove={Boolean(propsArray[index - 1])} shouldCollapseAbove={Boolean(propsArray[index - 1])}
shouldCollapseBelow={Boolean(propsArray[index + 1])} shouldCollapseBelow={Boolean(propsArray[index + 1])}
@@ -383,7 +384,12 @@ const renderMany = (propsArray: ReadonlyArray<Props>) => (
</> </>
); );
const renderThree = (props: Props) => renderMany([props, props, props]); const renderThree = (props: Props) =>
renderMany([
{ ...props, shouldHideMetadata: true },
{ ...props, shouldHideMetadata: true },
props,
]);
const renderBothDirections = (props: Props) => ( const renderBothDirections = (props: Props) => (
<> <>
@@ -1050,6 +1056,110 @@ LinkPreviewWithSmallImage.args = {
text: 'Be sure to look at https://www.signal.org', text: 'Be sure to look at https://www.signal.org',
}; };
export const LinkPreviewWithUndownloadedImage = Template.bind({});
LinkPreviewWithUndownloadedImage.args = {
previews: [
{
domain: 'signal.org',
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
path: undefined,
size: 5300000,
}),
isStickerPack: false,
isCallLink: false,
title: 'Signal',
description:
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
url: 'https://www.signal.org',
date: new Date(2020, 2, 10).valueOf(),
},
],
status: 'sent',
text: 'Be sure to look at https://www.signal.org',
};
export const LinkPreviewWithDownloadingImage = Template.bind({});
LinkPreviewWithDownloadingImage.args = {
previews: [
{
domain: 'signal.org',
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
path: undefined,
pending: true,
size: 5300000,
totalDownloaded: 1230000,
}),
isStickerPack: false,
isCallLink: false,
title: 'Signal',
description:
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
url: 'https://www.signal.org',
date: new Date(2020, 2, 10).valueOf(),
},
],
status: 'sent',
text: 'Be sure to look at https://www.signal.org',
};
export const LinkPreviewWithUndownloadedSmallImage = Template.bind({});
LinkPreviewWithUndownloadedSmallImage.args = {
previews: [
{
domain: 'signal.org',
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 50,
width: 50,
path: undefined,
size: 5300000,
}),
isStickerPack: false,
isCallLink: false,
title: 'Signal',
description:
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
url: 'https://www.signal.org',
date: new Date(2020, 2, 10).valueOf(),
},
],
status: 'sent',
text: 'Be sure to look at https://www.signal.org',
};
export const LinkPreviewWithDownloadingSmallImage = Template.bind({});
LinkPreviewWithDownloadingSmallImage.args = {
previews: [
{
domain: 'signal.org',
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 50,
width: 50,
path: undefined,
pending: true,
size: 5300000,
totalDownloaded: 1230000,
}),
isStickerPack: false,
isCallLink: false,
title: 'Signal',
description:
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
url: 'https://www.signal.org',
date: new Date(2020, 2, 10).valueOf(),
},
],
status: 'sent',
text: 'Be sure to look at https://www.signal.org',
};
export const LinkPreviewWithoutImage = Template.bind({}); export const LinkPreviewWithoutImage = Template.bind({});
LinkPreviewWithoutImage.args = { LinkPreviewWithoutImage.args = {
previews: [ previews: [
@@ -1576,51 +1686,32 @@ PartialDownloadNotPendingGif.args = {
status: 'sent', status: 'sent',
}; };
export const _Audio = (): JSX.Element => { export const _Audio = Template.bind({});
function Wrapper() { _Audio.args = {
const [isPlayed, setIsPlayed] = React.useState(false); attachments: [
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: messageIdToAudioUrl['incompetech-com-Agnus-Dei-X'],
path: 'somepath',
}),
],
status: 'read',
readStatus: ReadStatus.Read,
};
const messageProps = createProps({ export const AudioViewed = Template.bind({});
id: 'incompetech-com-Agnus-Dei-X', AudioViewed.args = {
attachments: [ attachments: [
fakeAttachment({ fakeAttachment({
contentType: AUDIO_MP3, contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3', fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: messageIdToAudioUrl['incompetech-com-Agnus-Dei-X'], url: messageIdToAudioUrl['incompetech-com-Agnus-Dei-X'],
path: 'somepath', path: 'somepath',
}), }),
], ],
...(isPlayed status: 'viewed',
? { readStatus: ReadStatus.Viewed,
status: 'viewed',
readStatus: ReadStatus.Viewed,
}
: {
status: 'read',
readStatus: ReadStatus.Read,
}),
});
return (
<>
<button
type="button"
onClick={() => {
setIsPlayed(old => !old);
}}
style={{
display: 'block',
marginBottom: '2em',
}}
>
Toggle played
</button>
{renderBothDirections(messageProps)}
</>
);
}
return <Wrapper />;
}; };
export const LongAudio = Template.bind({}); export const LongAudio = Template.bind({});
@@ -1656,6 +1747,7 @@ AudioWithNotDownloadedAttachment.args = {
fakeAttachment({ fakeAttachment({
contentType: AUDIO_MP3, contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3', fileName: 'incompetech-com-Agnus-Dei-X.mp3',
path: undefined,
}), }),
], ],
status: 'sent', status: 'sent',
@@ -1668,6 +1760,8 @@ AudioWithPendingAttachment.args = {
contentType: AUDIO_MP3, contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3', fileName: 'incompetech-com-Agnus-Dei-X.mp3',
pending: true, pending: true,
size: 1000000,
totalDownloaded: 570000,
}), }),
], ],
status: 'sent', status: 'sent',
@@ -1678,8 +1772,65 @@ OtherFileType.args = {
attachments: [ attachments: [
fakeAttachment({ fakeAttachment({
contentType: stringToMIMEType('text/plain'), contentType: stringToMIMEType('text/plain'),
fileName: 'my-resume.txt', fileName: 'things.zip',
url: 'my-resume.txt', url: 'things.zip',
size: 10200000,
}),
],
status: 'sent',
};
export const OtherFileTypeFourChar = Template.bind({});
OtherFileTypeFourChar.args = {
attachments: [
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'things.four',
url: 'things.four',
size: 10200000,
}),
],
status: 'sent',
};
export const OtherFileTypeFiveChar = Template.bind({});
OtherFileTypeFiveChar.args = {
attachments: [
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'things.cinco',
url: 'things.cinco',
size: 10200000,
}),
],
status: 'sent',
};
export const OtherFileTypeUndownloaded = Template.bind({});
OtherFileTypeUndownloaded.args = {
attachments: [
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'things.zip',
url: 'things.zip',
size: 10200000,
path: undefined,
}),
],
status: 'sent',
};
export const OtherFileTypeDownloading = Template.bind({});
OtherFileTypeDownloading.args = {
attachments: [
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'things.zip',
url: 'things.zip',
size: 10200000,
path: undefined,
pending: true,
totalDownloaded: 7500000,
}), }),
], ],
status: 'sent', status: 'sent',
@@ -1709,9 +1860,36 @@ OtherFileTypeWithLongFilename.args = {
}), }),
], ],
status: 'sent', status: 'sent',
};
export const OtherFileTypeWithLongFilenameAndCaption = Template.bind({});
OtherFileTypeWithLongFilenameAndCaption.args = {
attachments: [
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName:
'INSERT-APP-NAME_INSERT-APP-APPLE-ID_AppStore_AppsGamesWatch.psd.zip',
url: 'a2/a2334324darewer4234',
}),
],
status: 'sent',
text: 'This is what I have done.', text: 'This is what I have done.',
}; };
export const DangerousFileType = Template.bind({});
DangerousFileType.args = {
attachments: [
fakeAttachment({
contentType: stringToMIMEType(
'application/vnd.microsoft.portable-executable'
),
fileName: 'terrible.exe',
url: 'terrible.exe',
}),
],
status: 'sent',
};
export const TapToViewImage = Template.bind({}); export const TapToViewImage = Template.bind({});
TapToViewImage.args = { TapToViewImage.args = {
attachments: [ attachments: [
@@ -1727,6 +1905,22 @@ TapToViewImage.args = {
status: 'sent', status: 'sent',
}; };
export const TapToViewImageInGroup = Template.bind({});
TapToViewImageInGroup.args = {
attachments: [
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
}),
],
isTapToView: true,
status: 'sent',
conversationType: 'group',
};
export const TapToViewVideo = Template.bind({}); export const TapToViewVideo = Template.bind({});
TapToViewVideo.args = { TapToViewVideo.args = {
attachments: [ attachments: [
@@ -1758,17 +1952,50 @@ TapToViewGif.args = {
status: 'sent', status: 'sent',
}; };
export const TapToViewExpired = Template.bind({}); export const TapToViewImageUndownloaded = Template.bind({});
TapToViewExpired.args = { TapToViewImageUndownloaded.args = {
attachments: [ attachments: [
fakeAttachment({ fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg', fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG, contentType: IMAGE_JPEG,
width: 128, width: 128,
height: 128, height: 128,
path: undefined,
size: 1800000,
}), }),
], ],
isTapToView: true,
status: 'sent',
};
export const TapToViewImageDownloading = Template.bind({});
TapToViewImageDownloading.args = {
attachments: [
fakeAttachment({
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
path: undefined,
pending: true,
size: 1800000,
totalDownloaded: 500000,
}),
],
isTapToView: true,
status: 'sent',
};
export const TapToViewViewed = Template.bind({});
TapToViewViewed.args = {
readStatus: ReadStatus.Viewed,
isTapToView: true,
isTapToViewExpired: true,
status: 'sent',
};
export const TapToViewExpired = Template.bind({});
TapToViewExpired.args = {
isTapToView: true, isTapToView: true,
isTapToViewExpired: true, isTapToViewExpired: true,
status: 'sent', status: 'sent',
@@ -1776,34 +2003,11 @@ TapToViewExpired.args = {
export const TapToViewError = Template.bind({}); export const TapToViewError = Template.bind({});
TapToViewError.args = { TapToViewError.args = {
attachments: [
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
}),
],
isTapToView: true, isTapToView: true,
isTapToViewError: true, isTapToViewError: true,
status: 'sent', status: 'sent',
}; };
export const DangerousFileType = Template.bind({});
DangerousFileType.args = {
attachments: [
fakeAttachment({
contentType: stringToMIMEType(
'application/vnd.microsoft.portable-executable'
),
fileName: 'terrible.exe',
url: 'terrible.exe',
}),
],
status: 'sent',
};
export function Colors(): JSX.Element { export function Colors(): JSX.Element {
return ( return (
<> <>
@@ -2058,6 +2262,75 @@ EmbeddedContactFullContact.args = {
contact: fullContact, contact: fullContact,
}; };
export const EmbeddedContactAvatarUndownloaded = Template.bind({});
EmbeddedContactAvatarUndownloaded.args = {
contact: {
...fullContact,
avatar: {
avatar: fakeAttachment({
path: undefined,
contentType: IMAGE_GIF,
}),
isProfile: true,
},
},
};
export const EmbeddedContactAvatarDownloading = Template.bind({});
EmbeddedContactAvatarDownloading.args = {
contact: {
...fullContact,
avatar: {
avatar: fakeAttachment({
path: undefined,
pending: true,
contentType: IMAGE_GIF,
size: 1000000,
totalDownloaded: 500000,
}),
isProfile: true,
},
},
};
export const EmbeddedContactAvatarTransientError = Template.bind({});
EmbeddedContactAvatarTransientError.args = {
contact: {
...fullContact,
avatar: {
avatar: fakeAttachment({
iv: 'something',
key: 'something',
digest: 'something',
cdnKey: 'something',
cdnNumber: 2,
path: undefined,
error: true,
contentType: IMAGE_GIF,
size: 1000000,
totalDownloaded: 500000,
}),
isProfile: true,
},
},
};
export const EmbeddedContactAvatarPermanentError = Template.bind({});
EmbeddedContactAvatarPermanentError.args = {
contact: {
...fullContact,
avatar: {
avatar: fakeAttachment({
id: undefined,
key: undefined,
error: true,
path: undefined,
contentType: IMAGE_GIF,
size: 1000000,
totalDownloaded: 500000,
}),
isProfile: true,
},
},
};
export const EmbeddedContactWithSendMessage = Template.bind({}); export const EmbeddedContactWithSendMessage = Template.bind({});
EmbeddedContactWithSendMessage.args = { EmbeddedContactWithSendMessage.args = {
contact: { contact: {
@@ -2110,22 +2383,6 @@ EmbeddedContactFamilyName.args = {
}, },
}; };
export const EmbeddedContactLoadingAvatar = Template.bind({});
EmbeddedContactLoadingAvatar.args = {
contact: {
name: {
nickname: 'Jerry',
},
avatar: {
avatar: fakeAttachment({
pending: true,
contentType: IMAGE_GIF,
}),
isProfile: true,
},
},
};
export const GiftBadgeUnopened = Template.bind({}); export const GiftBadgeUnopened = Template.bind({});
GiftBadgeUnopened.args = { GiftBadgeUnopened.args = {
giftBadge: { giftBadge: {
+20 -22
View File
@@ -6,45 +6,34 @@ import classNames from 'classnames';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import { Avatar, AvatarBlur } from '../Avatar'; import { Avatar, AvatarBlur } from '../Avatar';
import { Spinner } from '../Spinner'; import { AvatarColors } from '../../types/Colors';
import { getName } from '../../types/EmbeddedContact';
import { AttachmentStatusIcon } from './AttachmentStatusIcon';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { AvatarColors } from '../../types/Colors';
import type { EmbeddedContactType } from '../../types/EmbeddedContact'; import type { EmbeddedContactType } from '../../types/EmbeddedContact';
import { getName } from '../../types/EmbeddedContact'; import { isPermanentlyUndownloadable } from '../../types/Attachment';
export function renderAvatar({ export function renderAvatar({
contact, contact,
direction,
i18n, i18n,
size, size,
direction,
}: { }: {
contact: ReadonlyDeep<EmbeddedContactType>; contact: ReadonlyDeep<EmbeddedContactType>;
i18n: LocalizerType;
size: 28 | 52 | 80;
direction?: 'outgoing' | 'incoming'; direction?: 'outgoing' | 'incoming';
i18n: LocalizerType;
size: 52 | 80;
}): JSX.Element { }): JSX.Element {
const { avatar } = contact; const { avatar } = contact;
const avatarUrl = avatar && avatar.avatar && avatar.avatar.path; const avatarUrl = avatar && avatar.avatar && avatar.avatar.path;
const pending = avatar && avatar.avatar && avatar.avatar.pending;
const title = getName(contact) || ''; const title = getName(contact) || '';
const spinnerSvgSize = size < 50 ? 'small' : 'normal'; const isAttachmentNotAvailable = Boolean(
const spinnerSize = size < 50 ? '24px' : undefined; avatar?.avatar && isPermanentlyUndownloadable(avatar?.avatar)
);
if (pending) { const renderAttachmentDownloaded = () => (
return (
<div className="module-embedded-contact__spinner-container">
<Spinner
svgSize={spinnerSvgSize}
size={spinnerSize}
direction={direction}
/>
</div>
);
}
return (
<Avatar <Avatar
acceptedMessageRequest={false} acceptedMessageRequest={false}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
@@ -59,6 +48,15 @@ export function renderAvatar({
size={size} size={size}
/> />
); );
return (
<AttachmentStatusIcon
attachment={avatar?.avatar}
isAttachmentNotAvailable={isAttachmentNotAvailable}
isIncoming={direction === 'incoming'}
renderAttachmentDownloaded={renderAttachmentDownloaded}
/>
);
} }
export function renderName({ export function renderName({
+39 -7
View File
@@ -53,6 +53,8 @@ import {
import { safeParsePartial } from '../util/schemas'; import { safeParsePartial } from '../util/schemas';
import { deleteDownloadsJobQueue } from './deleteDownloadsJobQueue'; import { deleteDownloadsJobQueue } from './deleteDownloadsJobQueue';
import { createBatcher } from '../util/batcher'; import { createBatcher } from '../util/batcher';
import { isOlderThan } from '../util/timestamp';
import { ToastType } from '../types/Toast';
export enum AttachmentDownloadUrgency { export enum AttachmentDownloadUrgency {
IMMEDIATE = 'immediate', IMMEDIATE = 'immediate',
@@ -62,15 +64,17 @@ export enum AttachmentDownloadUrgency {
// Type for adding a new job // Type for adding a new job
export type NewAttachmentDownloadJobType = { export type NewAttachmentDownloadJobType = {
attachment: AttachmentType; attachment: AttachmentType;
attachmentType: AttachmentDownloadJobTypeType;
isManualDownload: boolean;
messageId: string; messageId: string;
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
attachmentType: AttachmentDownloadJobTypeType;
source: AttachmentDownloadSource; source: AttachmentDownloadSource;
urgency?: AttachmentDownloadUrgency; urgency?: AttachmentDownloadUrgency;
}; };
const MAX_CONCURRENT_JOBS = 3; const MAX_CONCURRENT_JOBS = 3;
const DOWNLOAD_FAILED_TIMESTAMP_REST = 10 * durations.SECOND;
const DEFAULT_RETRY_CONFIG = { const DEFAULT_RETRY_CONFIG = {
maxAttempts: 5, maxAttempts: 5,
@@ -204,8 +208,9 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
): Promise<AttachmentType> { ): Promise<AttachmentType> {
const { const {
attachment, attachment,
messageId,
attachmentType, attachmentType,
isManualDownload,
messageId,
receivedAt, receivedAt,
sentAt, sentAt,
source, source,
@@ -219,15 +224,16 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
} }
const parseResult = safeParsePartial(coreAttachmentDownloadJobSchema, { const parseResult = safeParsePartial(coreAttachmentDownloadJobSchema, {
attachment,
attachmentType,
ciphertextSize: getAttachmentCiphertextLength(attachment.size),
contentType: attachment.contentType,
digest: attachment.digest,
isManualDownload,
messageId, messageId,
receivedAt, receivedAt,
sentAt, sentAt,
attachmentType,
digest: attachment.digest,
contentType: attachment.contentType,
size: attachment.size, size: attachment.size,
ciphertextSize: getAttachmentCiphertextLength(attachment.size),
attachment,
// If the attachment does not have a backupLocator, we don't want to store it as a // If the attachment does not have a backupLocator, we don't want to store it as a
// "backup import" attachment, since it's really just a normal attachment that we'll // "backup import" attachment, since it's really just a normal attachment that we'll
// try to download from the transit tier (or it's an invalid attachment, etc.). We // try to download from the transit tier (or it's an invalid attachment, etc.). We
@@ -637,10 +643,36 @@ export async function runDownloadAttachmentJobInner({
); );
} }
} }
if (!abortSignal.aborted && job.isManualDownload) {
showDownloadFailedToast(messageId);
}
throw error; throw error;
} }
} }
export const lastErrorsByMessageId = new Map<string, number>();
function showDownloadFailedToast(messageId: string): void {
const now = Date.now();
for (const [id, timestamp] of lastErrorsByMessageId) {
if (isOlderThan(timestamp, DOWNLOAD_FAILED_TIMESTAMP_REST)) {
lastErrorsByMessageId.delete(id);
}
}
const existing = lastErrorsByMessageId.get(messageId);
if (!existing) {
window.reduxActions.toast.showToast({
toastType: ToastType.AttachmentDownloadFailed,
parameters: { messageId },
});
lastErrorsByMessageId.set(messageId, now);
}
}
async function downloadBackupThumbnail({ async function downloadBackupThumbnail({
attachment, attachment,
abortSignal, abortSignal,
+1
View File
@@ -84,6 +84,7 @@ export class SettingsChannel extends EventEmitter {
this.#installSetting('autoConvertEmoji'); this.#installSetting('autoConvertEmoji');
this.#installSetting('autoDownloadUpdate'); this.#installSetting('autoDownloadUpdate');
this.#installSetting('autoDownloadAttachment');
this.#installSetting('autoLaunch'); this.#installSetting('autoLaunch');
this.#installSetting('alwaysRelayCalls'); this.#installSetting('alwaysRelayCalls');
-13
View File
@@ -10,13 +10,10 @@ import { GiftBadgeStates } from '../components/conversation/Message';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import { getMessageIdForLogging } from '../util/idForLogging'; import { getMessageIdForLogging } from '../util/idForLogging';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp'; import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { isDownloaded } from '../types/Attachment';
import { isIncoming } from '../state/selectors/message'; import { isIncoming } from '../state/selectors/message';
import { markViewed } from '../services/MessageUpdater'; import { markViewed } from '../services/MessageUpdater';
import { notificationService } from '../services/notifications'; import { notificationService } from '../services/notifications';
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
import { queueUpdateMessage } from '../util/messageBatcher'; import { queueUpdateMessage } from '../util/messageBatcher';
import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
import { DataReader, DataWriter } from '../sql/Client'; import { DataReader, DataWriter } from '../sql/Client';
import { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
@@ -121,16 +118,6 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
if (message.get('readStatus') !== ReadStatus.Viewed) { if (message.get('readStatus') !== ReadStatus.Viewed) {
didChangeMessage = true; didChangeMessage = true;
message.set(markViewed(message.attributes, viewSync.viewedAt)); message.set(markViewed(message.attributes, viewSync.viewedAt));
const attachments = message.get('attachments');
if (!attachments?.every(isDownloaded)) {
const didQueueDownload = await queueAttachmentDownloads(message, {
urgency: AttachmentDownloadUrgency.STANDARD,
});
if (didQueueDownload) {
didChangeMessage = true;
}
}
} }
const giftBadge = message.get('giftBadge'); const giftBadge = message.get('giftBadge');
+4 -2
View File
@@ -2307,8 +2307,10 @@ export class ConversationModel extends window.Backbone
await Promise.all( await Promise.all(
readMessages.map(async m => { readMessages.map(async m => {
const registered = window.MessageCache.register(new MessageModel(m)); const registered = window.MessageCache.register(new MessageModel(m));
const shouldSave = const shouldSave = await queueAttachmentDownloadsForMessage(
await queueAttachmentDownloadsForMessage(registered); registered,
{ isManualDownload: false }
);
if (shouldSave) { if (shouldSave) {
await window.MessageCache.saveMessage(registered.attributes); await window.MessageCache.saveMessage(registered.attributes);
} }
+5 -6
View File
@@ -72,18 +72,17 @@ export async function markViewOnceMessageViewed(
): Promise<void> { ): Promise<void> {
const { fromSync } = options || {}; const { fromSync } = options || {};
if (!isValidTapToView(message.attributes)) {
log.warn(
`markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is not a valid tap to view message!`
);
return;
}
if (message.attributes.isErased) { if (message.attributes.isErased) {
log.warn( log.warn(
`markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is already erased!` `markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is already erased!`
); );
return; return;
} }
if (!isValidTapToView(message.attributes)) {
log.warn(
`markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is not a valid tap to view message!`
);
}
if (message.get('readStatus') !== ReadStatus.Viewed) { if (message.get('readStatus') !== ReadStatus.Viewed) {
message.set(markViewed(message.attributes)); message.set(markViewed(message.attributes));
+1
View File
@@ -658,6 +658,7 @@ export class BackupImportStream extends Writable {
attachmentDownloadJobPromises.push( attachmentDownloadJobPromises.push(
queueAttachmentDownloads(model, { queueAttachmentDownloads(model, {
source: AttachmentDownloadSource.BACKUP_IMPORT, source: AttachmentDownloadSource.BACKUP_IMPORT,
isManualDownload: false,
}) })
); );
} }
+4 -4
View File
@@ -2308,10 +2308,10 @@ function kickOffAttachmentDownload(
`kickOffAttachmentDownload: Message ${options.messageId} missing!` `kickOffAttachmentDownload: Message ${options.messageId} missing!`
); );
} }
const didUpdateValues = await queueAttachmentDownloadsForMessage( const didUpdateValues = await queueAttachmentDownloadsForMessage(message, {
message, urgency: AttachmentDownloadUrgency.IMMEDIATE,
AttachmentDownloadUrgency.IMMEDIATE isManualDownload: true,
); });
if (didUpdateValues) { if (didUpdateValues) {
drop(window.MessageCache.saveMessage(message.attributes)); drop(window.MessageCache.saveMessage(message.attributes));
+54 -5
View File
@@ -57,6 +57,7 @@ import { linkCallRoute } from '../../util/signalRoutes';
import type { StartCallData } from '../../components/ConfirmLeaveCallModal'; import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
import { getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import type { AttachmentNotAvailableModalType } from '../../components/AttachmentNotAvailableModal'; import type { AttachmentNotAvailableModalType } from '../../components/AttachmentNotAvailableModal';
import type { DataPropsType as TapToViewNotAvailablePropsType } from '../../components/TapToViewNotAvailableModal';
// State // State
@@ -135,6 +136,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType; safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
safetyNumberModalContactId?: string; safetyNumberModalContactId?: string;
stickerPackPreviewId?: string; stickerPackPreviewId?: string;
tapToViewNotAvailableModalProps?: TapToViewNotAvailablePropsType;
userNotFoundModalState?: UserNotFoundModalStateType; userNotFoundModalState?: UserNotFoundModalStateType;
}>; }>;
@@ -144,6 +146,10 @@ const SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL =
'globalModals/SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL'; 'globalModals/SHOW_ATTACHMENT_NOT_AVAILABLE_MODAL';
const HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL = const HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL =
'globalModals/HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL'; 'globalModals/HIDE_ATTACHMENT_NOT_AVAILABLE_MODAL';
const SHOW_TAP_TO_VIEW_NOT_AVAILABLE_MODAL =
'globalModals/SHOW_TAP_TO_VIEW_NOT_AVAILABLE_MODAL';
const HIDE_TAP_TO_VIEW_NOT_AVAILABLE_MODAL =
'globalModals/HIDE_TAP_TO_VIEW_NOT_AVAILABLE_MODAL';
const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL'; const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL';
const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL'; const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL';
const HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL'; const HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL';
@@ -223,6 +229,15 @@ type ShowAttachmentNotAvailableModalActionType = ReadonlyDeep<{
payload: AttachmentNotAvailableModalType; payload: AttachmentNotAvailableModalType;
}>; }>;
type HideTapToViewNotAvailableModalActionType = ReadonlyDeep<{
type: typeof HIDE_TAP_TO_VIEW_NOT_AVAILABLE_MODAL;
}>;
type ShowTapToViewNotAvailableModalActionType = ReadonlyDeep<{
type: typeof SHOW_TAP_TO_VIEW_NOT_AVAILABLE_MODAL;
payload: TapToViewNotAvailablePropsType;
}>;
type HideContactModalActionType = ReadonlyDeep<{ type HideContactModalActionType = ReadonlyDeep<{
type: typeof HIDE_CONTACT_MODAL; type: typeof HIDE_CONTACT_MODAL;
}>; }>;
@@ -426,6 +441,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| HideContactModalActionType | HideContactModalActionType
| HideSendAnywayDialogActiontype | HideSendAnywayDialogActiontype
| HideStoriesSettingsActionType | HideStoriesSettingsActionType
| HideTapToViewNotAvailableModalActionType
| HideUserNotFoundModalActionType | HideUserNotFoundModalActionType
| HideWhatsNewModalActionType | HideWhatsNewModalActionType
| MessageChangedActionType | MessageChangedActionType
@@ -436,12 +452,11 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ShowEditHistoryModalActionType | ShowEditHistoryModalActionType
| ShowErrorModalActionType | ShowErrorModalActionType
| ShowMediaPermissionsModalActionType | ShowMediaPermissionsModalActionType
| ToggleEditNicknameAndNoteModalActionType
| ToggleMessageRequestActionsConfirmationActionType
| ShowSendAnywayDialogActionType | ShowSendAnywayDialogActionType
| ShowShortcutGuideModalActionType | ShowShortcutGuideModalActionType
| ShowStickerPackPreviewActionType | ShowStickerPackPreviewActionType
| ShowStoriesSettingsActionType | ShowStoriesSettingsActionType
| ShowTapToViewNotAvailableModalActionType
| ShowUserNotFoundModalActionType | ShowUserNotFoundModalActionType
| ShowWhatsNewModalActionType | ShowWhatsNewModalActionType
| StartMigrationToGV2ActionType | StartMigrationToGV2ActionType
@@ -453,7 +468,9 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ToggleConfirmationModalActionType | ToggleConfirmationModalActionType
| ToggleConfirmLeaveCallModalActionType | ToggleConfirmLeaveCallModalActionType
| ToggleDeleteMessagesModalActionType | ToggleDeleteMessagesModalActionType
| ToggleEditNicknameAndNoteModalActionType
| ToggleForwardMessagesModalActionType | ToggleForwardMessagesModalActionType
| ToggleMessageRequestActionsConfirmationActionType
| ToggleNotePreviewModalActionType | ToggleNotePreviewModalActionType
| ToggleProfileEditorActionType | ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType | ToggleProfileEditorErrorActionType
@@ -471,10 +488,12 @@ export const actions = {
closeShortcutGuideModal, closeShortcutGuideModal,
closeStickerPackPreview, closeStickerPackPreview,
closeMediaPermissionsModal, closeMediaPermissionsModal,
ensureSystemMediaPermissions,
hideAttachmentNotAvailableModal, hideAttachmentNotAvailableModal,
hideBlockingSafetyNumberChangeDialog, hideBlockingSafetyNumberChangeDialog,
hideContactModal, hideContactModal,
hideStoriesSettings, hideStoriesSettings,
hideTapToViewNotAvailableModal,
hideUserNotFoundModal, hideUserNotFoundModal,
hideWhatsNewModal, hideWhatsNewModal,
showAttachmentNotAvailableModal, showAttachmentNotAvailableModal,
@@ -482,14 +501,12 @@ export const actions = {
showContactModal, showContactModal,
showEditHistoryModal, showEditHistoryModal,
showErrorModal, showErrorModal,
ensureSystemMediaPermissions,
toggleEditNicknameAndNoteModal,
toggleMessageRequestActionsConfirmation,
showGV2MigrationDialog, showGV2MigrationDialog,
showShareCallLinkViaSignal, showShareCallLinkViaSignal,
showShortcutGuideModal, showShortcutGuideModal,
showStickerPackPreview, showStickerPackPreview,
showStoriesSettings, showStoriesSettings,
showTapToViewNotAvailableModal,
showUserNotFoundModal, showUserNotFoundModal,
showWhatsNewModal, showWhatsNewModal,
toggleAboutContactModal, toggleAboutContactModal,
@@ -500,7 +517,9 @@ export const actions = {
toggleConfirmationModal, toggleConfirmationModal,
toggleConfirmLeaveCallModal, toggleConfirmLeaveCallModal,
toggleDeleteMessagesModal, toggleDeleteMessagesModal,
toggleEditNicknameAndNoteModal,
toggleForwardMessagesModal, toggleForwardMessagesModal,
toggleMessageRequestActionsConfirmation,
toggleNotePreviewModal, toggleNotePreviewModal,
toggleProfileEditor, toggleProfileEditor,
toggleProfileEditorHasError, toggleProfileEditorHasError,
@@ -528,6 +547,21 @@ function showAttachmentNotAvailableModal(
}; };
} }
function hideTapToViewNotAvailableModal(): HideTapToViewNotAvailableModalActionType {
return {
type: HIDE_TAP_TO_VIEW_NOT_AVAILABLE_MODAL,
};
}
function showTapToViewNotAvailableModal(
payload: TapToViewNotAvailablePropsType
): ShowTapToViewNotAvailableModalActionType {
return {
type: SHOW_TAP_TO_VIEW_NOT_AVAILABLE_MODAL,
payload,
};
}
function hideContactModal(): HideContactModalActionType { function hideContactModal(): HideContactModalActionType {
return { return {
type: HIDE_CONTACT_MODAL, type: HIDE_CONTACT_MODAL,
@@ -1144,6 +1178,7 @@ export function getEmptyState(): GlobalModalsStateType {
profileEditorHasError: false, profileEditorHasError: false,
profileEditorInitialEditState: undefined, profileEditorInitialEditState: undefined,
messageRequestActionsConfirmationProps: null, messageRequestActionsConfirmationProps: null,
tapToViewNotAvailableModalProps: undefined,
notePreviewModalProps: null, notePreviewModalProps: null,
}; };
} }
@@ -1232,6 +1267,20 @@ export function reducer(
}; };
} }
if (action.type === HIDE_TAP_TO_VIEW_NOT_AVAILABLE_MODAL) {
return {
...state,
tapToViewNotAvailableModalProps: undefined,
};
}
if (action.type === SHOW_TAP_TO_VIEW_NOT_AVAILABLE_MODAL) {
return {
...state,
tapToViewNotAvailableModalProps: action.payload,
};
}
if (action.type === SHOW_CONTACT_MODAL) { if (action.type === SHOW_CONTACT_MODAL) {
const ourId = window.ConversationController.getOurConversationIdOrThrow(); const ourId = window.ConversationController.getOurConversationIdOrThrow();
if (action.payload.contactId === ourId) { if (action.payload.contactId === ourId) {
+2 -1
View File
@@ -285,8 +285,9 @@ function showLightbox(opts: {
if (isIncremental(attachment)) { if (isIncremental(attachment)) {
// Queue all attachments, but this target attachment should be IMMEDIATE // Queue all attachments, but this target attachment should be IMMEDIATE
const wasUpdated = await queueAttachmentDownloads(message, { const wasUpdated = await queueAttachmentDownloads(message, {
urgency: AttachmentDownloadUrgency.STANDARD,
attachmentDigestForImmediate: attachment.digest, attachmentDigestForImmediate: attachment.digest,
isManualDownload: true,
urgency: AttachmentDownloadUrgency.STANDARD,
}); });
if (wasUpdated) { if (wasUpdated) {
await window.MessageCache.saveMessage(message); await window.MessageCache.saveMessage(message);
+3 -1
View File
@@ -529,7 +529,9 @@ function queueStoryDownload(
payload: storyId, payload: storyId,
}); });
const wasUpdated = await queueAttachmentDownloads(message); const wasUpdated = await queueAttachmentDownloads(message, {
isManualDownload: true,
});
if (wasUpdated) { if (wasUpdated) {
await window.MessageCache.saveMessage(message); await window.MessageCache.saveMessage(message);
} }
+4 -1
View File
@@ -351,6 +351,7 @@ const getAuthorForMessage = (
avatarUrl, avatarUrl,
badges, badges,
color, color,
firstName,
id, id,
isMe, isMe,
name, name,
@@ -366,6 +367,7 @@ const getAuthorForMessage = (
avatarUrl, avatarUrl,
badges, badges,
color, color,
firstName,
id, id,
isMe, isMe,
name, name,
@@ -772,7 +774,8 @@ export const getPropsForMessage = (
isTapToView: isMessageTapToView, isTapToView: isMessageTapToView,
isTapToViewError: isTapToViewError:
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid, isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
isTapToViewExpired: isMessageTapToView && message.isErased, isTapToViewExpired:
isMessageTapToView && isIncoming(message) && message.isErased,
readStatus: message.readStatus ?? ReadStatus.Read, readStatus: message.readStatus ?? ReadStatus.Read,
selectedReaction, selectedReaction,
status: getMessagePropStatus(message, ourConversationId), status: getMessagePropStatus(message, ourConversationId),
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useEffect } from 'react';
import { useSelector } from 'react-redux';
import type { Props as ContactDetailProps } from '../../components/conversation/ContactDetail';
import { ContactDetail } from '../../components/conversation/ContactDetail';
import { useConversationsActions } from '../ducks/conversations';
import { getMessages } from '../selectors/conversations';
import { getIntl, getRegionCode } from '../selectors/user';
import { embeddedContactSelector } from '../../types/EmbeddedContact';
import { getAccountSelector } from '../selectors/accounts';
export type OwnProps = Pick<ContactDetailProps, 'messageId'>;
export const SmartContactDetail = memo(function SmartContactDetail({
messageId,
}: OwnProps): JSX.Element | null {
const i18n = useSelector(getIntl);
const regionCode = useSelector(getRegionCode);
const messageLookup = useSelector(getMessages);
const accountSelector = useSelector(getAccountSelector);
const {
cancelAttachmentDownload,
kickOffAttachmentDownload,
popPanelForConversation,
startConversation,
} = useConversationsActions();
const contact = messageLookup[messageId]?.contact?.[0];
useEffect(() => {
if (!contact) {
popPanelForConversation();
}
}, [contact, popPanelForConversation]);
if (!contact) {
return null;
}
const numbers = contact?.number;
const firstNumber = numbers && numbers[0] ? numbers[0].value : undefined;
const fixedContact = embeddedContactSelector(contact, {
firstNumber,
regionCode,
serviceId: accountSelector(firstNumber),
});
const signalAccount =
contact.firstNumber && contact.serviceId
? {
phoneNumber: contact.firstNumber,
serviceId: contact.serviceId,
}
: undefined;
return (
<ContactDetail
cancelAttachmentDownload={cancelAttachmentDownload}
contact={fixedContact}
hasSignalAccount={Boolean(signalAccount)}
i18n={i18n}
kickOffAttachmentDownload={kickOffAttachmentDownload}
messageId={messageId}
onSendMessage={() => {
if (signalAccount) {
startConversation(signalAccount.phoneNumber, signalAccount.serviceId);
}
}}
/>
);
});
+3 -20
View File
@@ -13,10 +13,10 @@ import React, {
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { PanelRenderType } from '../../types/Panels'; import type { PanelRenderType } from '../../types/Panels';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { ContactDetail } from '../../components/conversation/ContactDetail';
import { PanelType } from '../../types/Panels'; import { PanelType } from '../../types/Panels';
import { SmartAllMedia } from './AllMedia'; import { SmartAllMedia } from './AllMedia';
import { SmartChatColorPicker } from './ChatColorPicker'; import { SmartChatColorPicker } from './ChatColorPicker';
import { SmartContactDetail } from './ContactDetail';
import { SmartConversationDetails } from './ConversationDetails'; import { SmartConversationDetails } from './ConversationDetails';
import { SmartConversationNotificationsSettings } from './ConversationNotificationsSettings'; import { SmartConversationNotificationsSettings } from './ConversationNotificationsSettings';
import { SmartGV1Members } from './GV1Members'; import { SmartGV1Members } from './GV1Members';
@@ -315,9 +315,6 @@ function PanelElement({
conversationId, conversationId,
panel, panel,
}: PanelPropsType): JSX.Element | null { }: PanelPropsType): JSX.Element | null {
const i18n = useSelector(getIntl);
const { startConversation } = useConversationsActions();
if (panel.type === PanelType.AllMedia) { if (panel.type === PanelType.AllMedia) {
return <SmartAllMedia conversationId={conversationId} />; return <SmartAllMedia conversationId={conversationId} />;
} }
@@ -327,23 +324,9 @@ function PanelElement({
} }
if (panel.type === PanelType.ContactDetails) { if (panel.type === PanelType.ContactDetails) {
const { contact, signalAccount } = panel.args; const { messageId } = panel.args;
return ( return <SmartContactDetail messageId={messageId} />;
<ContactDetail
contact={contact}
hasSignalAccount={Boolean(signalAccount)}
i18n={i18n}
onSendMessage={() => {
if (signalAccount) {
startConversation(
signalAccount.phoneNumber,
signalAccount.serviceId
);
}
}}
/>
);
} }
if (panel.type === PanelType.ConversationDetails) { if (panel.type === PanelType.ConversationDetails) {
+4
View File
@@ -138,12 +138,14 @@ export const SmartGlobalModalContainer = memo(
safetyNumberChangedBlockingData, safetyNumberChangedBlockingData,
safetyNumberModalContactId, safetyNumberModalContactId,
stickerPackPreviewId, stickerPackPreviewId,
tapToViewNotAvailableModalProps,
userNotFoundModalState, userNotFoundModalState,
} = useSelector(getGlobalModalsState); } = useSelector(getGlobalModalsState);
const { const {
closeErrorModal, closeErrorModal,
closeMediaPermissionsModal, closeMediaPermissionsModal,
hideTapToViewNotAvailableModal,
hideUserNotFoundModal, hideUserNotFoundModal,
hideWhatsNewModal, hideWhatsNewModal,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
@@ -223,6 +225,7 @@ export const SmartGlobalModalContainer = memo(
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal} hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal} hideUserNotFoundModal={hideUserNotFoundModal}
hideWhatsNewModal={hideWhatsNewModal} hideWhatsNewModal={hideWhatsNewModal}
hideTapToViewNotAvailableModal={hideTapToViewNotAvailableModal}
i18n={i18n} i18n={i18n}
isAboutContactModalVisible={aboutContactModalContactId != null} isAboutContactModalVisible={aboutContactModalContactId != null}
isProfileEditorVisible={isProfileEditorVisible} isProfileEditorVisible={isProfileEditorVisible}
@@ -259,6 +262,7 @@ export const SmartGlobalModalContainer = memo(
safetyNumberChangedBlockingData={safetyNumberChangedBlockingData} safetyNumberChangedBlockingData={safetyNumberChangedBlockingData}
safetyNumberModalContactId={safetyNumberModalContactId} safetyNumberModalContactId={safetyNumberModalContactId}
stickerPackPreviewId={stickerPackPreviewId} stickerPackPreviewId={stickerPackPreviewId}
tapToViewNotAvailableModalProps={tapToViewNotAvailableModalProps}
theme={theme} theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
userNotFoundModalState={userNotFoundModalState} userNotFoundModalState={userNotFoundModalState}
+2
View File
@@ -63,6 +63,7 @@ export const SmartMessageDetail = memo(
showAttachmentNotAvailableModal, showAttachmentNotAvailableModal,
showContactModal, showContactModal,
showEditHistoryModal, showEditHistoryModal,
showTapToViewNotAvailableModal,
toggleSafetyNumberModal, toggleSafetyNumberModal,
} = useGlobalModalActions(); } = useGlobalModalActions();
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions(); const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
@@ -123,6 +124,7 @@ export const SmartMessageDetail = memo(
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia} showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast} showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showSpoiler={showSpoiler} showSpoiler={showSpoiler}
showTapToViewNotAvailableModal={showTapToViewNotAvailableModal}
startConversation={startConversation} startConversation={startConversation}
theme={theme} theme={theme}
toggleSafetyNumberModal={toggleSafetyNumberModal} toggleSafetyNumberModal={toggleSafetyNumberModal}
+2
View File
@@ -147,6 +147,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
showAttachmentNotAvailableModal, showAttachmentNotAvailableModal,
showContactModal, showContactModal,
showEditHistoryModal, showEditHistoryModal,
showTapToViewNotAvailableModal,
toggleMessageRequestActionsConfirmation, toggleMessageRequestActionsConfirmation,
toggleDeleteMessagesModal, toggleDeleteMessagesModal,
toggleEditNicknameAndNoteModal, toggleEditNicknameAndNoteModal,
@@ -241,6 +242,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia} showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast} showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast}
showSpoiler={showSpoiler} showSpoiler={showSpoiler}
showTapToViewNotAvailableModal={showTapToViewNotAvailableModal}
startConversation={startConversation} startConversation={startConversation}
toggleDeleteMessagesModal={toggleDeleteMessagesModal} toggleDeleteMessagesModal={toggleDeleteMessagesModal}
toggleForwardMessagesModal={toggleForwardMessagesModal} toggleForwardMessagesModal={toggleForwardMessagesModal}
+38 -5
View File
@@ -181,7 +181,7 @@ describe('processDataMessage', () => {
assert.strictEqual(out.profileKey, 'Khc3'); assert.strictEqual(out.profileKey, 'Khc3');
}); });
it('should process quote', () => { it('should process quote, dropping second attachment', () => {
const out = check({ const out = check({
quote: { quote: {
id: Long.fromNumber(1), id: Long.fromNumber(1),
@@ -190,7 +190,12 @@ describe('processDataMessage', () => {
attachments: [ attachments: [
{ {
contentType: 'image/jpeg', contentType: 'image/jpeg',
fileName: 'image.jpg', fileName: 'image1.jpg',
thumbnail: UNPROCESSED_ATTACHMENT,
},
{
contentType: 'image/jpeg',
fileName: 'image2.jpg',
thumbnail: UNPROCESSED_ATTACHMENT, thumbnail: UNPROCESSED_ATTACHMENT,
}, },
], ],
@@ -204,7 +209,7 @@ describe('processDataMessage', () => {
attachments: [ attachments: [
{ {
contentType: IMAGE_JPEG, contentType: IMAGE_JPEG,
fileName: 'image.jpg', fileName: 'image1.jpg',
thumbnail: PROCESSED_ATTACHMENT, thumbnail: PROCESSED_ATTACHMENT,
}, },
], ],
@@ -213,7 +218,7 @@ describe('processDataMessage', () => {
}); });
}); });
it('should process contact', () => { it('should process contact, dropping second contact', () => {
const out = check({ const out = check({
contact: [ contact: [
{ {
@@ -234,8 +239,36 @@ describe('processDataMessage', () => {
{ {
avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: false }, avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: false },
}, },
]);
});
it('should process preview, dropping second preview', () => {
const out = check({
preview: [
{
description:
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
image: UNPROCESSED_ATTACHMENT,
title: 'Signal Private Messenger #1',
url: 'https://signal.org',
},
{
description: 'Say "hello" again',
image: UNPROCESSED_ATTACHMENT,
title: 'Signal Private Messenger #2',
url: 'https://signal.org',
},
],
});
assert.deepStrictEqual(out.preview, [
{ {
avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: true }, date: undefined,
description:
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
image: PROCESSED_ATTACHMENT,
title: 'Signal Private Messenger #1',
url: 'https://signal.org',
}, },
]); ]);
}); });
@@ -122,6 +122,7 @@ describe('AttachmentDownloadManager/JobManager', () => {
await downloadManager?.addJob({ await downloadManager?.addJob({
urgency, urgency,
...job, ...job,
isManualDownload: Boolean(job.isManualDownload),
}); });
} }
async function addJobs( async function addJobs(
@@ -377,7 +378,10 @@ describe('AttachmentDownloadManager/JobManager', () => {
// add the same job again and it should retry ASAP and reset attempts // add the same job again and it should retry ASAP and reset attempts
attempts = getPromisesForAttempts(jobs[0], 5); attempts = getPromisesForAttempts(jobs[0], 5);
await downloadManager?.addJob(jobs[0]); await downloadManager?.addJob({
...jobs[0],
isManualDownload: Boolean(jobs[0].isManualDownload),
});
await attempts[0].completed; await attempts[0].completed;
assert.strictEqual(runJob.callCount, 4); assert.strictEqual(runJob.callCount, 4);
+4 -1
View File
@@ -43,7 +43,7 @@ describe('settings', function (this: Mocha.Suite) {
await settingsWindow.getByText('Language').first().waitFor(); await settingsWindow.getByText('Language').first().waitFor();
await settingsWindow.getByText('Chats').click(); await settingsWindow.getByText('Chats').click();
await settingsWindow.getByText('Sent media quality').waitFor(); await settingsWindow.getByText('Spell check text').waitFor();
await settingsWindow.getByText('Calls').click(); await settingsWindow.getByText('Calls').click();
await settingsWindow.getByText('Enable incoming calls').waitFor(); await settingsWindow.getByText('Enable incoming calls').waitFor();
@@ -53,5 +53,8 @@ describe('settings', function (this: Mocha.Suite) {
await settingsWindow.getByText('Privacy').click(); await settingsWindow.getByText('Privacy').click();
await settingsWindow.getByText('Read receipts').waitFor(); await settingsWindow.getByText('Read receipts').waitFor();
await settingsWindow.getByText('Data usage').click();
await settingsWindow.getByText('Sent media quality').waitFor();
}); });
}); });
+7
View File
@@ -13,6 +13,13 @@ import { DataReader, DataWriter } from '../sql/Client';
import type { SignalProtocolStore } from '../SignalProtocolStore'; import type { SignalProtocolStore } from '../SignalProtocolStore';
import * as log from '../logging/log'; import * as log from '../logging/log';
export const DEFAULT_AUTO_DOWNLOAD_ATTACHMENT = {
photos: true,
videos: true,
audio: true,
documents: true,
};
export class Storage implements StorageInterface { export class Storage implements StorageInterface {
public readonly user: User; public readonly user: User;
+3 -3
View File
@@ -151,7 +151,7 @@ export function processQuote(
id: quote.id?.toNumber(), id: quote.id?.toNumber(),
authorAci: normalizeAci(authorAci, 'Quote.authorAci'), authorAci: normalizeAci(authorAci, 'Quote.authorAci'),
text: dropNull(quote.text), text: dropNull(quote.text),
attachments: (quote.attachments ?? []).map(attachment => { attachments: (quote.attachments ?? []).slice(0, 1).map(attachment => {
return { return {
contentType: attachment.contentType contentType: attachment.contentType
? stringToMIMEType(attachment.contentType) ? stringToMIMEType(attachment.contentType)
@@ -172,7 +172,7 @@ export function processContact(
return undefined; return undefined;
} }
return contact.map(item => { return contact.slice(0, 1).map(item => {
return { return {
...item, ...item,
avatar: item.avatar avatar: item.avatar
@@ -206,7 +206,7 @@ export function processPreview(
return undefined; return undefined;
} }
return preview.map(item => { return preview.slice(0, 1).map(item => {
return { return {
url: dropNull(item.url), url: dropNull(item.url),
title: dropNull(item.title), title: dropNull(item.title),
+14 -12
View File
@@ -28,15 +28,16 @@ export type AttachmentDownloadJobTypeType = z.infer<
>; >;
export type CoreAttachmentDownloadJobType = { export type CoreAttachmentDownloadJobType = {
attachment: AttachmentType;
attachmentType: AttachmentDownloadJobTypeType;
ciphertextSize: number;
contentType: MIMEType;
digest: string;
isManualDownload?: boolean;
messageId: string; messageId: string;
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
attachmentType: AttachmentDownloadJobTypeType;
attachment: AttachmentType;
digest: string;
contentType: MIMEType;
size: number; size: number;
ciphertextSize: number;
source: AttachmentDownloadSource; source: AttachmentDownloadSource;
}; };
@@ -44,18 +45,19 @@ export type AttachmentDownloadJobType = CoreAttachmentDownloadJobType &
JobManagerJobType; JobManagerJobType;
export const coreAttachmentDownloadJobSchema = z.object({ export const coreAttachmentDownloadJobSchema = z.object({
messageId: z.string(),
receivedAt: z.number(),
sentAt: z.number(),
attachmentType: attachmentDownloadTypeSchema,
attachment: z attachment: z
.object({ size: z.number(), contentType: MIMETypeSchema }) .object({ size: z.number(), contentType: MIMETypeSchema })
.passthrough(), .passthrough(),
digest: z.string(), attachmentType: attachmentDownloadTypeSchema,
contentType: MIMETypeSchema,
size: z.number(),
ciphertextSize: z.number(), ciphertextSize: z.number(),
contentType: MIMETypeSchema,
digest: z.string(),
isManualDownload: z.boolean().optional(),
messageId: z.string(),
messageIdForLogging: z.string().optional(), messageIdForLogging: z.string().optional(),
receivedAt: z.number(),
sentAt: z.number(),
size: z.number(),
source: z.nativeEnum(AttachmentDownloadSource), source: z.nativeEnum(AttachmentDownloadSource),
}); });
+2 -12
View File
@@ -3,9 +3,7 @@
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import type { EmbeddedContactType } from './EmbeddedContact';
import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { ReadonlyMessageAttributesType } from '../model-types.d';
import type { ServiceIdString } from './ServiceId';
export enum PanelType { export enum PanelType {
AllMedia = 'AllMedia', AllMedia = 'AllMedia',
@@ -27,11 +25,7 @@ export type PanelRequestType = ReadonlyDeep<
| { | {
type: PanelType.ContactDetails; type: PanelType.ContactDetails;
args: { args: {
contact: EmbeddedContactType; messageId: string;
signalAccount?: {
phoneNumber: string;
serviceId: ServiceIdString;
};
}; };
} }
| { type: PanelType.ConversationDetails } | { type: PanelType.ConversationDetails }
@@ -50,11 +44,7 @@ export type PanelRenderType = ReadonlyDeep<
| { | {
type: PanelType.ContactDetails; type: PanelType.ContactDetails;
args: { args: {
contact: EmbeddedContactType; messageId: string;
signalAccount?: {
phoneNumber: string;
serviceId: ServiceIdString;
};
}; };
} }
| { type: PanelType.ConversationDetails } | { type: PanelType.ConversationDetails }
+8
View File
@@ -22,6 +22,13 @@ import type { ServiceIdString } from './ServiceId';
import type { RegisteredChallengeType } from '../challenge'; import type { RegisteredChallengeType } from '../challenge';
export type AutoDownloadAttachmentType = {
photos: boolean;
videos: boolean;
audio: boolean;
documents: boolean;
};
export type SerializedCertificateType = { export type SerializedCertificateType = {
expires: number; expires: number;
serialized: Uint8Array; serialized: Uint8Array;
@@ -47,6 +54,7 @@ export type StorageAccessType = {
'always-relay-calls': boolean; 'always-relay-calls': boolean;
'audio-notification': boolean; 'audio-notification': boolean;
'auto-download-update': boolean; 'auto-download-update': boolean;
'auto-download-attachment': AutoDownloadAttachmentType;
autoConvertEmoji: boolean; autoConvertEmoji: boolean;
'badge-count-muted-conversations': boolean; 'badge-count-muted-conversations': boolean;
'blocked-groups': ReadonlyArray<string>; 'blocked-groups': ReadonlyArray<string>;
+5
View File
@@ -6,6 +6,7 @@ export enum ToastType {
AddedUsersToCall = 'AddedUsersToCall', AddedUsersToCall = 'AddedUsersToCall',
AlreadyGroupMember = 'AlreadyGroupMember', AlreadyGroupMember = 'AlreadyGroupMember',
AlreadyRequestedToJoin = 'AlreadyRequestedToJoin', AlreadyRequestedToJoin = 'AlreadyRequestedToJoin',
AttachmentDownloadFailed = 'AttachmentDownloadFailed',
AttachmentDownloadStillInProgress = 'AttachmentDownloadStillInProgress', AttachmentDownloadStillInProgress = 'AttachmentDownloadStillInProgress',
Blocked = 'Blocked', Blocked = 'Blocked',
BlockedGroup = 'BlockedGroup', BlockedGroup = 'BlockedGroup',
@@ -82,6 +83,10 @@ export type AnyToast =
} }
| { toastType: ToastType.AlreadyGroupMember } | { toastType: ToastType.AlreadyGroupMember }
| { toastType: ToastType.AlreadyRequestedToJoin } | { toastType: ToastType.AlreadyRequestedToJoin }
| {
toastType: ToastType.AttachmentDownloadFailed;
parameters: { messageId: string };
}
| { | {
toastType: ToastType.AttachmentDownloadStillInProgress; toastType: ToastType.AttachmentDownloadStillInProgress;
parameters: { count: number }; parameters: { count: number };
+3 -1
View File
@@ -90,7 +90,9 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
// to display the message properly. // to display the message properly.
hasRequiredAttachmentDownloads(message.attributes) hasRequiredAttachmentDownloads(message.attributes)
) { ) {
const shouldSave = await queueAttachmentDownloadsForMessage(message); const shouldSave = await queueAttachmentDownloadsForMessage(message, {
isManualDownload: false,
});
if (shouldSave) { if (shouldSave) {
messageIdsToSave.push(messageId); messageIdsToSave.push(messageId);
} }
+13 -1
View File
@@ -6,7 +6,10 @@ import type { SystemPreferences } from 'electron';
import type { AudioDevice } from '@signalapp/ringrtc'; import type { AudioDevice } from '@signalapp/ringrtc';
import { noop } from 'lodash'; import { noop } from 'lodash';
import type { ZoomFactorType } from '../types/Storage.d'; import type {
AutoDownloadAttachmentType,
ZoomFactorType,
} from '../types/Storage.d';
import type { import type {
ConversationColorType, ConversationColorType,
CustomColorType, CustomColorType,
@@ -55,6 +58,7 @@ import type { SystemTraySetting } from '../types/SystemTraySetting';
import { drop } from './drop'; import { drop } from './drop';
import { sendSyncRequests } from '../textsecure/syncRequests'; import { sendSyncRequests } from '../textsecure/syncRequests';
import { waitForEvent } from '../shims/events'; import { waitForEvent } from '../shims/events';
import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../textsecure/Storage';
type SentMediaQualityType = 'standard' | 'high'; type SentMediaQualityType = 'standard' | 'high';
type NotificationSettingType = 'message' | 'name' | 'count' | 'off'; type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
@@ -64,6 +68,7 @@ export type IPCEventsValuesType = {
audioNotification: boolean | undefined; audioNotification: boolean | undefined;
audioMessage: boolean; audioMessage: boolean;
autoConvertEmoji: boolean; autoConvertEmoji: boolean;
autoDownloadAttachment: AutoDownloadAttachmentType;
autoDownloadUpdate: boolean; autoDownloadUpdate: boolean;
autoLaunch: boolean; autoLaunch: boolean;
callRingtoneNotification: boolean; callRingtoneNotification: boolean;
@@ -408,6 +413,13 @@ export function createIPCEvents(
window.storage.get('typingIndicators', false), window.storage.get('typingIndicators', false),
// Configurable settings // Configurable settings
getAutoDownloadAttachment: () =>
window.storage.get(
'auto-download-attachment',
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
),
setAutoDownloadAttachment: (setting: AutoDownloadAttachmentType) =>
window.storage.put('auto-download-attachment', setting),
getAutoDownloadUpdate: () => getAutoDownloadUpdate: () =>
window.storage.get('auto-download-update', true), window.storage.get('auto-download-update', true),
setAutoDownloadUpdate: value => setAutoDownloadUpdate: value =>
+3 -1
View File
@@ -289,7 +289,9 @@ export async function handleEditMessage(
}); });
// Queue up any downloads in case they're different, update the fields if so. // Queue up any downloads in case they're different, update the fields if so.
const wasUpdated = await queueAttachmentDownloads(mainMessageModel); const wasUpdated = await queueAttachmentDownloads(mainMessageModel, {
isManualDownload: false,
});
// If we've scheduled a bodyAttachment download, we need that edit to know about it // If we've scheduled a bodyAttachment download, we need that edit to know about it
if (wasUpdated && mainMessageModel.get('bodyAttachment')) { if (wasUpdated && mainMessageModel.get('bodyAttachment')) {
+7
View File
@@ -1785,6 +1785,13 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2023-08-10T00:23:35.320Z" "updated": "2023-08-10T00:23:35.320Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/conversation/AttachmentStatusIcon.tsx",
"line": " const timerRef = useRef<NodeJS.Timeout | undefined>();",
"reasonCategory": "usageTrusted",
"updated": "2025-02-21T04:17:59.239Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/conversation/CallingNotification.tsx", "path": "ts/components/conversation/CallingNotification.tsx",
+168 -74
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as defaultLogger from '../logging/log'; import * as defaultLogger from '../logging/log';
import { isLongMessage } from '../types/MIME'; import { isAudio, isImage, isLongMessage, isVideo } from '../types/MIME';
import { getMessageIdForLogging } from './idForLogging'; import { getMessageIdForLogging } from './idForLogging';
import { import {
copyStickerToAttachments, copyStickerToAttachments,
@@ -23,6 +23,7 @@ import {
getAttachmentSignatureSafe, getAttachmentSignatureSafe,
isDownloading, isDownloading,
isDownloaded, isDownloaded,
isVoiceMessage,
partitionBodyAndNormalAttachments, partitionBodyAndNormalAttachments,
} from '../types/Attachment'; } from '../types/Attachment';
import type { StickerType } from '../types/Stickers'; import type { StickerType } from '../types/Stickers';
@@ -44,6 +45,7 @@ import {
} from './attachmentDownloadQueue'; } from './attachmentDownloadQueue';
import { queueUpdateMessage } from './messageBatcher'; import { queueUpdateMessage } from './messageBatcher';
import type { LoggerType } from '../types/Logging'; import type { LoggerType } from '../types/Logging';
import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../textsecure/Storage';
export type MessageAttachmentsDownloadedType = { export type MessageAttachmentsDownloadedType = {
bodyAttachment?: AttachmentType; bodyAttachment?: AttachmentType;
@@ -84,18 +86,21 @@ export async function handleAttachmentDownloadsForNewMessage(
if (shouldUseAttachmentDownloadQueue()) { if (shouldUseAttachmentDownloadQueue()) {
addToAttachmentDownloadQueue(logId, message); addToAttachmentDownloadQueue(logId, message);
} else { } else {
await queueAttachmentDownloadsForMessage(message); await queueAttachmentDownloadsForMessage(message, {
isManualDownload: false,
});
} }
} }
} }
export async function queueAttachmentDownloadsForMessage( export async function queueAttachmentDownloadsForMessage(
message: MessageModel, message: MessageModel,
urgency?: AttachmentDownloadUrgency options: {
urgency?: AttachmentDownloadUrgency;
isManualDownload: boolean;
}
): Promise<boolean> { ): Promise<boolean> {
const updated = await queueAttachmentDownloads(message, { const updated = await queueAttachmentDownloads(message, options);
urgency,
});
if (!updated) { if (!updated) {
return false; return false;
} }
@@ -111,15 +116,22 @@ export async function queueAttachmentDownloadsForMessage(
export async function queueAttachmentDownloads( export async function queueAttachmentDownloads(
message: MessageModel, message: MessageModel,
{ {
urgency = AttachmentDownloadUrgency.STANDARD,
source = AttachmentDownloadSource.STANDARD,
attachmentDigestForImmediate, attachmentDigestForImmediate,
isManualDownload,
source = AttachmentDownloadSource.STANDARD,
urgency = AttachmentDownloadUrgency.STANDARD,
}: { }: {
urgency?: AttachmentDownloadUrgency;
source?: AttachmentDownloadSource;
attachmentDigestForImmediate?: string; attachmentDigestForImmediate?: string;
} = {} isManualDownload: boolean;
source?: AttachmentDownloadSource;
urgency?: AttachmentDownloadUrgency;
}
): Promise<boolean> { ): Promise<boolean> {
const autoDownloadAttachment = window.storage.get(
'auto-download-attachment',
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
);
const messageId = message.id; const messageId = message.id;
const idForLogging = getMessageIdForLogging(message.attributes); const idForLogging = getMessageIdForLogging(message.attributes);
@@ -153,50 +165,51 @@ export async function queueAttachmentDownloads(
bodyAttachmentsToDownload.map(attachment => bodyAttachmentsToDownload.map(attachment =>
AttachmentDownloadManager.addJob({ AttachmentDownloadManager.addJob({
attachment, attachment,
messageId,
attachmentType: 'long-message', attachmentType: 'long-message',
isManualDownload,
messageId,
receivedAt: message.get('received_at'), receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'), sentAt: message.get('sent_at'),
urgency,
source, source,
urgency,
}) })
) )
); );
count += bodyAttachmentsToDownload.length; count += bodyAttachmentsToDownload.length;
} }
const startingAttachments = message.get('attachments') || [];
const { attachments, count: attachmentsCount } = await queueNormalAttachments( const { attachments, count: attachmentsCount } = await queueNormalAttachments(
{ {
attachmentDigestForImmediate,
attachments: startingAttachments,
isManualDownload,
logId, logId,
messageId, messageId,
attachments: message.get('attachments'),
otherAttachments: message otherAttachments: message
.get('editHistory') .get('editHistory')
?.flatMap(x => x.attachments ?? []), ?.flatMap(x => x.attachments ?? []),
receivedAt: message.get('received_at'), receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'), sentAt: message.get('sent_at'),
urgency,
source, source,
attachmentDigestForImmediate, urgency,
} }
); );
if (attachmentsCount > 0) { if (attachmentsCount > 0) {
message.set({ attachments }); message.set({ attachments });
}
if (startingAttachments.length > 0) {
log.info( log.info(
`${logId}: Queueing ${attachmentsCount} normal attachment downloads` `${logId}: Queued ${attachmentsCount} (of ${startingAttachments.length}) normal attachment downloads`
); );
} }
count += attachmentsCount; count += attachmentsCount;
const previewsToQueue = message.get('preview') || []; const previewsToQueue = message.get('preview') || [];
if (previewsToQueue.length > 0) {
log.info(
`${logId}: Queueing ${previewsToQueue.length} preview attachment downloads`
);
}
const { preview, count: previewCount } = await queuePreviews({ const { preview, count: previewCount } = await queuePreviews({
logId, logId,
isManualDownload,
messageId, messageId,
previews: previewsToQueue, previews: previewsToQueue,
otherPreviews: message.get('editHistory')?.flatMap(x => x.preview ?? []), otherPreviews: message.get('editHistory')?.flatMap(x => x.preview ?? []),
@@ -208,40 +221,41 @@ export async function queueAttachmentDownloads(
if (previewCount > 0) { if (previewCount > 0) {
message.set({ preview }); message.set({ preview });
} }
if (previewsToQueue.length > 0) {
log.info(
`${logId}: Queued ${previewCount} (of ${previewsToQueue.length}) preview attachment downloads`
);
}
count += previewCount; count += previewCount;
const numQuoteAttachments = message.get('quote')?.attachments?.length ?? 0; const numQuoteAttachments = message.get('quote')?.attachments?.length ?? 0;
if (numQuoteAttachments > 0) {
log.info(
`${logId}: Queueing ${numQuoteAttachments} ` +
'quote attachment downloads'
);
}
const { quote, count: thumbnailCount } = await queueQuoteAttachments({ const { quote, count: thumbnailCount } = await queueQuoteAttachments({
logId, logId,
isManualDownload,
messageId, messageId,
quote: message.get('quote'),
otherQuotes: otherQuotes:
message message
.get('editHistory') .get('editHistory')
?.map(x => x.quote) ?.map(x => x.quote)
.filter(isNotNil) ?? [], .filter(isNotNil) ?? [],
quote: message.get('quote'),
receivedAt: message.get('received_at'), receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'), sentAt: message.get('sent_at'),
urgency,
source, source,
urgency,
}); });
if (thumbnailCount > 0) { if (thumbnailCount > 0) {
message.set({ quote }); message.set({ quote });
} }
if (numQuoteAttachments > 0) {
log.info(
`${logId}: Queued ${thumbnailCount} (of ${numQuoteAttachments}) quote attachment downloads`
);
}
count += thumbnailCount; count += thumbnailCount;
const contactsToQueue = message.get('contact') || []; const contactsToQueue = message.get('contact') || [];
if (contactsToQueue.length > 0) { let avatarCount = 0;
log.info(
`${logId}: Queueing ${contactsToQueue.length} contact attachment downloads`
);
}
const contact = await Promise.all( const contact = await Promise.all(
contactsToQueue.map(async item => { contactsToQueue.map(async item => {
if (!item.avatar || !item.avatar.avatar) { if (!item.avatar || !item.avatar.avatar) {
@@ -253,31 +267,47 @@ export async function queueAttachmentDownloads(
return item; return item;
} }
count += 1; if (!isManualDownload) {
if (autoDownloadAttachment.photos === false) {
return item;
}
}
avatarCount += 1;
return { return {
...item, ...item,
avatar: { avatar: {
...item.avatar, ...item.avatar,
avatar: await AttachmentDownloadManager.addJob({ avatar: await AttachmentDownloadManager.addJob({
attachment: item.avatar.avatar, attachment: item.avatar.avatar,
messageId,
attachmentType: 'contact', attachmentType: 'contact',
isManualDownload,
messageId,
receivedAt: message.get('received_at'), receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'), sentAt: message.get('sent_at'),
urgency,
source, source,
urgency,
}), }),
}, },
}; };
}) })
); );
message.set({ contact }); if (avatarCount > 0) {
message.set({ contact });
}
if (contactsToQueue.length > 0) {
log.info(
`${logId}: Queued ${avatarCount} (of ${contactsToQueue.length}) contact attachment downloads`
);
}
count += avatarCount;
let sticker = message.get('sticker'); let sticker = message.get('sticker');
let copiedSticker = false;
let queuedStickerDownload = false;
if (sticker && sticker.data && sticker.data.path) { if (sticker && sticker.data && sticker.data.path) {
log.info(`${logId}: Sticker attachment already downloaded`); log.info(`${logId}: Sticker attachment already downloaded`);
} else if (sticker) { } else if (sticker) {
log.info(`${logId}: Queueing sticker download`);
count += 1; count += 1;
const { packId, stickerId, packKey } = sticker; const { packId, stickerId, packKey } = sticker;
@@ -286,7 +316,9 @@ export async function queueAttachmentDownloads(
if (status && (status === 'downloaded' || status === 'installed')) { if (status && (status === 'downloaded' || status === 'installed')) {
try { try {
log.info(`${logId}: Copying sticker from installed pack`);
data = await copyStickerToAttachments(packId, stickerId); data = await copyStickerToAttachments(packId, stickerId);
copiedSticker = true;
} catch (error) { } catch (error) {
log.error( log.error(
`${logId}: Problem copying sticker (${packId}, ${stickerId}) to attachments:`, `${logId}: Problem copying sticker (${packId}, ${stickerId}) to attachments:`,
@@ -294,16 +326,20 @@ export async function queueAttachmentDownloads(
); );
} }
} }
if (!data) { if (!data) {
if (sticker.data) { if (sticker.data) {
log.info(`${logId}: Queueing sticker download`);
queuedStickerDownload = true;
data = await AttachmentDownloadManager.addJob({ data = await AttachmentDownloadManager.addJob({
attachment: sticker.data, attachment: sticker.data,
messageId,
attachmentType: 'sticker', attachmentType: 'sticker',
isManualDownload,
messageId,
receivedAt: message.get('received_at'), receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'), sentAt: message.get('sent_at'),
urgency,
source, source,
urgency,
}); });
} else { } else {
log.error(`${logId}: Sticker data was missing`); log.error(`${logId}: Sticker data was missing`);
@@ -317,7 +353,7 @@ export async function queueAttachmentDownloads(
}; };
if (!status) { if (!status) {
// Save the packId/packKey for future download/install // Save the packId/packKey for future download/install
void savePackMetadata(packId, packKey, stickerRef); await savePackMetadata(packId, packKey, stickerRef);
} else { } else {
await DataWriter.addStickerPackReference(stickerRef); await DataWriter.addStickerPackReference(stickerRef);
} }
@@ -328,39 +364,45 @@ export async function queueAttachmentDownloads(
sticker = { sticker = {
...sticker, ...sticker,
packId,
data, data,
}; };
} }
message.set({ sticker }); if (queuedStickerDownload || copiedSticker) {
message.set({ sticker });
}
let editHistory = message.get('editHistory'); let editHistory = message.get('editHistory');
let allEditsAttachmentCount = 0;
if (editHistory) { if (editHistory) {
log.info(`${logId}: Looping through ${editHistory.length} edits`); log.info(`${logId}: Looping through ${editHistory.length} edits`);
editHistory = await Promise.all( editHistory = await Promise.all(
editHistory.map(async edit => { editHistory.map(async edit => {
const { attachments: editAttachments, count: editAttachmentsCount } = const { attachments: editAttachments, count: editAttachmentsCount } =
await queueNormalAttachments({ await queueNormalAttachments({
attachments: edit.attachments,
isManualDownload,
logId, logId,
messageId, messageId,
attachments: edit.attachments,
otherAttachments: attachments, otherAttachments: attachments,
receivedAt: message.get('received_at'), receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'), sentAt: message.get('sent_at'),
urgency,
source, source,
urgency,
}); });
count += editAttachmentsCount; count += editAttachmentsCount;
if (editAttachmentsCount !== 0) { allEditsAttachmentCount += editAttachmentsCount;
if (editAttachments.length !== 0) {
log.info( log.info(
`${logId}: Queueing ${editAttachmentsCount} normal attachment ` + `${logId}: Queued ${editAttachmentsCount} (of ${edit.attachments?.length ?? 0}) ` +
`downloads (edited:${edit.timestamp})` `normal attachment downloads (edited:${edit.timestamp})`
); );
} }
const { preview: editPreview, count: editPreviewCount } = const { preview: editPreview, count: editPreviewCount } =
await queuePreviews({ await queuePreviews({
logId, logId,
isManualDownload,
messageId, messageId,
previews: edit.preview, previews: edit.preview,
otherPreviews: preview, otherPreviews: preview,
@@ -370,10 +412,11 @@ export async function queueAttachmentDownloads(
source, source,
}); });
count += editPreviewCount; count += editPreviewCount;
if (editPreviewCount !== 0) { allEditsAttachmentCount += editPreviewCount;
if (editPreview.length !== 0) {
log.info( log.info(
`${logId}: Queueing ${editPreviewCount} preview attachment ` + `${logId}: Queued ${editPreviewCount} (of ${edit.preview?.length ?? 0}) ` +
`downloads (edited:${edit.timestamp})` `preview attachment downloads (edited:${edit.timestamp})`
); );
} }
@@ -385,7 +428,9 @@ export async function queueAttachmentDownloads(
}) })
); );
} }
message.set({ editHistory }); if (allEditsAttachmentCount > 0) {
message.set({ editHistory });
}
if (count <= 0) { if (count <= 0) {
return false; return false;
@@ -397,25 +442,27 @@ export async function queueAttachmentDownloads(
} }
export async function queueNormalAttachments({ export async function queueNormalAttachments({
attachmentDigestForImmediate,
attachments = [],
isManualDownload,
logId, logId,
messageId, messageId,
attachments = [],
otherAttachments, otherAttachments,
receivedAt, receivedAt,
sentAt, sentAt,
urgency,
source, source,
attachmentDigestForImmediate, urgency,
}: { }: {
attachmentDigestForImmediate?: string;
attachments: MessageAttributesType['attachments'];
isManualDownload: boolean;
logId: string; logId: string;
messageId: string; messageId: string;
attachments: MessageAttributesType['attachments'];
otherAttachments: MessageAttributesType['attachments']; otherAttachments: MessageAttributesType['attachments'];
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
urgency: AttachmentDownloadUrgency;
source: AttachmentDownloadSource; source: AttachmentDownloadSource;
attachmentDigestForImmediate?: string; urgency: AttachmentDownloadUrgency;
}): Promise<{ }): Promise<{
attachments: Array<AttachmentType>; attachments: Array<AttachmentType>;
count: number; count: number;
@@ -472,6 +519,33 @@ export async function queueNormalAttachments({
return existingAttachment; return existingAttachment;
} }
const { contentType } = attachment;
if (!isManualDownload) {
const autoDownloadAttachment = window.storage.get(
'auto-download-attachment',
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
);
if (isVideo(contentType)) {
if (autoDownloadAttachment.videos === false) {
return attachment;
}
} else if (isImage(contentType)) {
if (autoDownloadAttachment.photos === false) {
return attachment;
}
} else if (isAudio(contentType)) {
if (
autoDownloadAttachment.audio === false &&
!isVoiceMessage(attachment)
) {
return attachment;
}
} else if (autoDownloadAttachment.documents === false) {
return attachment;
}
}
count += 1; count += 1;
const urgencyForAttachment = const urgencyForAttachment =
@@ -481,12 +555,13 @@ export async function queueNormalAttachments({
: urgency; : urgency;
return AttachmentDownloadManager.addJob({ return AttachmentDownloadManager.addJob({
attachment, attachment,
messageId,
attachmentType: 'attachment', attachmentType: 'attachment',
isManualDownload,
messageId,
receivedAt, receivedAt,
sentAt, sentAt,
urgency: urgencyForAttachment,
source, source,
urgency: urgencyForAttachment,
}); });
}) })
); );
@@ -513,23 +588,25 @@ function getLinkPreviewSignature(preview: LinkPreviewType): string | undefined {
} }
async function queuePreviews({ async function queuePreviews({
isManualDownload,
logId, logId,
messageId, messageId,
previews = [],
otherPreviews, otherPreviews,
previews = [],
receivedAt, receivedAt,
sentAt, sentAt,
urgency,
source, source,
urgency,
}: { }: {
isManualDownload: boolean;
logId: string; logId: string;
messageId: string; messageId: string;
previews: MessageAttributesType['preview'];
otherPreviews: MessageAttributesType['preview']; otherPreviews: MessageAttributesType['preview'];
previews: MessageAttributesType['preview'];
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
urgency: AttachmentDownloadUrgency;
source: AttachmentDownloadSource; source: AttachmentDownloadSource;
urgency: AttachmentDownloadUrgency;
}): Promise<{ preview: Array<LinkPreviewType>; count: number }> { }): Promise<{ preview: Array<LinkPreviewType>; count: number }> {
const log = getLogger(source); const log = getLogger(source);
// Similar to queueNormalAttachments' logic for detecting same attachments // Similar to queueNormalAttachments' logic for detecting same attachments
@@ -572,17 +649,29 @@ async function queuePreviews({
return existingPreview; return existingPreview;
} }
if (!isManualDownload) {
const autoDownloadAttachment = window.storage.get(
'auto-download-attachment',
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
);
if (autoDownloadAttachment.photos === false) {
return item;
}
}
count += 1; count += 1;
return { return {
...item, ...item,
image: await AttachmentDownloadManager.addJob({ image: await AttachmentDownloadManager.addJob({
attachment: item.image, attachment: item.image,
messageId,
attachmentType: 'preview', attachmentType: 'preview',
isManualDownload,
messageId,
receivedAt, receivedAt,
sentAt, sentAt,
urgency,
source, source,
urgency,
}), }),
}; };
}) })
@@ -609,23 +698,25 @@ function getQuoteThumbnailSignature(
} }
async function queueQuoteAttachments({ async function queueQuoteAttachments({
isManualDownload,
logId, logId,
messageId, messageId,
quote,
otherQuotes, otherQuotes,
quote,
receivedAt, receivedAt,
sentAt, sentAt,
urgency,
source, source,
urgency,
}: { }: {
logId: string; logId: string;
isManualDownload: boolean;
messageId: string; messageId: string;
quote: QuotedMessageType | undefined;
otherQuotes: ReadonlyArray<QuotedMessageType>; otherQuotes: ReadonlyArray<QuotedMessageType>;
quote: QuotedMessageType | undefined;
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
urgency: AttachmentDownloadUrgency;
source: AttachmentDownloadSource; source: AttachmentDownloadSource;
urgency: AttachmentDownloadUrgency;
}): Promise<{ quote?: QuotedMessageType; count: number }> { }): Promise<{ quote?: QuotedMessageType; count: number }> {
const log = getLogger(source); const log = getLogger(source);
let count = 0; let count = 0;
@@ -691,17 +782,20 @@ async function queueQuoteAttachments({
}; };
} }
// Note: we always download quote attachments
count += 1; count += 1;
return { return {
...item, ...item,
thumbnail: await AttachmentDownloadManager.addJob({ thumbnail: await AttachmentDownloadManager.addJob({
attachment: item.thumbnail, attachment: item.thumbnail,
messageId,
attachmentType: 'quote', attachmentType: 'quote',
isManualDownload,
messageId,
receivedAt, receivedAt,
sentAt, sentAt,
urgency,
source, source,
urgency,
}), }),
}; };
}) })
+1
View File
@@ -42,6 +42,7 @@ installSetting('audioMessage');
installSetting('audioNotification'); installSetting('audioNotification');
installSetting('autoConvertEmoji'); installSetting('autoConvertEmoji');
installSetting('autoDownloadUpdate'); installSetting('autoDownloadUpdate');
installSetting('autoDownloadAttachment');
installSetting('autoLaunch'); installSetting('autoLaunch');
installSetting('callRingtoneNotification'); installSetting('callRingtoneNotification');
installSetting('callSystemNotification'); installSetting('callSystemNotification');
+4
View File
@@ -25,6 +25,7 @@ setEnvironment(
SettingsWindowProps.onRender( SettingsWindowProps.onRender(
({ ({
addCustomColor, addCustomColor,
autoDownloadAttachment,
availableCameras, availableCameras,
availableLocales, availableLocales,
availableMicrophones, availableMicrophones,
@@ -75,6 +76,7 @@ SettingsWindowProps.onRender(
notificationContent, notificationContent,
onAudioNotificationsChange, onAudioNotificationsChange,
onAutoConvertEmojiChange, onAutoConvertEmojiChange,
onAutoDownloadAttachmentChange,
onAutoDownloadUpdateChange, onAutoDownloadUpdateChange,
onAutoLaunchChange, onAutoLaunchChange,
onCallNotificationsChange, onCallNotificationsChange,
@@ -126,6 +128,7 @@ SettingsWindowProps.onRender(
ReactDOM.render( ReactDOM.render(
<Preferences <Preferences
addCustomColor={addCustomColor} addCustomColor={addCustomColor}
autoDownloadAttachment={autoDownloadAttachment}
availableCameras={availableCameras} availableCameras={availableCameras}
availableLocales={availableLocales} availableLocales={availableLocales}
availableMicrophones={availableMicrophones} availableMicrophones={availableMicrophones}
@@ -180,6 +183,7 @@ SettingsWindowProps.onRender(
notificationContent={notificationContent} notificationContent={notificationContent}
onAudioNotificationsChange={onAudioNotificationsChange} onAudioNotificationsChange={onAudioNotificationsChange}
onAutoConvertEmojiChange={onAutoConvertEmojiChange} onAutoConvertEmojiChange={onAutoConvertEmojiChange}
onAutoDownloadAttachmentChange={onAutoDownloadAttachmentChange}
onAutoDownloadUpdateChange={onAutoDownloadUpdateChange} onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
onAutoLaunchChange={onAutoLaunchChange} onAutoLaunchChange={onAutoLaunchChange}
onCallNotificationsChange={onCallNotificationsChange} onCallNotificationsChange={onCallNotificationsChange}
+7
View File
@@ -25,6 +25,7 @@ const settingMessageAudio = createSetting('audioMessage');
const settingAudioNotification = createSetting('audioNotification'); const settingAudioNotification = createSetting('audioNotification');
const settingAutoConvertEmoji = createSetting('autoConvertEmoji'); const settingAutoConvertEmoji = createSetting('autoConvertEmoji');
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate'); const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
const settingAutoDownloadAttachment = createSetting('autoDownloadAttachment');
const settingAutoLaunch = createSetting('autoLaunch'); const settingAutoLaunch = createSetting('autoLaunch');
const settingCallRingtoneNotification = createSetting( const settingCallRingtoneNotification = createSetting(
'callRingtoneNotification' 'callRingtoneNotification'
@@ -139,6 +140,7 @@ function attachRenderCallback<Value>(f: (value: Value) => Promise<Value>) {
async function renderPreferences() { async function renderPreferences() {
const { const {
autoDownloadAttachment,
blockedCount, blockedCount,
deviceName, deviceName,
hasAudioNotifications, hasAudioNotifications,
@@ -181,6 +183,7 @@ async function renderPreferences() {
defaultConversationColor, defaultConversationColor,
isSyncNotSupported, isSyncNotSupported,
} = await awaitObject({ } = await awaitObject({
autoDownloadAttachment: settingAutoDownloadAttachment.getValue(),
blockedCount: settingBlockedCount.getValue(), blockedCount: settingBlockedCount.getValue(),
deviceName: settingDeviceName.getValue(), deviceName: settingDeviceName.getValue(),
hasAudioNotifications: settingAudioNotification.getValue(), hasAudioNotifications: settingAudioNotification.getValue(),
@@ -266,6 +269,7 @@ async function renderPreferences() {
const props = { const props = {
// Settings // Settings
autoDownloadAttachment,
availableCameras, availableCameras,
availableLocales, availableLocales,
availableMicrophones, availableMicrophones,
@@ -352,6 +356,9 @@ async function renderPreferences() {
onAutoDownloadUpdateChange: attachRenderCallback( onAutoDownloadUpdateChange: attachRenderCallback(
settingAutoDownloadUpdate.setValue settingAutoDownloadUpdate.setValue
), ),
onAutoDownloadAttachmentChange: attachRenderCallback(
settingAutoDownloadAttachment.setValue
),
onAutoLaunchChange: attachRenderCallback(settingAutoLaunch.setValue), onAutoLaunchChange: attachRenderCallback(settingAutoLaunch.setValue),
onCallNotificationsChange: attachRenderCallback( onCallNotificationsChange: attachRenderCallback(
settingCallSystemNotification.setValue settingCallSystemNotification.setValue