diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 59e13f5dec..372b863f6f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2419,6 +2419,22 @@ "messageformat": "Message", "description": "In conversation details, label for button to switch to the conversation view in order to draft a message in that converation" }, + "icu:ConversationDetails--help-section": { + "messageformat": "Help", + "description": "Title of the help section in the conversation details screen" + }, + "icu:ConversationDetails--support-center": { + "messageformat": "Support Center", + "description": "Label for the support center link in conversation details" + }, + "icu:ConversationDetails--contact-us": { + "messageformat": "Contact us", + "description": "Label for the contact us link in conversation details" + }, + "icu:ConversationDetails--donate": { + "messageformat": "Donate to Signal", + "description": "Label for the donation link in conversation details" + }, "icu:SafetyNumberNotification__viewSafetyNumber": { "messageformat": "View Safety Number", "description": "In conversation, safety number change notification, label for button to view safety number, opens safety number modal" @@ -3635,6 +3651,14 @@ "messageformat": "Be careful when accepting message requests from people you don’t know. Watch out for:", "description": "Description of the safety tips modal" }, + "icu:SafetyTipsModal__TipTitle--Fake": { + "messageformat": "Fake names and accounts", + "description": "Title of the fake name safety tip" + }, + "icu:SafetyTipsModal__TipDescription--Fake": { + "messageformat": "Signal will never contact you for your registration code or PIN. Be cautious of requests that impersonate others. Profile names are chosen by their account holder and aren't verified.", + "description": "Description of the fake name safety tip" + }, "icu:SafetyTipsModal__TipTitle--Crypto": { "messageformat": "Crypto or money scams", "description": "Title of the crypto safety tip" @@ -3811,6 +3835,14 @@ "messageformat": "Accept", "description": "Shown as a button to let the user accept a message request" }, + "icu:MessageRequests--accept-confirm-title": { + "messageformat": "Accept Request?", + "description": "Title of confirmation dialog shown before accepting a message request" + }, + "icu:MessageRequests--accept-confirm-body": { + "messageformat": "Review requests carefully. Profile names are chosen by their account owner and aren't verified.", + "description": "Body text of confirmation dialog shown before accepting a message request" + }, "icu:MessageRequests--continue": { "messageformat": "Continue", "description": "Shown as a button to share your profile, necessary to continue messaging in a conversation" @@ -3827,6 +3859,26 @@ "messageformat": "{count, plural, one {# member} other {# members}}", "description": "Specifies the number of members in a group conversation" }, + "icu:ConversationHero--review-carefully": { + "messageformat": "Review carefully", + "description": "Label shown in conversation hero to advise users to review the conversation carefully" + }, + "icu:ConversationHero--group-names": { + "messageformat": "Group names are not verified", + "description": "Label for group names in the name verification warning in conversation hero" + }, + "icu:ConversationHero--profile-names": { + "messageformat": "Profile names are not verified", + "description": "Label for profile names in the name verification warning in conversation hero" + }, + "icu:ConversationHero--signal-official-chat": { + "messageformat": "This is the official and only chat from Signal", + "description": "Text indicating that this is the official Signal conversation" + }, + "icu:ConversationHero--release-notes": { + "messageformat": "Keep up to date with news and release notes.", + "description": "Text explaining the purpose of the Signal official conversation" + }, "icu:member-of-1-group": { "messageformat": "Member of {group}", "description": "Shown in the conversation hero to indicate this user is a member of a mutual group" @@ -5823,10 +5875,6 @@ "messageformat": "Use this link to join a Signal call: {url}", "description": "Draft message text for sharing a call link" }, - "icu:MessageRequestWarning__learn-more": { - "messageformat": "Learn more", - "description": "Shown on the message request warning. Clicking this button will open a dialog with more information" - }, "icu:MessageRequestWarning__safety-tips": { "messageformat": "Safety Tips", "description": "Shown on the message request warning. Clicking this button will open a dialog with safety tips" @@ -8077,6 +8125,38 @@ "messageformat": "To change this setting, set “Who can see my number” to “Nobody”.", "description": "A toast displayed when user clicks disabled option in settings window" }, + "icu:ProfileNameWarningModal__description--direct": { + "messageformat": "Profile names on Signal are chosen by their account holder:", + "description": "Description of how profile names work in the profile name warning modal for direct conversations" + }, + "icu:ProfileNameWarningModal__list--item1--direct": { + "messageformat": "Profile names aren't verified", + "description": "First list item in profile name warning modal for direct conversations" + }, + "icu:ProfileNameWarningModal__list--item2--direct": { + "messageformat": "Be cautious of accounts that impersonate others", + "description": "Second list item in profile name warning modal for direct conversations" + }, + "icu:ProfileNameWarningModal__list--item3--direct": { + "messageformat": "Don't share personal information with people you don't know", + "description": "Third list item in profile name warning modal for direct conversations" + }, + "icu:ProfileNameWarningModal__description--group": { + "messageformat": "Group names are chosen by members of the group.", + "description": "Description of how group names work in the profile name warning modal for group conversations" + }, + "icu:ProfileNameWarningModal__list--item1--group": { + "messageformat": "Be cautious of groups that impersonate organizations and businesses", + "description": "First list item in profile name warning modal for group conversations" + }, + "icu:ProfileNameWarningModal__list--item2--group": { + "messageformat": "Profile names of members in groups are not verified", + "description": "Second list item in profile name warning modal for group conversations" + }, + "icu:ProfileNameWarningModal__list--item3--group": { + "messageformat": "Don't share personal information with people you don't know", + "description": "Third list item in profile name warning modal for group conversations" + }, "icu:WhatsNew__modal-title": { "messageformat": "What's New", "description": "Title for the whats new modal" diff --git a/images/icons/v3/error/error-triangle-fill-compact-bold.svg b/images/icons/v3/error/error-triangle-fill-compact-bold.svg new file mode 100644 index 0000000000..81036d1fea --- /dev/null +++ b/images/icons/v3/error/error-triangle-fill-compact-bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v3/group/group-questionmark-compact.svg b/images/icons/v3/group/group-questionmark-compact.svg new file mode 100644 index 0000000000..3ae627af56 --- /dev/null +++ b/images/icons/v3/group/group-questionmark-compact.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/images/icons/v3/help/help-light.svg b/images/icons/v3/help/help-light.svg new file mode 100644 index 0000000000..a5f73c334f --- /dev/null +++ b/images/icons/v3/help/help-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v3/invite/invite.svg b/images/icons/v3/invite/invite.svg new file mode 100644 index 0000000000..be289305cd --- /dev/null +++ b/images/icons/v3/invite/invite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v3/person/person-questionmark-compact.svg b/images/icons/v3/person/person-questionmark-compact.svg new file mode 100644 index 0000000000..3609b4ddfb --- /dev/null +++ b/images/icons/v3/person/person-questionmark-compact.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/images/icons/v3/person/person-questionmark-light.svg b/images/icons/v3/person/person-questionmark-light.svg new file mode 100644 index 0000000000..3228751d06 --- /dev/null +++ b/images/icons/v3/person/person-questionmark-light.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/images/safety-tips/safety-tip-business.png b/images/safety-tips/safety-tip-business.png deleted file mode 100644 index 5e6254b6d3..0000000000 Binary files a/images/safety-tips/safety-tip-business.png and /dev/null differ diff --git a/images/safety-tips/safety-tip-business.webp b/images/safety-tips/safety-tip-business.webp new file mode 100644 index 0000000000..d163e2fe18 Binary files /dev/null and b/images/safety-tips/safety-tip-business.webp differ diff --git a/images/safety-tips/safety-tip-crypto.png b/images/safety-tips/safety-tip-crypto.png deleted file mode 100644 index 4209a052b3..0000000000 Binary files a/images/safety-tips/safety-tip-crypto.png and /dev/null differ diff --git a/images/safety-tips/safety-tip-crypto.webp b/images/safety-tips/safety-tip-crypto.webp new file mode 100644 index 0000000000..893fdc1b23 Binary files /dev/null and b/images/safety-tips/safety-tip-crypto.webp differ diff --git a/images/safety-tips/safety-tip-fake.webp b/images/safety-tips/safety-tip-fake.webp new file mode 100644 index 0000000000..6179e5345f Binary files /dev/null and b/images/safety-tips/safety-tip-fake.webp differ diff --git a/images/safety-tips/safety-tip-links.png b/images/safety-tips/safety-tip-links.png deleted file mode 100644 index 44ae839630..0000000000 Binary files a/images/safety-tips/safety-tip-links.png and /dev/null differ diff --git a/images/safety-tips/safety-tip-links.webp b/images/safety-tips/safety-tip-links.webp new file mode 100644 index 0000000000..a98c6b0770 Binary files /dev/null and b/images/safety-tips/safety-tip-links.webp differ diff --git a/images/safety-tips/safety-tip-vague.png b/images/safety-tips/safety-tip-vague.png deleted file mode 100644 index a17d2c2051..0000000000 Binary files a/images/safety-tips/safety-tip-vague.png and /dev/null differ diff --git a/images/safety-tips/safety-tip-vague.webp b/images/safety-tips/safety-tip-vague.webp new file mode 100644 index 0000000000..830ca29aa1 Binary files /dev/null and b/images/safety-tips/safety-tip-vague.webp differ diff --git a/stylesheets/components/AboutContactModal.scss b/stylesheets/components/AboutContactModal.scss index fe0e8f9788..bf0118fc9e 100644 --- a/stylesheets/components/AboutContactModal.scss +++ b/stylesheets/components/AboutContactModal.scss @@ -44,13 +44,10 @@ flex-shrink: 0; @mixin about-modal-icon($url) { - @include mixins.light-theme { - @include mixins.color-svg($url, variables.$color-black); - } - - @include mixins.dark-theme { - @include mixins.color-svg($url, variables.$color-gray-05); - } + @include mixins.color-svg( + $url, + light-dark(variables.$color-black, variables.$color-gray-05) + ); } &--profile { @@ -106,6 +103,18 @@ &--note { @include about-modal-icon('../images/icons/v3/note/note.svg'); } + + &--group-question { + @include about-modal-icon( + '../images/icons/v3/group/group-questionmark-compact.svg' + ); + } + + &--direct-question { + @include about-modal-icon( + '../images/icons/v3/person/person-questionmark-compact.svg' + ); + } } &__button { diff --git a/stylesheets/components/ConversationDetails.scss b/stylesheets/components/ConversationDetails.scss index bf8709b304..4e1b7d3fb1 100644 --- a/stylesheets/components/ConversationDetails.scss +++ b/stylesheets/components/ConversationDetails.scss @@ -33,17 +33,14 @@ justify-content: center; width: 32px; - @include mixins.light-theme { - background: variables.$color-gray-02; - &::before { - @include plus-icon(variables.$color-black); - } - } - @include mixins.dark-theme { - background: variables.$color-gray-90; - &::before { - @include plus-icon(variables.$color-gray-15); - } + background: light-dark( + variables.$color-gray-02, + variables.$color-gray-90 + ); + &::before { + @include plus-icon( + light-dark(variables.$color-black, variables.$color-gray-15) + ); } } } @@ -63,12 +60,7 @@ &__pending--info { @include mixins.font-subtitle; - @include mixins.light-theme { - color: variables.$color-gray-60; - } - @include mixins.dark-theme { - color: variables.$color-gray-25; - } + color: light-dark(variables.$color-gray-60, variables.$color-gray-25); & { padding-block: 0; padding-inline: 28px; @@ -112,13 +104,7 @@ $light-color: variables.$color-gray-75, $dark-color: variables.$color-gray-15 ) { - @include mixins.light-theme { - @include mixins.color-svg($url, $light-color); - } - - @include mixins.dark-theme { - @include mixins.color-svg($url, $dark-color); - } + @include mixins.color-svg($url, light-dark($light-color, $dark-color)); } &--color { @@ -171,6 +157,12 @@ } } + &--bell { + &::after { + @include details-icon('../images/icons/v3/bell/bell-compact.svg'); + } + } + &--link { &::after { @include details-icon('../images/icons/v3/link/link.svg'); @@ -215,13 +207,10 @@ &--down { border-radius: 18px; - @include mixins.light-theme { - background-color: variables.$color-gray-02; - } - - @include mixins.dark-theme { - background-color: variables.$color-gray-90; - } + background-color: light-dark( + variables.$color-gray-02, + variables.$color-gray-90 + ); &::after { width: 18px; @@ -264,6 +253,24 @@ ); } } + + &--help { + &::after { + @include details-icon('../images/icons/v3/help/help-light.svg'); + } + } + + &--invite { + &::after { + @include details-icon('../images/icons/v3/invite/invite.svg'); + } + } + + &--heart { + &::after { + @include details-icon('../images/icons/v3/heart/heart.svg'); + } + } } } @@ -303,13 +310,7 @@ background: none; border: none; padding: 0; - - @include mixins.light-theme { - color: variables.$color-gray-95; - } - @include mixins.dark-theme { - color: variables.$color-gray-05; - } + color: light-dark(variables.$color-gray-95, variables.$color-gray-05); } } @@ -331,13 +332,10 @@ background: none; &:hover:not(:disabled) { - @include mixins.light-theme { - background-color: variables.$color-gray-02; - } - - @include mixins.dark-theme { - background-color: variables.$color-gray-90; - } + background-color: light-dark( + variables.$color-gray-02, + variables.$color-gray-90 + ); & .ConversationDetails-panel-row__actions { opacity: 1; @@ -376,14 +374,7 @@ &__info { margin-top: 4px; - - @include mixins.light-theme { - color: variables.$color-gray-60; - } - - @include mixins.dark-theme { - color: variables.$color-gray-25; - } + color: light-dark(variables.$color-gray-60, variables.$color-gray-25); } &__right { @@ -410,15 +401,10 @@ &:not(:first-child)::before { border-top: 1px solid transparent; - - @include mixins.light-theme { - border-top-color: variables.$color-gray-15; - } - - @include mixins.dark-theme { - border-top-color: variables.$color-gray-65; - } - + border-top-color: light-dark( + variables.$color-gray-15, + variables.$color-gray-65 + ); & { content: ''; display: block; @@ -495,48 +481,24 @@ } .ConversationDetails__CallHistoryGroup__ItemIcon--Audio { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/phone/phone.svg', - variables.$color-black - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/phone/phone.svg', - variables.$color-gray-15 - ); - } + @include mixins.color-svg( + '../images/icons/v3/phone/phone.svg', + light-dark(variables.$color-black, variables.$color-gray-15) + ); } .ConversationDetails__CallHistoryGroup__ItemIcon--Video { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/video/video.svg', - variables.$color-black - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/video/video.svg', - variables.$color-gray-15 - ); - } + @include mixins.color-svg( + '../images/icons/v3/video/video.svg', + light-dark(variables.$color-black, variables.$color-gray-15) + ); } .ConversationDetails__CallHistoryGroup__ItemIcon--Adhoc { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/link/link.svg', - variables.$color-black - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/link/link.svg', - variables.$color-gray-15 - ); - } + @include mixins.color-svg( + '../images/icons/v3/link/link.svg', + light-dark(variables.$color-black, variables.$color-gray-15) + ); } .ConversationDetails__CallHistoryGroup__ItemLabel { @@ -558,18 +520,10 @@ content: ''; width: 20px; height: 20px; - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-down.svg', - variables.$color-black - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-down.svg', - variables.$color-white - ); - } + @include mixins.color-svg( + '../images/icons/v3/chevron/chevron-down.svg', + light-dark(variables.$color-black, variables.$color-white) + ); } } @@ -580,16 +534,8 @@ .ConversationDetails--nickname-actions--delete { width: 16px; height: 16px; - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/trash/trash.svg', - variables.$color-black - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/trash/trash.svg', - variables.$color-white - ); - } + @include mixins.color-svg( + '../images/icons/v3/trash/trash.svg', + light-dark(variables.$color-black, variables.$color-white) + ); } diff --git a/stylesheets/components/ConversationHero.scss b/stylesheets/components/ConversationHero.scss index 1df007b883..de6763551b 100644 --- a/stylesheets/components/ConversationHero.scss +++ b/stylesheets/components/ConversationHero.scss @@ -34,18 +34,10 @@ position: relative; inset-block-start: 2px; - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right-bold.svg', - variables.$color-gray-90 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right-bold.svg', - variables.$color-gray-05 - ); - } + @include mixins.color-svg( + '../images/icons/v3/chevron/chevron-right-bold.svg', + light-dark(variables.$color-gray-90, variables.$color-gray-05) + ); } &__profile-name { @@ -57,13 +49,7 @@ margin-bottom: 2px; margin-top: 0; - @include mixins.light-theme { - color: variables.$color-gray-90; - } - - @include mixins.dark-theme { - color: variables.$color-gray-05; - } + color: light-dark(variables.$color-gray-90, variables.$color-gray-05); } &__with { @@ -73,13 +59,7 @@ margin-bottom: 20px; max-width: 500px; - @include mixins.light-theme { - color: variables.$color-gray-60; - } - - @include mixins.dark-theme { - color: variables.$color-gray-25; - } + color: light-dark(variables.$color-gray-60, variables.$color-gray-25); } &__note-to-self { @@ -88,12 +68,16 @@ padding-block: 0; padding-inline: 16px; - @include mixins.light-theme { - color: variables.$color-gray-60; - } + color: light-dark(variables.$color-gray-60, variables.$color-gray-25); + } - @include mixins.dark-theme { - color: variables.$color-gray-25; + &__members-count__button { + @include mixins.button-reset; + & { + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: variables.$color-gray-25; } } @@ -101,12 +85,54 @@ border-radius: 9999px; padding-block: 6px; padding-inline: 14px; - margin-top: 12px; + margin-top: 5px; @include mixins.font-subtitle; } - &__membership { - @include mixins.font-body-2; + &__review-carefully { + @include mixins.font-body-2-bold; + color: #a98b52; + } + + &__group-question-icon { + display: inline-block; + height: 16px; + width: 22px; + vertical-align: text-top; + margin-inline-end: 8px; + + @include mixins.color-svg( + '../images/icons/v3/group/group-questionmark-compact.svg', + light-dark(variables.$color-black, variables.$color-gray-05) + ); + } + + &__direct-question-icon { + display: inline-block; + height: 16px; + width: 16px; + vertical-align: text-top; + margin-inline-end: 8px; + + @include mixins.color-svg( + '../images/icons/v3/person/person-questionmark-compact.svg', + light-dark(variables.$color-black, variables.$color-gray-05) + ); + } + + &__name-not-verified__button { + @include mixins.button-reset; + & { + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: variables.$color-gray-25; + } + } + + &--release-notes-notice { + @include mixins.font-body-1; + user-select: none; max-width: 255px; @@ -114,24 +140,72 @@ padding-block: 16px; padding-inline: 20px; + border-radius: 18px; + background-color: #e0e8fc; + margin-inline: auto; + display: flex; + flex-direction: column; + gap: 8px; + color: variables.$color-gray-75; + } + + &__release-notes-notice-content { + text-align: center; + } + + &__release-notes-notice-check-icon { + display: inline-block; + height: 16px; + width: 16px; + margin-inline-end: 4px; + position: relative; + top: 3px; + + @include mixins.color-svg( + '../images/icons/v3/check/check-circle-fill.svg', + variables.$color-borage-blue + ); + } + + &__release-notes-notice-bell-icon { + display: inline-block; + height: 16px; + width: 16px; + + margin-inline-end: 4px; + position: relative; + top: 3px; + + @include mixins.color-svg( + '../images/icons/v3/bell/bell-compact.svg', + variables.$color-gray-75 + ); + } + + &__membership { + @include mixins.font-body-2; + user-select: none; + + max-width: 255px; + margin-inline: auto; + margin-block-start: 10px; + padding-block: 16px; + padding-inline: 20px; + border-radius: 18px; border-style: solid; - border-width: 1.5px; + border-width: 2.5px; - @include mixins.light-theme() { - border-color: variables.$color-gray-05; - } - @include mixins.dark-theme() { - border-color: variables.$color-gray-80; - } + display: flex; + flex-direction: column; + gap: 10px; - @include mixins.light-theme { - color: variables.$color-gray-90; - } + border-color: light-dark( + variables.$color-gray-04, + variables.$color-gray-80 + ); - @include mixins.dark-theme { - color: variables.$color-gray-02; - } + color: light-dark(variables.$color-gray-90, variables.$color-gray-02); &__chevron { display: inline-block; @@ -140,19 +214,10 @@ vertical-align: text-top; margin-inline-end: 8px; - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/group/group.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/group/group.svg', - variables.$color-gray-05 - ); - } + @include mixins.color-svg( + '../images/icons/v3/group/group.svg', + light-dark(variables.$color-black, variables.$color-gray-05) + ); } &__name { @@ -160,38 +225,34 @@ font-weight: normal; } + &__review-carefully-icon { + display: inline-block; + height: 18px; + width: 18px; + vertical-align: text-top; + margin-inline-end: 8px; + + @include mixins.color-svg( + '../images/icons/v3/error/error-triangle-fill-compact-bold.svg', + #a98b52 + ); + } + &__warning { line-height: 20px; - - &__icon { - content: ''; - display: inline-block; - height: 18px; - margin-inline-end: 8px; - width: 18px; - vertical-align: middle; - - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/info/info.svg', - variables.$color-gray-90 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/info/info.svg', - variables.$color-gray-02 - ); - } - } - - &__learn-more { - @include mixins.button-reset(); - & { - cursor: pointer; - text-decoration: underline; - } - } } } + + &__members-count-icon { + display: inline-block; + height: 16px; + width: 16px; + vertical-align: text-top; + margin-inline-end: 8px; + + @include mixins.color-svg( + '../images/icons/v3/group/group-compact.svg', + light-dark(variables.$color-black, variables.$color-gray-05) + ); + } } diff --git a/stylesheets/components/ProfileNameWarningModal.scss b/stylesheets/components/ProfileNameWarningModal.scss new file mode 100644 index 0000000000..9c473e428b --- /dev/null +++ b/stylesheets/components/ProfileNameWarningModal.scss @@ -0,0 +1,51 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +@use '../mixins'; +@use '../variables'; + +.ProfileNameWarningModal { + .ProfileNameWarningModal__body_inner { + display: flex; + flex-direction: column; + } + + .ProfileNameWarningModal__header-icon { + display: block; + align-self: center; + height: 48px; + width: 48px; + margin-bottom: 24px; + @include mixins.color-svg( + '../images/icons/v3/person/person-questionmark-light.svg', + light-dark(variables.$color-black, variables.$color-gray-05) + ); + } + + .ProfileNameWarningModal__description { + margin-bottom: 24px; + } + + .ProfileNameWarningModal__list { + padding-inline-start: 24px; + margin-top: 0px; + } + + .ProfileNameWarningModal__list-item { + position: relative; + padding-inline-start: 17px; + margin-bottom: 25px; + list-style-type: none; + } + + .ProfileNameWarningModal__list-item::before { + content: ''; + position: absolute; + inset-inline-start: 0; + top: 0; + width: 4px; + height: 100%; + background-color: variables.$color-gray-20; + border-radius: 6px; + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index e1bcc29327..8afcdc3ef6 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -144,6 +144,7 @@ @use 'components/PlaybackRateButton.scss'; @use 'components/Preferences.scss'; @use 'components/ProfileEditor.scss'; +@use 'components/ProfileNameWarningModal.scss'; @use 'components/ProgressBar.scss'; @use 'components/ProgressCircle.scss'; @use 'components/Quote.scss'; diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 1d7a0bec09..f428e9e4aa 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -100,6 +100,7 @@ export type OwnProps = Readonly<{ areWeAdmin: boolean | null; areWePending: boolean | null; areWePendingApproval: boolean | null; + sharedGroupNames?: ReadonlyArray; cancelRecording: () => unknown; completeRecording: ( conversationId: string, @@ -350,6 +351,7 @@ export const CompositionArea = memo(function CompositionArea({ // SMS-only contacts isSmsOnlyOrUnregistered, isFetchingUUID, + sharedGroupNames, renderSmartCompositionRecording, renderSmartCompositionRecordingDraft, // Selected messages @@ -893,6 +895,7 @@ export const CompositionArea = memo(function CompositionArea({ isBlocked={isBlocked} isHidden={isHidden} isReported={isReported} + sharedGroupNames={sharedGroupNames} acceptConversation={acceptConversation} reportSpam={reportSpam} blockAndReportSpam={blockAndReportSpam} diff --git a/ts/components/DialogUpdate.tsx b/ts/components/DialogUpdate.tsx index f6632b3d2f..ad50a7e25c 100644 --- a/ts/components/DialogUpdate.tsx +++ b/ts/components/DialogUpdate.tsx @@ -11,12 +11,16 @@ import { I18n } from './I18n'; import { LeftPaneDialog } from './LeftPaneDialog'; import type { WidthBreakpoint } from './_util'; import { formatFileSize } from '../util/formatFileSize'; +import { getLocalizedUrl } from '../util/getLocalizedUrl'; function contactSupportLink(parts: ReactNode): JSX.Element { + const localizedSupportLink = getLocalizedUrl( + 'https://support.signal.org/hc/LOCALE/requests/new?desktop' + ); return ( diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index 45bb2dc9e4..d3c5b41903 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -133,6 +133,9 @@ export type PropsType = { // UsernameOnboarding usernameOnboardingState: UsernameOnboardingState; renderUsernameOnboarding: () => JSX.Element; + isProfileNameWarningModalVisible: boolean; + profileNameWarningModalConversationType?: string; + renderProfileNameWarningModal: () => JSX.Element; }; export function GlobalModalContainer({ @@ -220,6 +223,9 @@ export function GlobalModalContainer({ // UsernameOnboarding usernameOnboardingState, renderUsernameOnboarding, + // ProfileNameWarningModal + isProfileNameWarningModalVisible, + renderProfileNameWarningModal, }: PropsType): JSX.Element | null { // We want the following dialogs to show in this order: // 1. Errors @@ -296,6 +302,10 @@ export function GlobalModalContainer({ return renderProfileEditor(); } + if (isProfileNameWarningModalVisible) { + return renderProfileNameWarningModal(); + } + if (isShortcutGuideModalVisible) { return renderShortcutGuideModal(); } diff --git a/ts/components/SafetyTipsModal.tsx b/ts/components/SafetyTipsModal.tsx index 0f798db5a5..04a6b8f39c 100644 --- a/ts/components/SafetyTipsModal.tsx +++ b/ts/components/SafetyTipsModal.tsx @@ -19,29 +19,35 @@ export function SafetyTipsModal({ }: SafetyTipsModalProps): JSX.Element { const pages = useMemo(() => { return [ + { + key: 'fake', + title: i18n('icu:SafetyTipsModal__TipTitle--Fake'), + description: i18n('icu:SafetyTipsModal__TipDescription--Fake'), + imageUrl: 'images/safety-tips/safety-tip-fake.webp', + }, { key: 'crypto', title: i18n('icu:SafetyTipsModal__TipTitle--Crypto'), description: i18n('icu:SafetyTipsModal__TipDescription--Crypto'), - imageUrl: 'images/safety-tips/safety-tip-crypto.png', + imageUrl: 'images/safety-tips/safety-tip-crypto.webp', }, { key: 'vague', title: i18n('icu:SafetyTipsModal__TipTitle--Vague'), description: i18n('icu:SafetyTipsModal__TipDescription--Vague'), - imageUrl: 'images/safety-tips/safety-tip-vague.png', + imageUrl: 'images/safety-tips/safety-tip-vague.webp', }, { key: 'links', title: i18n('icu:SafetyTipsModal__TipTitle--Links'), description: i18n('icu:SafetyTipsModal__TipDescription--Links'), - imageUrl: 'images/safety-tips/safety-tip-links.png', + imageUrl: 'images/safety-tips/safety-tip-links.webp', }, { key: 'business', title: i18n('icu:SafetyTipsModal__TipTitle--Business'), description: i18n('icu:SafetyTipsModal__TipDescription--Business'), - imageUrl: 'images/safety-tips/safety-tip-business.png', + imageUrl: 'images/safety-tips/safety-tip-business.webp', }, ]; }, [i18n]); diff --git a/ts/components/conversation/AboutContactModal.stories.tsx b/ts/components/conversation/AboutContactModal.stories.tsx index ef3f747518..930f5ae405 100644 --- a/ts/components/conversation/AboutContactModal.stories.tsx +++ b/ts/components/conversation/AboutContactModal.stories.tsx @@ -66,9 +66,11 @@ export default { onOpenNotePreviewModal: action('onOpenNotePreviewModal'), toggleSignalConnectionsModal: action('toggleSignalConnections'), toggleSafetyNumberModal: action('toggleSafetyNumberModal'), + toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'), updateSharedGroups: action('updateSharedGroups'), unblurAvatar: action('unblurAvatar'), conversation, + fromOrAddedByTrustedContact: false, isSignalConnection: false, }, } satisfies ComponentMeta; @@ -124,3 +126,13 @@ export function WithSharedGroups(args: PropsType): JSX.Element { /> ); } + +export function DirectFromTrustedContact(args: PropsType): JSX.Element { + return ( + + ); +} diff --git a/ts/components/conversation/AboutContactModal.tsx b/ts/components/conversation/AboutContactModal.tsx index 1cdc329c24..4a8eeaf39c 100644 --- a/ts/components/conversation/AboutContactModal.tsx +++ b/ts/components/conversation/AboutContactModal.tsx @@ -1,7 +1,7 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useCallback, useEffect } from 'react'; +import React, { type ReactNode, useCallback, useEffect } from 'react'; import type { ConversationType } from '../../state/ducks/conversations'; import type { LocalizerType } from '../../types/Util'; import { isInSystemContacts } from '../../util/isInSystemContacts'; @@ -26,9 +26,11 @@ export type PropsType = Readonly<{ onClose: () => void; onOpenNotePreviewModal: () => void; conversation: ConversationType; + fromOrAddedByTrustedContact?: boolean; isSignalConnection: boolean; toggleSignalConnectionsModal: () => void; toggleSafetyNumberModal: (id: string) => void; + toggleProfileNameWarningModal: () => void; updateSharedGroups: (id: string) => void; unblurAvatar: (conversationId: string) => void; }>; @@ -36,9 +38,11 @@ export type PropsType = Readonly<{ export function AboutContactModal({ i18n, conversation, + fromOrAddedByTrustedContact, isSignalConnection, toggleSignalConnectionsModal, toggleSafetyNumberModal, + toggleProfileNameWarningModal, updateSharedGroups, unblurAvatar, onClose, @@ -77,6 +81,14 @@ export function AboutContactModal({ [toggleSafetyNumberModal, conversation.id] ); + const onProfileNameWarningClick = useCallback( + (ev: React.MouseEvent) => { + ev.preventDefault(); + toggleProfileNameWarningModal(); + }, + [toggleProfileNameWarningModal] + ); + let statusRow: JSX.Element | undefined; if (isMe) { @@ -185,6 +197,32 @@ export function AboutContactModal({ )} + {!isMe && !fromOrAddedByTrustedContact ? ( +
+ + +
+ ) : null} + {!isMe && conversation.isVerified ? (
diff --git a/ts/components/conversation/ChatSessionRefreshedNotification.tsx b/ts/components/conversation/ChatSessionRefreshedNotification.tsx index fb79253b59..d0f91e5f68 100644 --- a/ts/components/conversation/ChatSessionRefreshedNotification.tsx +++ b/ts/components/conversation/ChatSessionRefreshedNotification.tsx @@ -10,7 +10,7 @@ import { Button, ButtonSize, ButtonVariant } from '../Button'; import { SystemMessage } from './SystemMessage'; import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog'; import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser'; -import { mapToSupportLocale } from '../../util/mapToSupportLocale'; +import { getLocalizedUrl } from '../../util/getLocalizedUrl'; type PropsHousekeepingType = { i18n: LocalizerType; @@ -34,11 +34,9 @@ export function ChatSessionRefreshedNotification( const wrappedContactSupport = useCallback(() => { setIsDialogOpen(false); - const baseUrl = - 'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed'; - const locale = window.SignalContext.getResolvedMessagesLocale(); - const supportLocale = mapToSupportLocale(locale); - const url = baseUrl.replace('LOCALE', supportLocale); + const url = getLocalizedUrl( + 'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed' + ); openLinkInWebBrowser(url); }, [setIsDialogOpen]); diff --git a/ts/components/conversation/ContactSpoofingReviewDialog.tsx b/ts/components/conversation/ContactSpoofingReviewDialog.tsx index 4921cd1e91..16ce5ad7f5 100644 --- a/ts/components/conversation/ContactSpoofingReviewDialog.tsx +++ b/ts/components/conversation/ContactSpoofingReviewDialog.tsx @@ -147,6 +147,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element { case MessageRequestState.reportingAndMaybeBlocking: case MessageRequestState.acceptedOptions: case MessageRequestState.unblocking: + case MessageRequestState.accepting: assertDev( false, `Got unexpected MessageRequestState.${MessageRequestState[messageRequestState]} state. Clearing confiration state` diff --git a/ts/components/conversation/ConversationHero.stories.tsx b/ts/components/conversation/ConversationHero.stories.tsx index 5a90e1fbd0..b1ad80691c 100644 --- a/ts/components/conversation/ConversationHero.stories.tsx +++ b/ts/components/conversation/ConversationHero.stories.tsx @@ -19,12 +19,16 @@ export default { component: ConversationHero, args: { conversationType: 'direct', + fromOrAddedByTrustedContact: true, i18n, + isDirectConvoAndHasNickname: false, theme: ThemeType.light, unblurAvatar: action('unblurAvatar'), updateSharedGroups: action('updateSharedGroups'), viewUserStories: action('viewUserStories'), toggleAboutContactModal: action('toggleAboutContactModal'), + toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'), + openConversationDetails: action('openConversationDetails'), }, } satisfies Meta; @@ -73,6 +77,12 @@ DirectNoGroupsJustProfile.args = { phoneNumber: casual.phone, }; +export const SignalConversation = Template.bind({}); +SignalConversation.args = { + isSignalConversation: true, + phoneNumber: casual.phone, +}; + export const DirectNoGroupsJustPhoneNumber = Template.bind({}); DirectNoGroupsJustPhoneNumber.args = { phoneNumber: casual.phone, @@ -146,6 +156,15 @@ GroupNoName.args = { title: '', }; +export const GroupNotAccepted = Template.bind({}); +GroupNotAccepted.args = { + conversationType: 'group', + groupDescription: casual.sentence, + membersCount: casual.integer(20, 100), + title: casual.title, + acceptedMessageRequest: false, +}; + export const NoteToSelf = Template.bind({}); NoteToSelf.args = { isMe: true, @@ -160,3 +179,26 @@ export const ReadStories = Template.bind({}); ReadStories.args = { hasStories: HasStories.Read, }; + +export const DirectNotFromTrustedContact = Template.bind({}); +DirectNotFromTrustedContact.args = { + conversationType: 'direct', + title: casual.full_name, + fromOrAddedByTrustedContact: false, +}; + +export const DirectWithNickname = Template.bind({}); +DirectWithNickname.args = { + conversationType: 'direct', + title: casual.full_name, + fromOrAddedByTrustedContact: false, + isDirectConvoAndHasNickname: true, +}; + +export const GroupNotFromTrustedContact = Template.bind({}); +GroupNotFromTrustedContact.args = { + conversationType: 'group', + title: casual.title, + membersCount: casual.integer(5, 20), + fromOrAddedByTrustedContact: false, +}; diff --git a/ts/components/conversation/ConversationHero.tsx b/ts/components/conversation/ConversationHero.tsx index b36131bfa5..0989c44d4c 100644 --- a/ts/components/conversation/ConversationHero.tsx +++ b/ts/components/conversation/ConversationHero.tsx @@ -1,7 +1,8 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useEffect, useState } from 'react'; +import React, { type ReactNode, useEffect, useState } from 'react'; +import classNames from 'classnames'; import type { Props as AvatarProps } from '../Avatar'; import { Avatar, AvatarSize, AvatarBlur } from '../Avatar'; import { ContactName } from './ContactName'; @@ -12,22 +13,24 @@ import type { LocalizerType, ThemeType } from '../../types/Util'; import type { HasStories } from '../../types/Stories'; import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories'; import { StoryViewModeType } from '../../types/Stories'; -import { ConfirmationDialog } from '../ConfirmationDialog'; import { shouldBlurAvatar } from '../../util/shouldBlurAvatar'; -import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser'; import { Button, ButtonVariant } from '../Button'; import { SafetyTipsModal } from '../SafetyTipsModal'; +import { I18n } from '../I18n'; export type Props = { about?: string; acceptedMessageRequest?: boolean; + fromOrAddedByTrustedContact?: boolean; groupDescription?: string; hasStories?: HasStories; id: string; i18n: LocalizerType; + isDirectConvoAndHasNickname?: boolean; isMe: boolean; isSignalConversation?: boolean; membersCount?: number; + openConversationDetails?: () => unknown; phoneNumber?: string; sharedGroupNames?: ReadonlyArray; unblurAvatar: (conversationId: string) => void; @@ -36,30 +39,39 @@ export type Props = { theme: ThemeType; viewUserStories: ViewUserStoriesActionCreatorType; toggleAboutContactModal: (conversationId: string) => unknown; + toggleProfileNameWarningModal: (conversationType?: string) => unknown; } & Omit; -const renderMembershipRow = ({ +const renderExtraInformation = ({ acceptedMessageRequest, conversationType, + fromOrAddedByTrustedContact, i18n, + isDirectConvoAndHasNickname, isMe, - onClickMessageRequestWarning, + membersCount, + onClickProfileNameWarning, onToggleSafetyTips, + openConversationDetails, phoneNumber, sharedGroupNames, }: Pick< Props, | 'acceptedMessageRequest' | 'conversationType' + | 'fromOrAddedByTrustedContact' | 'i18n' + | 'isDirectConvoAndHasNickname' | 'isMe' + | 'membersCount' + | 'openConversationDetails' | 'phoneNumber' > & Required> & { - onClickMessageRequestWarning: () => void; + onClickProfileNameWarning: () => void; onToggleSafetyTips: (showSafetyTips: boolean) => void; }) => { - if (conversationType !== 'direct') { + if (conversationType !== 'direct' && conversationType !== 'group') { return null; } @@ -71,7 +83,7 @@ const renderMembershipRow = ({ ); } - const safetyTipsButton = ( + const safetyTipsButton = !acceptedMessageRequest ? (
- ); + ) : null; - if (sharedGroupNames.length > 0) { - return ( -
+ const shouldShowReviewCarefully = + !acceptedMessageRequest && + (conversationType === 'group' || sharedGroupNames.length <= 1); + + const reviewCarefullyLabel = shouldShowReviewCarefully ? ( +
+ + {i18n('icu:ConversationHero--review-carefully')} +
+ ) : null; + + const sharedGroupsLabel = + conversationType === 'direct' ? ( +
- {safetyTipsButton}
- ); + ) : null; + + const nameNotVerifiedLabel = + !fromOrAddedByTrustedContact && !isDirectConvoAndHasNickname ? ( +
+ + ( + + ), + }} + i18n={i18n} + id={ + conversationType === 'group' + ? 'icu:ConversationHero--group-names' + : 'icu:ConversationHero--profile-names' + } + /> +
+ ) : null; + + const membersCountLabel = + conversationType === 'group' && membersCount != null ? ( +
+ + +
+ ) : null; + + if ( + conversationType === 'direct' && + sharedGroupNames.length === 0 && + acceptedMessageRequest && + phoneNumber + ) { + return null; } - if (acceptedMessageRequest) { - if (phoneNumber) { - return null; - } - return ( -
- {i18n('icu:no-groups-in-common')} - {safetyTipsButton} -
- ); + + // Check if we should show anything at all + const shouldShowAnything = + Boolean(reviewCarefullyLabel) || + Boolean(nameNotVerifiedLabel) || + Boolean(sharedGroupsLabel) || + Boolean(safetyTipsButton) || + Boolean(membersCountLabel); + + if (!shouldShowAnything) { + return null; } return (
-
- - {i18n('icu:no-groups-in-common-warning')} -   - -
+ {reviewCarefullyLabel} + {nameNotVerifiedLabel} + {sharedGroupsLabel} + {membersCountLabel} {safetyTipsButton}
); }; +function ReleaseNotesExtraInformation({ + i18n, +}: { + i18n: LocalizerType; +}): JSX.Element { + return ( +
+
+ + {i18n('icu:ConversationHero--signal-official-chat')} +
+
+ + {i18n('icu:ConversationHero--release-notes')} +
+
+ ); +} + export function ConversationHero({ i18n, about, @@ -140,10 +234,13 @@ export function ConversationHero({ badge, color, conversationType, + fromOrAddedByTrustedContact, groupDescription, hasStories, id, + isDirectConvoAndHasNickname, isMe, + openConversationDetails, isSignalConversation, membersCount, sharedGroupNames = [], @@ -156,13 +253,9 @@ export function ConversationHero({ updateSharedGroups, viewUserStories, toggleAboutContactModal, + toggleProfileNameWarningModal, }: Props): JSX.Element { const [isShowingSafetyTips, setIsShowingSafetyTips] = useState(false); - const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] = - useState(false); - const closeMessageRequestWarning = () => { - setIsShowingMessageRequestWarning(false); - }; useEffect(() => { // Kick off the expensive hydration of the current sharedGroupNames @@ -215,7 +308,6 @@ export function ConversationHero({ ); } - /* eslint-disable no-nested-ternary */ return ( <>
@@ -248,55 +340,36 @@ export function ConversationHero({
)} - {!isMe ? ( + {!isMe && groupDescription ? (
- {groupDescription ? ( - - ) : membersCount != null ? ( - i18n('icu:ConversationHero--members', { count: membersCount }) - ) : null} +
) : null} {!isSignalConversation && - renderMembershipRow({ + renderExtraInformation({ acceptedMessageRequest, conversationType, + fromOrAddedByTrustedContact, i18n, + isDirectConvoAndHasNickname, isMe, - onClickMessageRequestWarning() { - setIsShowingMessageRequestWarning(true); + membersCount, + onClickProfileNameWarning() { + toggleProfileNameWarningModal(conversationType); }, onToggleSafetyTips(showSafetyTips: boolean) { setIsShowingSafetyTips(showSafetyTips); }, + openConversationDetails, phoneNumber, sharedGroupNames, })} + {isSignalConversation && }
- {isShowingMessageRequestWarning && ( - { - openLinkInWebBrowser( - 'https://support.signal.org/hc/articles/360007459591' - ); - closeMessageRequestWarning(); - }, - }, - ]} - > - {i18n('icu:MessageRequestWarning__dialog__details')} - - )} {isShowingSafetyTips && ( ); - /* eslint-enable no-nested-ternary */ } diff --git a/ts/components/conversation/MessageRequestActions.tsx b/ts/components/conversation/MessageRequestActions.tsx index 2a5191bb01..974b826ea6 100644 --- a/ts/components/conversation/MessageRequestActions.tsx +++ b/ts/components/conversation/MessageRequestActions.tsx @@ -16,6 +16,7 @@ import { strictAssert } from '../../util/assert'; export type Props = { i18n: LocalizerType; isHidden: boolean | null; + sharedGroupNames?: ReadonlyArray; } & Omit< MessageRequestActionsConfirmationProps, 'i18n' | 'state' | 'onChangeState' @@ -30,6 +31,7 @@ export function MessageRequestActions({ isBlocked, isHidden, isReported, + sharedGroupNames = [], acceptConversation, blockAndReportSpam, blockConversation, @@ -153,7 +155,16 @@ export function MessageRequestActions({ )} {!isBlocked ? (
+ {isSignalConversation && ( + <> + + + } + label={i18n('icu:ConversationHero--signal-official-chat')} + /> + + } + label={i18n('icu:ConversationHero--release-notes')} + /> + + + + + } + label={i18n('icu:ConversationDetails--support-center')} + onClick={() => { + openLinkInWebBrowser( + getLocalizedUrl('https://support.signal.org/hc/LOCALE') + ); + }} + /> + + } + label={i18n('icu:contactUs')} + onClick={() => { + openLinkInWebBrowser( + getLocalizedUrl( + 'https://support.signal.org/hc/LOCALE/requests/new?desktop' + ) + ); + }} + /> + + } + label={i18n('icu:BadgeDialog__become-a-sustainer-button')} + onClick={() => setModalState(ModalState.BecomeSustainer)} + /> + + + )} + {callHistoryGroup && ( ; +export type ToggleProfileNameWarningModalActionType = ReadonlyDeep<{ + type: typeof TOGGLE_PROFILE_NAME_WARNING_MODAL; + payload?: { + conversationType: string; + }; +}>; + type ToggleSafetyNumberModalActionType = ReadonlyDeep<{ type: typeof TOGGLE_SAFETY_NUMBER_MODAL; payload: string | undefined; @@ -474,6 +485,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | ToggleNotePreviewModalActionType | ToggleProfileEditorActionType | ToggleProfileEditorErrorActionType + | ToggleProfileNameWarningModalActionType | ToggleSafetyNumberModalActionType | ToggleSignalConnectionsModalActionType | ToggleUsernameOnboardingActionType @@ -523,6 +535,7 @@ export const actions = { toggleNotePreviewModal, toggleProfileEditor, toggleProfileEditorHasError, + toggleProfileNameWarningModal, toggleSafetyNumberModal, toggleSignalConnectionsModal, toggleUsernameOnboarding, @@ -844,6 +857,15 @@ function toggleProfileEditorHasError(): ToggleProfileEditorErrorActionType { return { type: TOGGLE_PROFILE_EDITOR_ERROR }; } +function toggleProfileNameWarningModal( + conversationType?: string +): ToggleProfileNameWarningModalActionType { + return { + type: TOGGLE_PROFILE_NAME_WARNING_MODAL, + payload: conversationType ? { conversationType } : undefined, + }; +} + function toggleSafetyNumberModal( safetyNumberModalContactId?: string ): ToggleSafetyNumberModalActionType { @@ -1170,6 +1192,8 @@ export function getEmptyState(): GlobalModalsStateType { confirmLeaveCallModalState: null, editNicknameAndNoteModalProps: null, isProfileEditorVisible: false, + isProfileNameWarningModalVisible: false, + profileNameWarningModalConversationType: undefined, isShortcutGuideModalVisible: false, isSignalConnectionsVisible: false, isStoriesSettingsVisible: false, @@ -1222,6 +1246,16 @@ export function reducer( profileEditorHasError: !state.profileEditorHasError, }; } + if (action.type === TOGGLE_PROFILE_NAME_WARNING_MODAL) { + return { + ...state, + isProfileNameWarningModalVisible: !state.isProfileNameWarningModalVisible, + profileNameWarningModalConversationType: + state.isProfileNameWarningModalVisible + ? undefined + : action.payload?.conversationType, + }; + } if (action.type === SHOW_WHATS_NEW_MODAL) { return { diff --git a/ts/state/smart/AboutContactModal.tsx b/ts/state/smart/AboutContactModal.tsx index 90beb3cc77..cc4731f897 100644 --- a/ts/state/smart/AboutContactModal.tsx +++ b/ts/state/smart/AboutContactModal.tsx @@ -7,9 +7,28 @@ import { isSignalConnection } from '../../util/getSignalConnections'; import { getIntl } from '../selectors/user'; import { getGlobalModalsState } from '../selectors/globalModals'; import { getConversationSelector } from '../selectors/conversations'; +import type { ConversationType } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations'; import { useGlobalModalActions } from '../ducks/globalModals'; import { strictAssert } from '../../util/assert'; +import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation'; + +function isFromOrAddedByTrustedContact( + conversation: ConversationType +): boolean { + if (conversation.type === 'direct') { + return Boolean(conversation.name) || Boolean(conversation.profileSharing); + } + + const addedByConv = getAddedByForOurPendingInvitation(conversation); + if (!addedByConv) { + return false; + } + + return Boolean( + addedByConv.isMe || addedByConv.name || addedByConv.profileSharing + ); +} export const SmartAboutContactModal = memo(function SmartAboutContactModal() { const i18n = useSelector(getIntl); @@ -27,6 +46,7 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() { toggleSignalConnectionsModal, toggleSafetyNumberModal, toggleNotePreviewModal, + toggleProfileNameWarningModal, } = useGlobalModalActions(); const handleOpenNotePreviewModal = useCallback(() => { @@ -47,8 +67,10 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() { toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSafetyNumberModal={toggleSafetyNumberModal} isSignalConnection={isSignalConnection(conversation)} + fromOrAddedByTrustedContact={isFromOrAddedByTrustedContact(conversation)} onClose={toggleAboutContactModal} onOpenNotePreviewModal={handleOpenNotePreviewModal} + toggleProfileNameWarningModal={toggleProfileNameWarningModal} /> ); }); diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 9a7f323158..4119a1f537 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -336,6 +336,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ blockConversation={blockConversation} reportSpam={reportSpam} deleteConversation={deleteConversation} + sharedGroupNames={conversation.sharedGroupNames} // Signal Conversation isSignalConversation={isSignalConversation(conversation)} isMuted={isConversationMuted(conversation)} diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx index 7b73ec7f72..c5cffb55ab 100644 --- a/ts/state/smart/GlobalModalContainer.tsx +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -31,6 +31,7 @@ import { SmartCallLinkAddNameModal } from './CallLinkAddNameModal'; import { SmartConfirmLeaveCallModal } from './ConfirmLeaveCallModal'; import { SmartCallLinkPendingParticipantModal } from './CallLinkPendingParticipantModal'; import { SmartAttachmentNotAvailableModal } from './AttachmentNotAvailableModal'; +import { SmartProfileNameWarningModal } from './ProfileNameWarningModal'; function renderCallLinkAddNameModal(): JSX.Element { return ; @@ -60,6 +61,10 @@ function renderProfileEditor(): JSX.Element { return ; } +function renderProfileNameWarningModal(): JSX.Element { + return ; +} + function renderUsernameOnboarding(): JSX.Element { return ; } @@ -130,6 +135,8 @@ export const SmartGlobalModalContainer = memo( messageRequestActionsConfirmationProps, notePreviewModalProps, isProfileEditorVisible, + isProfileNameWarningModalVisible, + profileNameWarningModalConversationType, isShortcutGuideModalVisible, isSignalConnectionsVisible, isStoriesSettingsVisible, @@ -229,6 +236,7 @@ export const SmartGlobalModalContainer = memo( i18n={i18n} isAboutContactModalVisible={aboutContactModalContactId != null} isProfileEditorVisible={isProfileEditorVisible} + isProfileNameWarningModalVisible={isProfileNameWarningModalVisible} isShortcutGuideModalVisible={isShortcutGuideModalVisible} isSignalConnectionsVisible={isSignalConnectionsVisible} isStoriesSettingsVisible={isStoriesSettingsVisible} @@ -253,6 +261,7 @@ export const SmartGlobalModalContainer = memo( } renderNotePreviewModal={renderNotePreviewModal} renderProfileEditor={renderProfileEditor} + renderProfileNameWarningModal={renderProfileNameWarningModal} renderUsernameOnboarding={renderUsernameOnboarding} renderSafetyNumber={renderSafetyNumber} renderSendAnywayDialog={renderSendAnywayDialog} @@ -267,6 +276,9 @@ export const SmartGlobalModalContainer = memo( toggleSignalConnectionsModal={toggleSignalConnectionsModal} userNotFoundModalState={userNotFoundModalState} usernameOnboardingState={usernameOnboardingState} + profileNameWarningModalConversationType={ + profileNameWarningModalConversationType + } /> ); } diff --git a/ts/state/smart/HeroRow.tsx b/ts/state/smart/HeroRow.tsx index 6803a568b3..1fa3e1e751 100644 --- a/ts/state/smart/HeroRow.tsx +++ b/ts/state/smart/HeroRow.tsx @@ -1,21 +1,43 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; +import { PanelType } from '../../types/Panels'; import { ConversationHero } from '../../components/conversation/ConversationHero'; import { getPreferredBadgeSelector } from '../selectors/badges'; import { getIntl, getTheme } from '../selectors/user'; import { getHasStoriesSelector } from '../selectors/stories2'; import { isSignalConversation } from '../../util/isSignalConversation'; import { getConversationSelector } from '../selectors/conversations'; -import { useConversationsActions } from '../ducks/conversations'; +import { + type ConversationType, + useConversationsActions, +} from '../ducks/conversations'; import { useGlobalModalActions } from '../ducks/globalModals'; import { useStoriesActions } from '../ducks/stories'; +import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation'; type SmartHeroRowProps = Readonly<{ id: string; }>; +function isFromOrAddedByTrustedContact( + conversation: ConversationType +): boolean { + if (conversation.type === 'direct') { + return Boolean(conversation.name) || Boolean(conversation.profileSharing); + } + + const addedByConv = getAddedByForOurPendingInvitation(conversation); + if (!addedByConv) { + return false; + } + + return Boolean( + addedByConv.isMe || addedByConv.name || addedByConv.profileSharing + ); +} + export const SmartHeroRow = memo(function SmartHeroRow({ id, }: SmartHeroRowProps) { @@ -31,8 +53,15 @@ export const SmartHeroRow = memo(function SmartHeroRow({ const badge = getPreferredBadge(conversation.badges); const hasStories = hasStoriesSelector(id); const isSignalConversationValue = isSignalConversation(conversation); - const { unblurAvatar, updateSharedGroups } = useConversationsActions(); - const { toggleAboutContactModal } = useGlobalModalActions(); + const fromOrAddedByTrustedContact = + isFromOrAddedByTrustedContact(conversation); + const { pushPanelForConversation, unblurAvatar, updateSharedGroups } = + useConversationsActions(); + const { toggleAboutContactModal, toggleProfileNameWarningModal } = + useGlobalModalActions(); + const openConversationDetails = useCallback(() => { + pushPanelForConversation({ type: PanelType.ConversationDetails }); + }, [pushPanelForConversation]); const { viewUserStories } = useStoriesActions(); const { about, @@ -41,6 +70,8 @@ export const SmartHeroRow = memo(function SmartHeroRow({ groupDescription, isMe, membersCount, + nicknameGivenName, + nicknameFamilyName, phoneNumber, profileName, sharedGroupNames, @@ -48,6 +79,10 @@ export const SmartHeroRow = memo(function SmartHeroRow({ type, unblurredAvatarUrl, } = conversation; + + const isDirectConvoAndHasNickname = + type === 'direct' && Boolean(nicknameGivenName || nicknameFamilyName); + return ( + ); + } +); diff --git a/ts/test-mock/helpers.ts b/ts/test-mock/helpers.ts index f46f3e8d6c..bef40cff15 100644 --- a/ts/test-mock/helpers.ts +++ b/ts/test-mock/helpers.ts @@ -281,10 +281,22 @@ export async function pinContact( await phone.setStorageState(state); } -export function acceptConversation(page: Page): Promise { - return page +export async function acceptConversation(page: Page): Promise { + await page .locator('.module-message-request-actions button >> "Accept"') .click(); + + const confirmationButton = page + .locator('.MessageRequestActionsConfirmation') + .getByRole('button', { name: 'Accept' }); + + await confirmationButton.waitFor({ + timeout: 500, + }); + + if (await confirmationButton.isVisible()) { + await confirmationButton.click(); + } } export function getTimeline(page: Page): Locator { diff --git a/ts/test-mock/messaging/unknown_contact_test.ts b/ts/test-mock/messaging/unknown_contact_test.ts index 629b38565b..76c8dfe730 100644 --- a/ts/test-mock/messaging/unknown_contact_test.ts +++ b/ts/test-mock/messaging/unknown_contact_test.ts @@ -10,6 +10,7 @@ import assert from 'assert'; import * as durations from '../../util/durations'; import type { App } from '../playwright'; import { Bootstrap } from '../bootstrap'; +import { acceptConversation } from '../helpers'; export const debug = createDebug('mock:test:edit'); @@ -68,7 +69,7 @@ describe('unknown contacts', function (this: Mocha.Suite) { debug('accepting message request'); await page.getByText('message you and share your name').waitFor(); - await page.getByRole('button', { name: 'Accept' }).click(); + await acceptConversation(page); await page.getByText('message you and share your name').waitFor({ state: 'detached', }); diff --git a/ts/test-mock/pnp/accept_gv2_invite_test.ts b/ts/test-mock/pnp/accept_gv2_invite_test.ts index 5496a98c06..44c6b18d5d 100644 --- a/ts/test-mock/pnp/accept_gv2_invite_test.ts +++ b/ts/test-mock/pnp/accept_gv2_invite_test.ts @@ -13,7 +13,7 @@ import { } from '../../util/libphonenumberInstance'; import { Bootstrap } from '../bootstrap'; import type { App } from '../bootstrap'; -import { expectSystemMessages } from '../helpers'; +import { acceptConversation, expectSystemMessages } from '../helpers'; export const debug = createDebug('mock:test:gv2'); @@ -103,9 +103,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { const conversationStack = window.locator('.Inbox__conversation-stack'); debug('Accepting'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); group = await phone.waitForGroupUpdate(group); assert.strictEqual(group.revision, 2); @@ -256,9 +254,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { .waitFor(); debug('Accepting'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); debug('Checking final notification'); await window diff --git a/ts/test-mock/pnp/pni_signature_test.ts b/ts/test-mock/pnp/pni_signature_test.ts index efe913547d..2f457b5c67 100644 --- a/ts/test-mock/pnp/pni_signature_test.ts +++ b/ts/test-mock/pnp/pni_signature_test.ts @@ -24,6 +24,7 @@ import { } from '../../types/Receipt'; import { sleep } from '../../util/sleep'; import { + acceptConversation, expectSystemMessages, typeIntoInput, waitForEnabledComposer, @@ -85,7 +86,6 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) { const window = await app.getWindow(); const leftPane = window.locator('#LeftPane'); - const conversationStack = window.locator('.Inbox__conversation-stack'); debug('creating a stranger'); const stranger = await server.createPrimaryDevice({ @@ -137,9 +137,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) { .click(); debug('Accept conversation from a stranger'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); debug('Wait for a pniSignatureMessage'); { diff --git a/ts/test-mock/rate-limit/viewed_test.ts b/ts/test-mock/rate-limit/viewed_test.ts index e1b667fb0e..a81b07a941 100644 --- a/ts/test-mock/rate-limit/viewed_test.ts +++ b/ts/test-mock/rate-limit/viewed_test.ts @@ -10,7 +10,11 @@ import { Bootstrap } from '../bootstrap'; import type { App } from '../bootstrap'; import { ReceiptType } from '../../types/Receipt'; import { toUntaggedPni } from '../../types/ServiceId'; -import { typeIntoInput, waitForEnabledComposer } from '../helpers'; +import { + acceptConversation, + typeIntoInput, + waitForEnabledComposer, +} from '../helpers'; export const debug = createDebug('mock:test:challenge:receipts'); @@ -106,7 +110,6 @@ describe('challenge/receipts', function (this: Mocha.Suite) { const window = await app.getWindow(); const leftPane = window.locator('#LeftPane'); - const conversationStack = window.locator('.Inbox__conversation-stack'); debug(`Opening conversation with contact (${contact.toContact().aci})`); await leftPane @@ -114,9 +117,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) { .click(); debug('Accept conversation from contact - does not trigger captcha!'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); debug('Sending a message back to user - will trigger captcha!'); { @@ -164,7 +165,6 @@ describe('challenge/receipts', function (this: Mocha.Suite) { const window = await app.getWindow(); const leftPane = window.locator('#LeftPane'); - const conversationStack = window.locator('.Inbox__conversation-stack'); debug('Sending a message from ContactA'); const timestampA = bootstrap.getTimestamp(); @@ -178,9 +178,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) { .click(); debug('Accept conversation from ContactA - does not trigger captcha!'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); debug('Sending a message from ContactB'); const timestampB = bootstrap.getTimestamp(); @@ -194,9 +192,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) { .click(); debug('Accept conversation from ContactB - does not trigger captcha!'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); debug('Sending a message back to ContactB - will trigger captcha!'); { @@ -276,7 +272,6 @@ describe('challenge/receipts', function (this: Mocha.Suite) { const window = await app.getWindow(); const leftPane = window.locator('#LeftPane'); - const conversationStack = window.locator('.Inbox__conversation-stack'); debug(`Opening conversation with contact (${contact.toContact().aci})`); await leftPane @@ -284,9 +279,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) { .click(); debug('Accept conversation from contact - does not trigger captcha!'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); debug('Sending a message back to user - will trigger captcha!'); { @@ -355,9 +348,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) { .click(); debug('Accept conversation from Contact B - does not trigger captcha!'); - await conversationStack - .locator('.module-message-request-actions button >> "Accept"') - .click(); + await acceptConversation(window); debug( 'Sending to Contact B - we should not pop captcha because we are waiting!' diff --git a/ts/test-mock/storage/message_request_test.ts b/ts/test-mock/storage/message_request_test.ts index 5415072e4e..2c36861485 100644 --- a/ts/test-mock/storage/message_request_test.ts +++ b/ts/test-mock/storage/message_request_test.ts @@ -81,6 +81,11 @@ describe('storage service', function (this: Mocha.Suite) { .locator('.module-message-request-actions button >> "Accept"') .click(); + await window + .locator('.MessageRequestActionsConfirmation') + .getByRole('button', { name: 'Accept' }) + .click(); + debug('Verify that storage state was updated'); { const nextState = await phone.waitForStorageState({ diff --git a/ts/util/getLocalizedUrl.ts b/ts/util/getLocalizedUrl.ts new file mode 100644 index 0000000000..6c2c6771fa --- /dev/null +++ b/ts/util/getLocalizedUrl.ts @@ -0,0 +1,26 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { mapToSupportLocale } from './mapToSupportLocale'; + +/** + * Ensures the provided string contains "LOCALE". + * If not, produces a readable TypeScript error. + */ +type RequiresLocale = T extends `${string}LOCALE${string}` + ? T + : `Error: The URL must contain "LOCALE" but got "${T}"`; + +/** + * Replaces "LOCALE" in a URL with the appropriate localized support locale. + * + * @param url The URL string containing "LOCALE" to be replaced + * @returns The URL with "LOCALE" replaced with the appropriate locale + */ +export function getLocalizedUrl( + url: RequiresLocale +): string { + const locale = window.SignalContext.getResolvedMessagesLocale(); + const supportLocale = mapToSupportLocale(locale); + return url.replace('LOCALE', supportLocale); +}