From 7dc11c1928f47b759f8f77ced030f40fa6f1f2db Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:09:54 -0800 Subject: [PATCH] Username Education Co-authored-by: Jamie Kyle --- _locales/en/messages.json | 58 ++++++- images/icons/v2/lock_color_32.svg | 1 - images/icons/v2/number_color_32.svg | 1 - images/phone_40_color.svg | 7 + images/phone_40_color_dark.svg | 17 ++ images/qr-and-link.svg | 23 --- images/qr_codes_40_color.svg | 32 ++++ images/qr_codes_40_color_dark.svg | 30 ++++ images/usernames_40_color.svg | 5 + images/usernames_40_color_dark.svg | 4 + stylesheets/_variables.scss | 1 + stylesheets/components/Toast.scss | 20 +-- stylesheets/components/ToastManager.scss | 45 ++++++ stylesheets/components/UsernameMegaphone.scss | 110 +++++++++++++ .../components/UsernameOnboardingModal.scss | 125 +++++++++++++++ .../UsernameOnboardingModalBody.scss | 106 ------------- stylesheets/manifest.scss | 4 +- ts/background.ts | 12 +- ts/components/App.tsx | 23 --- ts/components/CallsTab.tsx | 6 + ts/components/CompositionArea.tsx | 1 + .../CompositionRecording.stories.tsx | 2 + ts/components/CompositionRecording.tsx | 26 ++- ts/components/DebugLogWindow.tsx | 57 ++++--- ts/components/GlobalModalContainer.tsx | 11 ++ ts/components/LeftPane.stories.tsx | 14 ++ ts/components/LeftPane.tsx | 5 + ts/components/MyStories.stories.tsx | 1 + ts/components/MyStories.tsx | 6 + ts/components/NavSidebar.tsx | 6 + ts/components/Preferences.tsx | 3 + ts/components/ProfileEditor.stories.tsx | 1 - ts/components/ProfileEditor.tsx | 26 +-- ts/components/ProfileEditorModal.tsx | 1 - ts/components/StoriesTab.stories.tsx | 1 + ts/components/StoriesTab.tsx | 7 + ts/components/Toast.tsx | 126 +++++---------- .../ToastAlreadyRequestedToJoin.stories.tsx | 26 --- ts/components/ToastAlreadyRequestedToJoin.tsx | 22 --- ts/components/ToastCaptchaFailed.stories.tsx | 25 --- ts/components/ToastCaptchaFailed.tsx | 15 -- ts/components/ToastCaptchaSolved.stories.tsx | 25 --- ts/components/ToastCaptchaSolved.tsx | 15 -- ts/components/ToastDebugLogError.stories.tsx | 25 --- ts/components/ToastDebugLogError.tsx | 15 -- .../ToastFailedToFetchPhoneNumber.tsx | 22 --- ts/components/ToastFailedToFetchUsername.tsx | 22 --- ts/components/ToastFileSize.stories.tsx | 25 --- ts/components/ToastFileSize.tsx | 29 ---- .../ToastGroupLinkCopied.stories.tsx | 25 --- ts/components/ToastGroupLinkCopied.tsx | 22 --- ts/components/ToastInternalError.stories.tsx | 45 ------ ts/components/ToastInternalError.tsx | 63 -------- ts/components/ToastLinkCopied.stories.tsx | 25 --- ts/components/ToastLinkCopied.tsx | 15 -- .../ToastLoadingFullLogs.stories.tsx | 25 --- ts/components/ToastLoadingFullLogs.tsx | 18 --- ts/components/ToastManager.stories.tsx | 62 +++++++- ts/components/ToastManager.tsx | 149 +++++++++++++++++- .../ToastStickerPackInstallFailed.stories.tsx | 25 --- .../ToastStickerPackInstallFailed.tsx | 22 --- ts/components/ToastVoiceNoteError.tsx | 15 -- ts/components/ToastVoiceNoteLimit.stories.tsx | 25 --- ts/components/ToastVoiceNoteLimit.tsx | 15 -- ...tVoiceNoteMustBeOnlyAttachment.stories.tsx | 25 --- .../ToastVoiceNoteMustBeOnlyAttachment.tsx | 20 --- ts/components/UsernameMegaphone.stories.tsx | 27 ++++ ts/components/UsernameMegaphone.tsx | 50 ++++++ ...sx => UsernameOnboardingModal.stories.tsx} | 12 +- ts/components/UsernameOnboardingModal.tsx | 80 ++++++++++ ts/components/UsernameOnboardingModalBody.tsx | 70 -------- ts/components/conversation/AudioCapture.tsx | 48 ++---- ts/state/ducks/app.ts | 5 + ts/state/ducks/crashReports.ts | 53 +++++-- ts/state/ducks/globalModals.ts | 37 ++++- ts/state/ducks/items.ts | 15 +- ts/state/ducks/toast.ts | 10 +- ts/state/ducks/username.ts | 6 + ts/state/selectors/globalModals.ts | 9 +- ts/state/smart/App.tsx | 112 +++++++------ ts/state/smart/CallsTab.tsx | 9 ++ ts/state/smart/ChatsTab.tsx | 9 +- ts/state/smart/CompositionRecording.tsx | 4 + ts/state/smart/GlobalModalContainer.tsx | 8 + ts/state/smart/InstallScreen.tsx | 12 +- ts/state/smart/LeftPane.tsx | 22 ++- ts/state/smart/ProfileEditorModal.tsx | 5 +- ts/state/smart/StoriesTab.tsx | 9 ++ ts/state/smart/ToastManager.tsx | 107 +++++++++++++ ts/state/smart/UsernameOnboardingModal.tsx | 43 +++++ ts/test-both/state/selectors/items_test.ts | 3 +- ts/test-mock/pnp/username_test.ts | 3 - ts/types/Megaphone.ts | 20 +++ ts/types/Toast.tsx | 30 ++++ ts/types/globalModals.ts | 8 + ts/util/copyGroupLink.ts | 5 +- ts/util/handleRetry.ts | 17 +- ts/util/lookupConversationWithoutServiceId.ts | 12 +- ts/util/processAttachment.ts | 8 +- ts/util/showToast.tsx | 58 ------- 100 files changed, 1443 insertions(+), 1269 deletions(-) delete mode 100644 images/icons/v2/lock_color_32.svg delete mode 100644 images/icons/v2/number_color_32.svg create mode 100644 images/phone_40_color.svg create mode 100644 images/phone_40_color_dark.svg delete mode 100644 images/qr-and-link.svg create mode 100644 images/qr_codes_40_color.svg create mode 100644 images/qr_codes_40_color_dark.svg create mode 100644 images/usernames_40_color.svg create mode 100644 images/usernames_40_color_dark.svg create mode 100644 stylesheets/components/ToastManager.scss create mode 100644 stylesheets/components/UsernameMegaphone.scss create mode 100644 stylesheets/components/UsernameOnboardingModal.scss delete mode 100644 stylesheets/components/UsernameOnboardingModalBody.scss delete mode 100644 ts/components/ToastAlreadyRequestedToJoin.stories.tsx delete mode 100644 ts/components/ToastAlreadyRequestedToJoin.tsx delete mode 100644 ts/components/ToastCaptchaFailed.stories.tsx delete mode 100644 ts/components/ToastCaptchaFailed.tsx delete mode 100644 ts/components/ToastCaptchaSolved.stories.tsx delete mode 100644 ts/components/ToastCaptchaSolved.tsx delete mode 100644 ts/components/ToastDebugLogError.stories.tsx delete mode 100644 ts/components/ToastDebugLogError.tsx delete mode 100644 ts/components/ToastFailedToFetchPhoneNumber.tsx delete mode 100644 ts/components/ToastFailedToFetchUsername.tsx delete mode 100644 ts/components/ToastFileSize.stories.tsx delete mode 100644 ts/components/ToastFileSize.tsx delete mode 100644 ts/components/ToastGroupLinkCopied.stories.tsx delete mode 100644 ts/components/ToastGroupLinkCopied.tsx delete mode 100644 ts/components/ToastInternalError.stories.tsx delete mode 100644 ts/components/ToastInternalError.tsx delete mode 100644 ts/components/ToastLinkCopied.stories.tsx delete mode 100644 ts/components/ToastLinkCopied.tsx delete mode 100644 ts/components/ToastLoadingFullLogs.stories.tsx delete mode 100644 ts/components/ToastLoadingFullLogs.tsx delete mode 100644 ts/components/ToastStickerPackInstallFailed.stories.tsx delete mode 100644 ts/components/ToastStickerPackInstallFailed.tsx delete mode 100644 ts/components/ToastVoiceNoteError.tsx delete mode 100644 ts/components/ToastVoiceNoteLimit.stories.tsx delete mode 100644 ts/components/ToastVoiceNoteLimit.tsx delete mode 100644 ts/components/ToastVoiceNoteMustBeOnlyAttachment.stories.tsx delete mode 100644 ts/components/ToastVoiceNoteMustBeOnlyAttachment.tsx create mode 100644 ts/components/UsernameMegaphone.stories.tsx create mode 100644 ts/components/UsernameMegaphone.tsx rename ts/components/{UsernameOnboardingModalBody.stories.tsx => UsernameOnboardingModal.stories.tsx} (65%) create mode 100644 ts/components/UsernameOnboardingModal.tsx delete mode 100644 ts/components/UsernameOnboardingModalBody.tsx create mode 100644 ts/state/smart/ToastManager.tsx create mode 100644 ts/state/smart/UsernameOnboardingModal.tsx create mode 100644 ts/types/Megaphone.ts create mode 100644 ts/types/globalModals.ts delete mode 100644 ts/util/showToast.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6e0a8aaa0..8c799df0e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -885,7 +885,7 @@ }, "icu:cdsMirroringErrorToast": { "messageformat": "Desktop ran into a Contact Discovery Service inconsistency.", - "description": "An error popup when we discovered an inconsistency between mirrored Contact Discovery Service requests." + "description": "(Deleted 2024/01/22) An error popup when we discovered an inconsistency between mirrored Contact Discovery Service requests." }, "icu:decryptionErrorToast": { "messageformat": "Desktop ran into a decryption error from {name}, device {deviceId}", @@ -6884,29 +6884,73 @@ "description": "Text of the confirmation dialog shown on username link error" }, "icu:UsernameOnboardingModalBody__title": { - "messageformat": "Set up your Signal username", + "messageformat": "New ways to connect", "description": "Title of username onboarding modal" }, + "icu:UsernameOnboardingModalBody__row__number__title": { + "messageformat": "Phone number privacy", + "description": "Title of the first row of username onboarding modal" + }, + "icu:UsernameOnboardingModalBody__row__number__body": { + "messageformat": "Your phone number is no longer shared with chats. If your number is saved to a friend’s contacts, they will still see it.", + "description": "Body of the first row of username onboarding modal" + }, + "icu:UsernameOnboardingModalBody__row__username__title": { + "messageformat": "Usernames", + "description": "Title of the second row of username onboarding modal" + }, + "icu:UsernameOnboardingModalBody__row__username__body": { + "messageformat": "People can now message you using your optional username. Phone numbers are no longer required and usernames aren’t visible on your profile.", + "description": "Body of the second row of username onboarding modal" + }, + "icu:UsernameOnboardingModalBody__row__qr__title": { + "messageformat": "QR codes and links", + "description": "Title of the third row of username onboarding modal" + }, + "icu:UsernameOnboardingModalBody__row__qr__body": { + "messageformat": "Usernames have a unique QR code and link you can share with friends to quickly start a chat with you.", + "description": "Body of the third row of username onboarding modal" + }, "icu:UsernameOnboardingModalBody__row__number": { "messageformat": "Usernames are paired with a set of digits and aren’t shared on your profile", - "description": "Content of the first row of username onboarding modal" + "description": "(Deleted 01/16/2023) Content of the first row of username onboarding modal" }, "icu:UsernameOnboardingModalBody__row__link": { "messageformat": "Each username has a unique QR code and link you can share with friends to start a chat with you", - "description": "Content of the second row of username onboarding modal" + "description": "(Deleted 01/16/2023) Content of the second row of username onboarding modal" }, "icu:UsernameOnboardingModalBody__row__lock": { "messageformat": "Turn off phone number discovery under Settings > Privacy > Phone Number > Who can find my number, to use your username as the primary way others can contact you.", - "description": "Content of the third row of username onboarding modal" + "description": "(Deleted 01/16/2023) Content of the third row of username onboarding modal" }, "icu:UsernameOnboardingModalBody__learn-more": { "messageformat": "Learn More", - "description": "Text that open a popup with information about username onboarding" + "description": "(Deleted 01/16/2023) Text that open a popup with information about username onboarding" }, "icu:UsernameOnboardingModalBody__continue": { - "messageformat": "Continue", + "messageformat": "Set up your Username", "description": "Text of the primary button on username onboarding modal" }, + "icu:UsernameOnboardingModalBody__skip": { + "messageformat": "Not Now", + "description": "Text of the secondary button on username onboarding modal" + }, + "icu:UsernameMegaphone__title": { + "messageformat": "New ways to connect", + "description": "Title of username megaphone" + }, + "icu:UsernameMegaphone__body": { + "messageformat": "Introducing phone number privacy, optional usernames and links.", + "description": "Body of username megaphone" + }, + "icu:UsernameMegaphone__learn-more": { + "messageformat": "Learn more", + "description": "Text of the primary button on username megaphone" + }, + "icu:UsernameMegaphone__dismiss": { + "messageformat": "Dismiss", + "description": "Text of the secondary button on username megaphone" + }, "icu:UnsupportedOSWarningDialog__body": { "messageformat": "Signal desktop will no longer support your computer’s version of {OS} soon. To keep using Signal, update your computer’s operating system by {expirationDate}. Learn more", "description": "Body of a dialog displayed on unsupported operating systems" diff --git a/images/icons/v2/lock_color_32.svg b/images/icons/v2/lock_color_32.svg deleted file mode 100644 index e76d0b2bd..000000000 --- a/images/icons/v2/lock_color_32.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/icons/v2/number_color_32.svg b/images/icons/v2/number_color_32.svg deleted file mode 100644 index 6772dd224..000000000 --- a/images/icons/v2/number_color_32.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/phone_40_color.svg b/images/phone_40_color.svg new file mode 100644 index 000000000..9e2700e2a --- /dev/null +++ b/images/phone_40_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/images/phone_40_color_dark.svg b/images/phone_40_color_dark.svg new file mode 100644 index 000000000..30f3a3f6b --- /dev/null +++ b/images/phone_40_color_dark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/images/qr-and-link.svg b/images/qr-and-link.svg deleted file mode 100644 index 8ee17d066..000000000 --- a/images/qr-and-link.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/images/qr_codes_40_color.svg b/images/qr_codes_40_color.svg new file mode 100644 index 000000000..a660b384b --- /dev/null +++ b/images/qr_codes_40_color.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/qr_codes_40_color_dark.svg b/images/qr_codes_40_color_dark.svg new file mode 100644 index 000000000..1cad44a0b --- /dev/null +++ b/images/qr_codes_40_color_dark.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/usernames_40_color.svg b/images/usernames_40_color.svg new file mode 100644 index 000000000..710e7915d --- /dev/null +++ b/images/usernames_40_color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/usernames_40_color_dark.svg b/images/usernames_40_color_dark.svg new file mode 100644 index 000000000..842d05b0c --- /dev/null +++ b/images/usernames_40_color_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index 310ca2293..08469cd58 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -265,6 +265,7 @@ $z-index-negative: -1; $z-index-base: 1; $z-index-above-base: 2; $z-index-above-above-base: 3; +$z-index-megaphone: 75; $z-index-popup-overlay: 99; $z-index-popup: 100; $z-index-context-menu: 125; diff --git a/stylesheets/components/Toast.scss b/stylesheets/components/Toast.scss index b82ceaf2d..6a00e0140 100644 --- a/stylesheets/components/Toast.scss +++ b/stylesheets/components/Toast.scss @@ -6,7 +6,6 @@ @include font-body-2; - position: absolute; align-items: stretch; border-radius: $border-radius-px; box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.05), 0px 4px 12px rgba(0, 0, 0, 0.3); @@ -14,11 +13,14 @@ justify-content: space-between; user-select: none; overflow: hidden; - z-index: $z-index-toast; - bottom: 62px; + width: fit-content; max-width: 280px; background-color: $color-gray-75; color: $color-gray-05; + z-index: $z-index-toast; + + inset-inline-start: 20px; + text-align: start; &:focus { outline: none; @@ -27,15 +29,13 @@ } } - &--align-center { - @include position-absolute-center-x; - text-align: center; + .ToastManager--narrow-sidebar & { + min-width: max-content; } - &--align-left { - inset-inline-start: 20px; - bottom: 18px; - text-align: start; + .ToastManager--narrow-sidebar.ToastManager--composition-area-visible & { + min-width: initial; + text-align: center; } &__content { diff --git a/stylesheets/components/ToastManager.scss b/stylesheets/components/ToastManager.scss new file mode 100644 index 000000000..376de4045 --- /dev/null +++ b/stylesheets/components/ToastManager.scss @@ -0,0 +1,45 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.ToastManager { + display: flex; + flex-direction: column; + pointer-events: none; + + gap: 16px; + + // Sync inner width with left pane + position: fixed; + width: inherit; + bottom: 0; + z-index: $z-index-toast; + + padding: 16px; + + & * { + pointer-events: auto; + } + + // Separate container used when modals are on screen + &__root { + display: flex; + flex-direction: column; + + position: fixed; + bottom: 16px; + z-index: $z-index-toast; + + inset-inline-start: 0; + width: 100%; + align-items: center; + } +} + +.ToastManager--narrow-sidebar.ToastManager--composition-area-visible { + inset-inline-start: 0; + width: 100%; + align-items: center; + + // Roughly size of composer + a bit of padding + bottom: 40px; +} diff --git a/stylesheets/components/UsernameMegaphone.scss b/stylesheets/components/UsernameMegaphone.scss new file mode 100644 index 000000000..4af15fba3 --- /dev/null +++ b/stylesheets/components/UsernameMegaphone.scss @@ -0,0 +1,110 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.UsernameMegaphone { + @include font-body-2; + + border-radius: 10px; + box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.08); + display: flex; + flex-direction: column; + + user-select: none; + z-index: $z-index-megaphone; + padding-block: 12px; + padding-inline: 8px; + + @include light-theme { + background-color: $color-white; + border: 1px solid $color-gray-20; + } + @include dark-theme { + background: $color-gray-75; + border: 1px solid $color-gray-60; + } + + &__row { + display: flex; + flex-direction: row; + margin-top: 4px; + margin-bottom: 12px; + gap: 12px; + } + + &__row__icon { + flex-shrink: 0; + width: 40px; + height: 40px; + margin-bottom: 2px; + margin-inline-start: 8px; + + background-size: cover; + @include light-theme { + background-image: url(../images/usernames_40_color.svg); + } + + @include dark-theme { + background-image: url(../images/usernames_40_color_dark.svg); + } + } + + &__row__text { + @include font-body-2; + + h2 { + @include font-body-2-bold; + margin: 0; + line-height: 20px; + } + + p { + margin: 0; + + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } + } + } + + &__buttons { + display: flex; + flex-direction: row; + justify-content: end; + gap: 12px; + } + + &__buttons__button { + margin-bottom: 0; + font-weight: 600; + padding-block: 5px; + + @include light-theme { + background: transparent; + + &:hover, + &:active { + @include not-disabled { + background: transparent; + } + } + } + + @include dark-theme { + background: transparent; + + &:hover, + &:active { + @include not-disabled { + background: transparent; + } + } + } + } + + .NavSidebar--narrow & { + display: none; + } +} diff --git a/stylesheets/components/UsernameOnboardingModal.scss b/stylesheets/components/UsernameOnboardingModal.scss new file mode 100644 index 000000000..124acae01 --- /dev/null +++ b/stylesheets/components/UsernameOnboardingModal.scss @@ -0,0 +1,125 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.UsernameOnboardingModal { + display: flex; + flex-direction: column; + align-items: center; + user-select: none; + + &__title { + @include font-title-2; + margin-bottom: 20px; + max-width: 240px; + text-align: center; + } + + &__row { + display: flex; + gap: 24px; + margin-bottom: 32px; + + &__icon { + flex-shrink: 0; + width: 40px; + height: 40px; + background-size: cover; + + &--number { + @include light-theme { + background-image: url(../images/phone_40_color.svg); + } + + @include dark-theme { + background-image: url(../images/phone_40_color_dark.svg); + } + } + + &--username { + @include light-theme { + background-image: url(../images/usernames_40_color.svg); + } + + @include dark-theme { + background-image: url(../images/usernames_40_color_dark.svg); + } + } + + &--qr { + @include light-theme { + background-image: url(../images/qr_codes_40_color.svg); + } + + @include dark-theme { + background-image: url(../images/qr_codes_40_color_dark.svg); + } + } + } + + &__body { + @include font-body-2; + + @include light-theme { + color: $color-gray-60; + } + + @include dark-theme { + color: $color-gray-25; + } + + max-width: 248px; + + h2 { + @include font-body-1; + + margin-top: 0; + margin-bottom: 2px; + font-weight: 400; + + @include light-theme { + color: $color-gray-90; + } + @include dark-theme { + color: $color-gray-05; + } + } + } + + &--center { + justify-content: center; + } + } + + &__submit { + width: 100%; + max-width: 296px; + margin-top: 16px; + margin-bottom: 12px; + } + + &__skip { + margin-bottom: 0; + + @include light-theme { + background: transparent; + + &:hover, + &:active { + @include not-disabled { + background: transparent; + } + } + } + + @include dark-theme { + background: transparent; + + &:hover, + &:active { + @include not-disabled { + background: transparent; + } + } + } + } +} diff --git a/stylesheets/components/UsernameOnboardingModalBody.scss b/stylesheets/components/UsernameOnboardingModalBody.scss deleted file mode 100644 index 068f80b9a..000000000 --- a/stylesheets/components/UsernameOnboardingModalBody.scss +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2023 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -.UsernameOnboardingModalBody { - display: flex; - flex-direction: column; - align-items: center; - user-select: none; - - &__large-at { - display: flex; - align-items: center; - justify-content: center; - - width: 48px; - height: 48px; - border-radius: 24px; - - margin-bottom: 12px; - - @include light-theme { - background-color: $color-gray-04; - } - - @include dark-theme { - background-color: $color-gray-65; - } - - &::after { - display: block; - width: 28px; - height: 28px; - content: ''; - - @include light-theme { - @include color-svg('../images/icons/v3/at/at.svg', $color-gray-75); - } - - @include dark-theme { - @include color-svg('../images/icons/v3/at/at.svg', $color-gray-15); - } - } - } - - &__title { - @include font-title-2; - margin-bottom: 20px; - max-width: 240px; - text-align: center; - } - - &__row { - display: flex; - gap: 16px; - margin-bottom: 24px; - - &__icon { - flex-shrink: 0; - width: 32px; - height: 32px; - - &--number { - background: url(../images/icons/v2/number_color_32.svg); - } - - &--link { - width: 32px; - height: 34px; - background: url(../images/qr-and-link.svg); - } - - &--lock { - background: url(../images/icons/v2/lock_color_32.svg); - } - } - - &__body { - @include font-body-2; - - @include light-theme { - color: $color-gray-60; - } - - @include dark-theme { - color: $color-gray-25; - } - - max-width: 248px; - } - - &--center { - justify-content: center; - } - } - - &__learn-more { - text-decoration: none; - font-weight: 600; - } - - &__submit { - width: 100%; - max-width: 296px; - margin-bottom: 16px; - } -} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 59e17ea4f..ced286738 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -155,8 +155,10 @@ @import './components/TimelineWarning.scss'; @import './components/TimelineWarnings.scss'; @import './components/Toast.scss'; +@import './components/ToastManager.scss'; @import './components/Waveform.scss'; @import './components/WaveformScrubber.scss'; @import './components/UsernameLinkModalBody.scss'; -@import './components/UsernameOnboardingModalBody.scss'; +@import './components/UsernameMegaphone.scss'; +@import './components/UsernameOnboardingModal.scss'; @import './components/WhatsNew.scss'; diff --git a/ts/background.ts b/ts/background.ts index 92682cd2e..129eaa276 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -29,6 +29,7 @@ import { ReceiptType } from './types/Receipt'; import { SocketStatus } from './types/SocketStatus'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; import { ThemeType } from './types/Util'; +import { ToastType } from './types/Toast'; import { ChallengeHandler } from './challenge'; import * as durations from './util/durations'; import { drop } from './util/drop'; @@ -143,9 +144,6 @@ import { normalizeAci } from './util/normalizeAci'; import * as log from './logging/log'; import { loadRecentEmojis } from './util/loadRecentEmojis'; import { deleteAllLogs } from './util/deleteAllLogs'; -import { ToastCaptchaFailed } from './components/ToastCaptchaFailed'; -import { ToastCaptchaSolved } from './components/ToastCaptchaSolved'; -import { showToast } from './util/showToast'; import { startInteractionMode } from './services/InteractionMode'; import type { MainWindowStatsType } from './windows/context'; import { ReactionSource } from './reactions/ReactionSource'; @@ -278,11 +276,15 @@ export async function startApp(): Promise { onChallengeFailed() { // TODO: DESKTOP-1530 // Display humanized `retryAfter` - showToast(ToastCaptchaFailed); + window.reduxActions.toast.showToast({ + toastType: ToastType.CaptchaFailed, + }); }, onChallengeSolved() { - showToast(ToastCaptchaSolved); + window.reduxActions.toast.showToast({ + toastType: ToastType.CaptchaSolved, + }); }, setChallengeStatus(challengeStatus) { diff --git a/ts/components/App.tsx b/ts/components/App.tsx index 29251de2c..5c0382b95 100644 --- a/ts/components/App.tsx +++ b/ts/components/App.tsx @@ -5,15 +5,12 @@ import React, { useEffect } from 'react'; import { Globals } from '@react-spring/web'; import classNames from 'classnames'; -import type { AnyToast } from '../types/Toast'; import type { ViewStoryActionCreatorType } from '../state/ducks/stories'; -import type { LocalizerType } from '../types/Util'; import type { VerificationTransport } from '../types/VerificationTransport'; import { ThemeType } from '../types/Util'; import { AppViewType } from '../state/ducks/app'; import { SmartInstallScreen } from '../state/smart/InstallScreen'; import { StandaloneRegistration } from './StandaloneRegistration'; -import { ToastManager } from './ToastManager'; import { usePageVisibility } from '../hooks/usePageVisibility'; import { useReducedMotion } from '../hooks/useReducedMotion'; @@ -27,7 +24,6 @@ type PropsType = { ) => Promise; renderCallManager: () => JSX.Element; renderGlobalModalContainer: () => JSX.Element; - i18n: LocalizerType; hasSelectedStoryData: boolean; renderStoryViewer: (closeView: () => unknown) => JSX.Element; renderLightbox: () => JSX.Element | null; @@ -39,13 +35,8 @@ type PropsType = { theme: ThemeType; isMaximized: boolean; isFullScreen: boolean; - onUndoArchive: (conversationId: string) => unknown; - openFileInFolder: (target: string) => unknown; - OS: string; osClassName: string; - hideToast: () => unknown; - toast?: AnyToast; scrollToMessage: (conversationId: string, messageId: string) => unknown; viewStory: ViewStoryActionCreatorType; renderInbox: () => JSX.Element; @@ -54,14 +45,9 @@ type PropsType = { export function App({ appView, hasSelectedStoryData, - hideToast, - i18n, isFullScreen, isMaximized, - onUndoArchive, - openFileInFolder, openInbox, - OS, osClassName, registerSingleDevice, renderCallManager, @@ -71,7 +57,6 @@ export function App({ renderStoryViewer, requestVerification, theme, - toast, viewStory, }: PropsType): JSX.Element { let contents; @@ -139,14 +124,6 @@ export function App({ })} > {contents} - {renderGlobalModalContainer()} {renderCallManager()} {renderLightbox()} diff --git a/ts/components/CallsTab.tsx b/ts/components/CallsTab.tsx index 37bdadea7..861606dab 100644 --- a/ts/components/CallsTab.tsx +++ b/ts/components/CallsTab.tsx @@ -17,6 +17,7 @@ import type { ActiveCallStateType } from '../state/ducks/calling'; import { ContextMenu } from './ContextMenu'; import { ConfirmationDialog } from './ConfirmationDialog'; import type { UnreadStats } from '../util/countUnreadStats'; +import type { WidthBreakpoint } from './_util'; enum CallsTabSidebarView { CallsListView, @@ -50,6 +51,9 @@ type CallsTabProps = Readonly<{ conversationId: string, callHistoryGroup: CallHistoryGroup | null ) => JSX.Element; + renderToastManager: (_: { + containerWidthBreakpoint: WidthBreakpoint; + }) => JSX.Element; regionCode: string | undefined; savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void; }>; @@ -73,6 +77,7 @@ export function CallsTab({ onOutgoingVideoCallInConversation, preferredLeftPaneWidth, renderConversationDetails, + renderToastManager, regionCode, savePreferredLeftPaneWidth, }: CallsTabProps): JSX.Element { @@ -175,6 +180,7 @@ export function CallsTab({ requiresFullWidth preferredLeftPaneWidth={preferredLeftPaneWidth} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} + renderToastManager={renderToastManager} actions={ <> {sidebarView === CallsTabSidebarView.CallsListView && ( diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 52c4c7ede..0e80d03f1 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -563,6 +563,7 @@ export function CompositionArea({ conversationId={conversationId} draftAttachments={draftAttachments} i18n={i18n} + showToast={showToast} startRecording={startRecording} /> diff --git a/ts/components/CompositionRecording.stories.tsx b/ts/components/CompositionRecording.stories.tsx index 7619ccb93..52b7a8f1c 100644 --- a/ts/components/CompositionRecording.stories.tsx +++ b/ts/components/CompositionRecording.stories.tsx @@ -51,6 +51,8 @@ export function Default(): JSX.Element { errorRecording={_ => action('error')()} addAttachment={action('addAttachment')} completeRecording={action('completeRecording')} + showToast={action('showToast')} + hideToast={action('hideToast')} /> )} diff --git a/ts/components/CompositionRecording.tsx b/ts/components/CompositionRecording.tsx index ec9a6dfa7..f4a6e97d5 100644 --- a/ts/components/CompositionRecording.tsx +++ b/ts/components/CompositionRecording.tsx @@ -5,14 +5,16 @@ import { noop } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { usePrevious } from '../hooks/usePrevious'; +import type { HideToastAction, ShowToastAction } from '../state/ducks/toast'; import type { InMemoryAttachmentDraftType } from '../types/Attachment'; import { ErrorDialogAudioRecorderType } from '../types/AudioRecorder'; import type { LocalizerType } from '../types/Util'; +import type { AnyToast } from '../types/Toast'; +import { ToastType } from '../types/Toast'; import { DurationInSeconds, SECOND } from '../util/durations'; import { durationToPlaybackText } from '../util/durationToPlaybackText'; import { ConfirmationDialog } from './ConfirmationDialog'; import { RecordingComposer } from './RecordingComposer'; -import { ToastVoiceNoteLimit } from './ToastVoiceNoteLimit'; export type Props = { i18n: LocalizerType; @@ -29,6 +31,8 @@ export type Props = { conversationId: string, onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown ) => unknown; + showToast: ShowToastAction; + hideToast: HideToastAction; }; export function CompositionRecording({ @@ -40,11 +44,11 @@ export function CompositionRecording({ errorDialogAudioRecorderType, addAttachment, completeRecording, + showToast, + hideToast, }: Props): JSX.Element { useEscapeHandling(onCancel); - const [showVoiceNoteLimitToast, setShowVoiceNoteLimitToast] = useState(true); - // when interrupted (blur, switching convos) // stop recording and save draft const handleRecordingInterruption = useCallback(() => { @@ -69,15 +73,12 @@ export function CompositionRecording({ } }); - const handleCloseToast = useCallback(() => { - setShowVoiceNoteLimitToast(false); - }, []); - useEffect(() => { - return () => { - handleCloseToast(); - }; - }, [handleCloseToast]); + const toast: AnyToast = { toastType: ToastType.VoiceNoteLimit }; + showToast(toast); + + return () => hideToast(toast); + }, [showToast, hideToast]); const startTime = useRef(Date.now()); const [duration, setDuration] = useState(0); @@ -148,9 +149,6 @@ export function CompositionRecording({ {confirmationDialog} - {showVoiceNoteLimitToast && ( - - )} ); } diff --git a/ts/components/DebugLogWindow.tsx b/ts/components/DebugLogWindow.tsx index 0476109d2..ed0624b89 100644 --- a/ts/components/DebugLogWindow.tsx +++ b/ts/components/DebugLogWindow.tsx @@ -6,13 +6,15 @@ import React, { useEffect, useState } from 'react'; import copyText from 'copy-text-to-clipboard'; import type { LocalizerType } from '../types/Util'; import * as Errors from '../types/errors'; +import type { AnyToast } from '../types/Toast'; +import { ToastType } from '../types/Toast'; import * as log from '../logging/log'; import { Button, ButtonVariant } from './Button'; import { Spinner } from './Spinner'; -import { ToastDebugLogError } from './ToastDebugLogError'; -import { ToastLinkCopied } from './ToastLinkCopied'; -import { ToastLoadingFullLogs } from './ToastLoadingFullLogs'; +import { ToastManager } from './ToastManager'; +import { WidthBreakpoint } from './_util'; import { createSupportUrl } from '../util/createSupportUrl'; +import { shouldNeverBeCalled } from '../util/shouldNeverBeCalled'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; @@ -31,12 +33,6 @@ export type PropsType = { uploadLogs: (logs: string) => Promise; }; -enum ToastType { - Copied, - Error, - Loading, -} - export function DebugLogWindow({ closeWindow, downloadLog, @@ -50,7 +46,7 @@ export function DebugLogWindow({ const [textAreaValue, setTextAreaValue] = useState( i18n('icu:loading') ); - const [toastType, setToastType] = useState(); + const [toast, setToast] = useState(); useEscapeHandling(closeWindow); @@ -66,7 +62,7 @@ export function DebugLogWindow({ return; } - setToastType(ToastType.Loading); + setToast({ toastType: ToastType.LoadingFullLogs }); setLogText(fetchedLogText); setLoadState(LoadState.Loaded); @@ -76,7 +72,7 @@ export function DebugLogWindow({ const value = fetchedLogText.split(/\n/g, linesToShow).join('\n'); setTextAreaValue(`${value}\n\n\n${i18n('icu:debugLogLogIsIncomplete')}`); - setToastType(undefined); + setToast(undefined); } void doFetchLogs(); @@ -103,28 +99,19 @@ export function DebugLogWindow({ } catch (error) { log.error('DebugLogWindow error:', Errors.toLogFormat(error)); setLoadState(LoadState.Loaded); - setToastType(ToastType.Error); + setToast({ toastType: ToastType.DebugLogError }); } }; function closeToast() { - setToastType(undefined); - } - - let toastElement: JSX.Element | undefined; - if (toastType === ToastType.Loading) { - toastElement = ; - } else if (toastType === ToastType.Copied) { - toastElement = ; - } else if (toastType === ToastType.Error) { - toastElement = ; + setToast(undefined); } if (publicLogURL) { const copyLog = (ev: MouseEvent) => { ev.preventDefault(); copyText(publicLogURL); - setToastType(ToastType.Copied); + setToast({ toastType: ToastType.LinkCopied }); }; const supportURL = createSupportUrl({ @@ -162,7 +149,16 @@ export function DebugLogWindow({ - {toastElement} + ); } @@ -209,7 +205,16 @@ export function DebugLogWindow({ {i18n('icu:submit')} - {toastElement} + ); } diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index aa4977457..95cf13216 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -14,6 +14,7 @@ import type { UserNotFoundModalStateType, } from '../state/ducks/globalModals'; import type { LocalizerType, ThemeType } from '../types/Util'; +import { UsernameOnboardingState } from '../types/globalModals'; import type { ExplodePromiseResultType } from '../util/explodePromise'; import { missingCaseError } from '../util/missingCaseError'; @@ -90,6 +91,9 @@ export type PropsType = { // WhatsNewModal isWhatsNewVisible: boolean; hideWhatsNewModal: () => unknown; + // UsernameOnboarding + usernameOnboardingState: UsernameOnboardingState; + renderUsernameOnboarding: () => JSX.Element; // AuthArtCreatorModal authArtCreatorData?: AuthorizeArtCreatorDataType; isAuthorizingArtCreator?: boolean; @@ -151,6 +155,9 @@ export function GlobalModalContainer({ // WhatsNewModal hideWhatsNewModal, isWhatsNewVisible, + // UsernameOnboarding + usernameOnboardingState, + renderUsernameOnboarding, // AuthArtCreatorModal authArtCreatorData, isAuthorizingArtCreator, @@ -253,6 +260,10 @@ export function GlobalModalContainer({ return ; } + if (usernameOnboardingState === UsernameOnboardingState.Open) { + return renderUsernameOnboarding(); + } + if (safetyNumberModalContactId) { return renderSafetyNumber(); } diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index 439f5bac0..d8f58e4f6 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -8,6 +8,7 @@ import type { PropsType } from './LeftPane'; import { LeftPane, LeftPaneMode } from './LeftPane'; import { CaptchaDialog } from './CaptchaDialog'; import { CrashReportDialog } from './CrashReportDialog'; +import { ToastManager } from './ToastManager'; import type { PropsType as DialogNetworkStatusPropsType } from './DialogNetworkStatus'; import { DialogExpiredBuild } from './DialogExpiredBuild'; import { DialogNetworkStatus } from './DialogNetworkStatus'; @@ -251,6 +252,19 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { {...props} /> ), + renderToastManager: ({ containerWidthBreakpoint }) => ( + + ), selectedConversationId: undefined, targetedMessageId: undefined, savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'), diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index a319aded0..5f00f7b82 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -158,6 +158,9 @@ export type PropsType = { renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element; renderCrashReportDialog: () => JSX.Element; renderExpiredBuildDialog: (_: DialogExpiredBuildPropsType) => JSX.Element; + renderToastManager: (_: { + containerWidthBreakpoint: WidthBreakpoint; + }) => JSX.Element; } & LookupConversationWithoutServiceIdActionsType; export function LeftPane({ @@ -200,6 +203,7 @@ export function LeftPane({ renderUnsupportedOSDialog, renderRelinkDialog, renderUpdateDialog, + renderToastManager, savePreferredLeftPaneWidth, searchInConversation, selectedConversationId, @@ -597,6 +601,7 @@ export function LeftPane({ modeSpecificProps.isAboutToSearch } savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} + renderToastManager={renderToastManager} actions={ <> , queueStoryDownload: action('queueStoryDownload'), retryMessageSend: action('retryMessageSend'), viewStory: action('viewStory'), diff --git a/ts/components/MyStories.tsx b/ts/components/MyStories.tsx index 51c94e64d..4535a4905 100644 --- a/ts/components/MyStories.tsx +++ b/ts/components/MyStories.tsx @@ -20,6 +20,7 @@ import { Theme } from '../util/theme'; import { resolveStorySendStatus } from '../util/resolveStorySendStatus'; import { useRetryStorySend } from '../hooks/useRetryStorySend'; import { NavSidebar } from './NavSidebar'; +import type { WidthBreakpoint } from './_util'; import type { UnreadStats } from '../util/countUnreadStats'; export type PropsType = { @@ -40,6 +41,9 @@ export type PropsType = { viewStory: ViewStoryActionCreatorType; hasViewReceiptSetting: boolean; preferredLeftPaneWidth: number; + renderToastManager: (_: { + containerWidthBreakpoint: WidthBreakpoint; + }) => JSX.Element; savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void; theme: ThemeType; }; @@ -62,6 +66,7 @@ export function MyStories({ onMediaPlaybackStart, onToggleNavTabsCollapse, preferredLeftPaneWidth, + renderToastManager, savePreferredLeftPaneWidth, theme, }: PropsType): JSX.Element { @@ -98,6 +103,7 @@ export function MyStories({ onToggleNavTabsCollapse={onToggleNavTabsCollapse} preferredLeftPaneWidth={preferredLeftPaneWidth} requiresFullWidth + renderToastManager={renderToastManager} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} >
diff --git a/ts/components/NavSidebar.tsx b/ts/components/NavSidebar.tsx index 8f69609da..1e83516a4 100644 --- a/ts/components/NavSidebar.tsx +++ b/ts/components/NavSidebar.tsx @@ -55,6 +55,9 @@ export type NavSidebarProps = Readonly<{ savePreferredLeftPaneWidth: (width: number) => void; title: string; otherTabsUnreadStats: UnreadStats; + renderToastManager: (_: { + containerWidthBreakpoint: WidthBreakpoint; + }) => JSX.Element; }>; enum DragState { @@ -78,6 +81,7 @@ export function NavSidebar({ savePreferredLeftPaneWidth, title, otherTabsUnreadStats, + renderToastManager, }: NavSidebarProps): JSX.Element { const isRTL = i18n.getLocaleDirection() === 'rtl'; const [dragState, setDragState] = useState(DragState.INITIAL); @@ -218,6 +222,8 @@ export function NavSidebar({ tabIndex={0} {...moveProps} /> + + {renderToastManager({ containerWidthBreakpoint: widthBreakpoint })}
); } diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index d62a4e561..3505f5a0b 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -39,6 +39,7 @@ import type { import { Button, ButtonVariant } from './Button'; import { ChatColorPicker } from './ChatColorPicker'; import { Checkbox } from './Checkbox'; +import { WidthBreakpoint } from './_util'; import { CircleCheckbox, Variant as CircleCheckboxVariant, @@ -1581,9 +1582,11 @@ export function Preferences({ OS="unused" hideToast={() => setToast(undefined)} i18n={i18n} + onShowDebugLog={shouldNeverBeCalled} onUndoArchive={shouldNeverBeCalled} openFileInFolder={shouldNeverBeCalled} toast={toast} + containerWidthBreakpoint={WidthBreakpoint.Narrow} /> ); diff --git a/ts/components/ProfileEditor.stories.tsx b/ts/components/ProfileEditor.stories.tsx index 64da82607..cf7ce5c64 100644 --- a/ts/components/ProfileEditor.stories.tsx +++ b/ts/components/ProfileEditor.stories.tsx @@ -79,7 +79,6 @@ export default { replaceAvatar: action('replaceAvatar'), resetUsernameLink: action('resetUsernameLink'), saveAvatarToDisk: action('saveAvatarToDisk'), - markCompletedUsernameOnboarding: action('markCompletedUsernameOnboarding'), markCompletedUsernameLinkOnboarding: action( 'markCompletedUsernameLinkOnboarding' ), diff --git a/ts/components/ProfileEditor.tsx b/ts/components/ProfileEditor.tsx index 95a10e9b9..0e258b1c7 100644 --- a/ts/components/ProfileEditor.tsx +++ b/ts/components/ProfileEditor.tsx @@ -39,7 +39,6 @@ import { missingCaseError } from '../util/missingCaseError'; import { ConfirmationDialog } from './ConfirmationDialog'; import { ContextMenu } from './ContextMenu'; import { UsernameLinkModalBody } from './UsernameLinkModalBody'; -import { UsernameOnboardingModalBody } from './UsernameOnboardingModalBody'; import { ConversationDetailsIcon, IconType, @@ -54,7 +53,6 @@ export enum EditState { ProfileName = 'ProfileName', Bio = 'Bio', Username = 'Username', - UsernameOnboarding = 'UsernameOnboarding', UsernameLink = 'UsernameLink', } @@ -75,13 +73,13 @@ export type PropsDataType = { conversationId: string; familyName?: string; firstName: string; - hasCompletedUsernameOnboarding: boolean; hasCompletedUsernameLinkOnboarding: boolean; i18n: LocalizerType; isUsernameFlagEnabled: boolean; phoneNumber?: string; userAvatarData: ReadonlyArray; username?: string; + initialEditState?: EditState; usernameCorrupted: boolean; usernameEditState: UsernameEditState; usernameLinkState: UsernameLinkState; @@ -92,7 +90,6 @@ export type PropsDataType = { type PropsActionType = { deleteAvatarFromDisk: DeleteAvatarFromDiskActionType; - markCompletedUsernameOnboarding: () => void; markCompletedUsernameLinkOnboarding: () => void; onSetSkinTone: (tone: number) => unknown; replaceAvatar: ReplaceAvatarActionType; @@ -147,11 +144,10 @@ export function ProfileEditor({ deleteUsername, familyName, firstName, - hasCompletedUsernameOnboarding, hasCompletedUsernameLinkOnboarding, i18n, + initialEditState = EditState.None, isUsernameFlagEnabled, - markCompletedUsernameOnboarding, markCompletedUsernameLinkOnboarding, onEditStateChanged, onProfileChanged, @@ -179,7 +175,7 @@ export function ProfileEditor({ usernameLinkCorrupted, }: PropsType): JSX.Element { const focusInputRef = useRef(null); - const [editState, setEditState] = useState(EditState.None); + const [editState, setEditState] = useState(initialEditState); const [confirmDiscardAction, setConfirmDiscardAction] = useState< (() => unknown) | undefined >(undefined); @@ -518,16 +514,6 @@ export function ProfileEditor({ content = renderEditUsernameModalBody({ onClose: () => setEditState(EditState.None), }); - } else if (editState === EditState.UsernameOnboarding) { - content = ( - { - markCompletedUsernameOnboarding(); - setEditState(EditState.Username); - }} - /> - ); } else if (editState === EditState.UsernameLink) { content = ( , renderStoryCreator: () => <>StoryCreator, retryMessageSend: action('retryMessageSend'), showConversation: action('showConversation'), diff --git a/ts/components/StoriesTab.tsx b/ts/components/StoriesTab.tsx index b914bb7be..684b5c9b6 100644 --- a/ts/components/StoriesTab.tsx +++ b/ts/components/StoriesTab.tsx @@ -24,6 +24,7 @@ import { StoriesPane } from './StoriesPane'; import { NavSidebar, NavSidebarActionButton } from './NavSidebar'; import { StoriesAddStoryButton } from './StoriesAddStoryButton'; import { ContextMenu } from './ContextMenu'; +import type { WidthBreakpoint } from './_util'; import type { UnreadStats } from '../util/countUnreadStats'; export type PropsType = { @@ -50,6 +51,9 @@ export type PropsType = { preferredWidthFromStorage: number; queueStoryDownload: (storyId: string) => unknown; renderStoryCreator: () => JSX.Element; + renderToastManager: (_: { + containerWidthBreakpoint: WidthBreakpoint; + }) => JSX.Element; retryMessageSend: (messageId: string) => unknown; savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void; setAddStoryData: (data: AddStoryData) => unknown; @@ -84,6 +88,7 @@ export function StoriesTab({ preferredLeftPaneWidth, queueStoryDownload, renderStoryCreator, + renderToastManager, retryMessageSend, savePreferredLeftPaneWidth, setAddStoryData, @@ -127,6 +132,7 @@ export function StoriesTab({ preferredLeftPaneWidth={preferredLeftPaneWidth} queueStoryDownload={queueStoryDownload} retryMessageSend={retryMessageSend} + renderToastManager={renderToastManager} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} theme={theme} viewStory={viewStory} @@ -143,6 +149,7 @@ export function StoriesTab({ requiresFullWidth savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} otherTabsUnreadStats={otherTabsUnreadStats} + renderToastManager={renderToastManager} actions={ <> (null); const [focusRef] = useRestoreFocus(); - const [align, setAlign] = React.useState<'left' | 'center'>('left'); useEffect(() => { - function updateAlign() { - const leftPane = document.querySelector('.module-left-pane'); - const composer = document.querySelector( - '.ConversationView__composition-area' - ); - - if ( - leftPane != null && - composer != null && - leftPane.classList.contains('module-left-pane--width-narrow') - ) { - setAlign('center'); - return; - } - - setAlign('left'); - } - - updateAlign(); - - if (window.reduxStore == null) { - log.warn('Toast: No redux store'); - return; - } - return window.reduxStore.subscribe(updateAlign); - }, []); - - useEffect(() => { - const div = document.createElement('div'); - document.body.appendChild(div); - setRoot(div); - - return () => { - document.body.removeChild(div); - setRoot(null); - }; - }, []); - - useEffect(() => { - if (!root || autoDismissDisabled) { + if (autoDismissDisabled) { return; } @@ -86,56 +43,53 @@ export const Toast = memo(function ToastInner({ return () => { clearTimeoutIfNecessary(timeoutId); }; - }, [autoDismissDisabled, onClose, root, timeout]); + }, [autoDismissDisabled, onClose, timeout]); - return root - ? createPortal( + return ( +
{ + if (!disableCloseOnClick) { + onClose(); + } + }} + onKeyDown={(ev: KeyboardEvent) => { + if (ev.key === 'Enter' || ev.key === ' ') { + if (!disableCloseOnClick) { + onClose(); + } + } + }} + role="button" + tabIndex={0} + style={style} + > +
{children}
+ {toastAction && (
{ - if (!disableCloseOnClick) { - onClose(); - } + className="Toast__button" + onClick={(ev: MouseEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + toastAction.onClick(); + onClose(); }} onKeyDown={(ev: KeyboardEvent) => { if (ev.key === 'Enter' || ev.key === ' ') { - if (!disableCloseOnClick) { - onClose(); - } + ev.stopPropagation(); + ev.preventDefault(); + toastAction.onClick(); + onClose(); } }} + ref={focusRef} role="button" tabIndex={0} - style={style} > -
{children}
- {toastAction && ( -
) => { - ev.stopPropagation(); - ev.preventDefault(); - toastAction.onClick(); - onClose(); - }} - onKeyDown={(ev: KeyboardEvent) => { - if (ev.key === 'Enter' || ev.key === ' ') { - ev.stopPropagation(); - ev.preventDefault(); - toastAction.onClick(); - onClose(); - } - }} - ref={focusRef} - role="button" - tabIndex={0} - > - {toastAction.label} -
- )} -
, - root - ) - : null; + {toastAction.label} +
+ )} + + ); }); diff --git a/ts/components/ToastAlreadyRequestedToJoin.stories.tsx b/ts/components/ToastAlreadyRequestedToJoin.stories.tsx deleted file mode 100644 index 01f397d33..000000000 --- a/ts/components/ToastAlreadyRequestedToJoin.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastAlreadyRequestedToJoin'; -import { ToastAlreadyRequestedToJoin } from './ToastAlreadyRequestedToJoin'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastAlreadyRequestedToJoin', -} satisfies Meta; - -export const _ToastAlreadyRequestedToJoin = (): JSX.Element => ( - -); diff --git a/ts/components/ToastAlreadyRequestedToJoin.tsx b/ts/components/ToastAlreadyRequestedToJoin.tsx deleted file mode 100644 index a35af9672..000000000 --- a/ts/components/ToastAlreadyRequestedToJoin.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastAlreadyRequestedToJoin({ - i18n, - onClose, -}: PropsType): JSX.Element { - return ( - - {i18n('icu:GroupV2--join--already-awaiting-approval')} - - ); -} diff --git a/ts/components/ToastCaptchaFailed.stories.tsx b/ts/components/ToastCaptchaFailed.stories.tsx deleted file mode 100644 index 7592b59c0..000000000 --- a/ts/components/ToastCaptchaFailed.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastCaptchaFailed'; -import { ToastCaptchaFailed } from './ToastCaptchaFailed'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastCaptchaFailed', -} satisfies Meta; - -export const _ToastCaptchaFailed = (): JSX.Element => ( - -); diff --git a/ts/components/ToastCaptchaFailed.tsx b/ts/components/ToastCaptchaFailed.tsx deleted file mode 100644 index 1fd0dd64e..000000000 --- a/ts/components/ToastCaptchaFailed.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastCaptchaFailed({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('icu:verificationFailed')}; -} diff --git a/ts/components/ToastCaptchaSolved.stories.tsx b/ts/components/ToastCaptchaSolved.stories.tsx deleted file mode 100644 index 838b99789..000000000 --- a/ts/components/ToastCaptchaSolved.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastCaptchaSolved'; -import { ToastCaptchaSolved } from './ToastCaptchaSolved'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastCaptchaSolved', -} satisfies Meta; - -export const _ToastCaptchaSolved = (): JSX.Element => ( - -); diff --git a/ts/components/ToastCaptchaSolved.tsx b/ts/components/ToastCaptchaSolved.tsx deleted file mode 100644 index 1b410fb1c..000000000 --- a/ts/components/ToastCaptchaSolved.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastCaptchaSolved({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('icu:verificationComplete')}; -} diff --git a/ts/components/ToastDebugLogError.stories.tsx b/ts/components/ToastDebugLogError.stories.tsx deleted file mode 100644 index 0197d49d8..000000000 --- a/ts/components/ToastDebugLogError.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastDebugLogError'; -import { ToastDebugLogError } from './ToastDebugLogError'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastDebugLogError', -} satisfies Meta; - -export const _ToastDebugLogError = (): JSX.Element => ( - -); diff --git a/ts/components/ToastDebugLogError.tsx b/ts/components/ToastDebugLogError.tsx deleted file mode 100644 index c0c7f95a3..000000000 --- a/ts/components/ToastDebugLogError.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastDebugLogError({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('icu:debugLogError')}; -} diff --git a/ts/components/ToastFailedToFetchPhoneNumber.tsx b/ts/components/ToastFailedToFetchPhoneNumber.tsx deleted file mode 100644 index 2a34b77c9..000000000 --- a/ts/components/ToastFailedToFetchPhoneNumber.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastFailedToFetchPhoneNumber({ - i18n, - onClose, -}: PropsType): JSX.Element { - return ( - - {i18n('icu:Toast--failed-to-fetch-phone-number')} - - ); -} diff --git a/ts/components/ToastFailedToFetchUsername.tsx b/ts/components/ToastFailedToFetchUsername.tsx deleted file mode 100644 index e62cf33af..000000000 --- a/ts/components/ToastFailedToFetchUsername.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastFailedToFetchUsername({ - i18n, - onClose, -}: PropsType): JSX.Element { - return ( - - {i18n('icu:Toast--failed-to-fetch-username')} - - ); -} diff --git a/ts/components/ToastFileSize.stories.tsx b/ts/components/ToastFileSize.stories.tsx deleted file mode 100644 index eeca4dbb4..000000000 --- a/ts/components/ToastFileSize.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastFileSize'; -import { ToastFileSize } from './ToastFileSize'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastFileSize', -} satisfies Meta; - -export const _ToastFileSize = (): JSX.Element => ( - -); diff --git a/ts/components/ToastFileSize.tsx b/ts/components/ToastFileSize.tsx deleted file mode 100644 index 76675693f..000000000 --- a/ts/components/ToastFileSize.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type ToastPropsType = { - limit: number; - units: string; -}; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -} & ToastPropsType; - -export function ToastFileSize({ - i18n, - limit, - onClose, - units, -}: PropsType): JSX.Element { - return ( - - {i18n('icu:fileSizeWarning', { limit, units })} - - ); -} diff --git a/ts/components/ToastGroupLinkCopied.stories.tsx b/ts/components/ToastGroupLinkCopied.stories.tsx deleted file mode 100644 index 5598c690d..000000000 --- a/ts/components/ToastGroupLinkCopied.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastGroupLinkCopied'; -import { ToastGroupLinkCopied } from './ToastGroupLinkCopied'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastGroupLinkCopied', -} satisfies Meta; - -export const _ToastGroupLinkCopied = (): JSX.Element => ( - -); diff --git a/ts/components/ToastGroupLinkCopied.tsx b/ts/components/ToastGroupLinkCopied.tsx deleted file mode 100644 index e24adfdaf..000000000 --- a/ts/components/ToastGroupLinkCopied.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastGroupLinkCopied({ - i18n, - onClose, -}: PropsType): JSX.Element { - return ( - - {i18n('icu:GroupLinkManagement--clipboard')} - - ); -} diff --git a/ts/components/ToastInternalError.stories.tsx b/ts/components/ToastInternalError.stories.tsx deleted file mode 100644 index 39b27e4d2..000000000 --- a/ts/components/ToastInternalError.stories.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastInternalError'; -import { - ToastInternalError, - ToastInternalErrorKind, -} from './ToastInternalError'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), - onShowDebugLog: action('onShowDebugLog'), -}; - -export default { - title: 'Components/ToastInternalError', -} satisfies Meta; - -export function ToastDecryptionError(): JSX.Element { - return ( - - ); -} - -export function ToastCDSMirroringError(): JSX.Element { - return ( - - ); -} diff --git a/ts/components/ToastInternalError.tsx b/ts/components/ToastInternalError.tsx deleted file mode 100644 index d65c3557d..000000000 --- a/ts/components/ToastInternalError.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { missingCaseError } from '../util/missingCaseError'; -import { Toast } from './Toast'; - -export enum ToastInternalErrorKind { - DecryptionError = 'DecryptionError', - CDSMirroringError = 'CDSMirroringError', -} - -export type ToastPropsType = { - onShowDebugLog: () => unknown; -} & ( - | { - kind: ToastInternalErrorKind.DecryptionError; - deviceId: number; - name: string; - } - | { - kind: ToastInternalErrorKind.CDSMirroringError; - } -); - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -} & ToastPropsType; - -export function ToastInternalError(props: PropsType): JSX.Element { - const { kind, i18n, onClose, onShowDebugLog } = props; - - let body: string; - if (kind === ToastInternalErrorKind.DecryptionError) { - const { deviceId, name } = props; - - body = i18n('icu:decryptionErrorToast', { - name, - deviceId, - }); - } else if (kind === ToastInternalErrorKind.CDSMirroringError) { - body = i18n('icu:cdsMirroringErrorToast'); - } else { - throw missingCaseError(kind); - } - - return ( - - {body} - - ); -} diff --git a/ts/components/ToastLinkCopied.stories.tsx b/ts/components/ToastLinkCopied.stories.tsx deleted file mode 100644 index 48f83e16c..000000000 --- a/ts/components/ToastLinkCopied.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastLinkCopied'; -import { ToastLinkCopied } from './ToastLinkCopied'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastLinkCopied', -} satisfies Meta; - -export const _ToastLinkCopied = (): JSX.Element => ( - -); diff --git a/ts/components/ToastLinkCopied.tsx b/ts/components/ToastLinkCopied.tsx deleted file mode 100644 index d4c30b651..000000000 --- a/ts/components/ToastLinkCopied.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastLinkCopied({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('icu:debugLogLinkCopied')}; -} diff --git a/ts/components/ToastLoadingFullLogs.stories.tsx b/ts/components/ToastLoadingFullLogs.stories.tsx deleted file mode 100644 index b242468ad..000000000 --- a/ts/components/ToastLoadingFullLogs.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastLoadingFullLogs'; -import { ToastLoadingFullLogs } from './ToastLoadingFullLogs'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastLoadingFullLogs', -} satisfies Meta; - -export const _ToastLoadingFullLogs = (): JSX.Element => ( - -); diff --git a/ts/components/ToastLoadingFullLogs.tsx b/ts/components/ToastLoadingFullLogs.tsx deleted file mode 100644 index bfd6cd081..000000000 --- a/ts/components/ToastLoadingFullLogs.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastLoadingFullLogs({ - i18n, - onClose, -}: PropsType): JSX.Element { - return {i18n('icu:loading')}; -} diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 107abe6a2..705ac327b 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -9,6 +9,8 @@ import enMessages from '../../_locales/en/messages.json'; import { ToastManager } from './ToastManager'; import type { AnyToast } from '../types/Toast'; import { ToastType } from '../types/Toast'; +import type { AnyActionableMegaphone } from '../types/Megaphone'; +import { MegaphoneType } from '../types/Megaphone'; import { setupI18n } from '../util/setupI18n'; import { missingCaseError } from '../util/missingCaseError'; import type { PropsType } from './ToastManager'; @@ -41,6 +43,10 @@ function getToast(toastType: ToastType): AnyToast { return { toastType: ToastType.CannotOpenGiftBadgeOutgoing }; case ToastType.CannotStartGroupCall: return { toastType: ToastType.CannotStartGroupCall }; + case ToastType.CaptchaFailed: + return { toastType: ToastType.CaptchaFailed }; + case ToastType.CaptchaSolved: + return { toastType: ToastType.CaptchaSolved }; case ToastType.ConversationArchived: return { toastType: ToastType.ConversationArchived, @@ -61,6 +67,16 @@ function getToast(toastType: ToastType): AnyToast { return { toastType: ToastType.CopiedUsernameLink }; case ToastType.DangerousFileType: return { toastType: ToastType.DangerousFileType }; + case ToastType.DebugLogError: + return { toastType: ToastType.DebugLogError }; + case ToastType.DecryptionError: + return { + toastType: ToastType.DecryptionError, + parameters: { + deviceId: 2, + name: 'Alice', + }, + }; case ToastType.DeleteForEveryoneFailed: return { toastType: ToastType.DeleteForEveryoneFailed }; case ToastType.Error: @@ -69,6 +85,10 @@ function getToast(toastType: ToastType): AnyToast { return { toastType: ToastType.Expired }; case ToastType.FailedToDeleteUsername: return { toastType: ToastType.FailedToDeleteUsername }; + case ToastType.FailedToFetchPhoneNumber: + return { toastType: ToastType.FailedToFetchPhoneNumber }; + case ToastType.FailedToFetchUsername: + return { toastType: ToastType.FailedToFetchUsername }; case ToastType.FileSaved: return { toastType: ToastType.FileSaved, @@ -79,10 +99,16 @@ function getToast(toastType: ToastType): AnyToast { toastType: ToastType.FileSize, parameters: { limit: 100, units: 'MB' }, }; + case ToastType.GroupLinkCopied: + return { toastType: ToastType.GroupLinkCopied }; case ToastType.InvalidConversation: return { toastType: ToastType.InvalidConversation }; case ToastType.LeftGroup: return { toastType: ToastType.LeftGroup }; + case ToastType.LinkCopied: + return { toastType: ToastType.LinkCopied }; + case ToastType.LoadingFullLogs: + return { toastType: ToastType.LoadingFullLogs }; case ToastType.MaxAttachments: return { toastType: ToastType.MaxAttachments }; case ToastType.MessageBodyTooLong: @@ -95,6 +121,8 @@ function getToast(toastType: ToastType): AnyToast { return { toastType: ToastType.ReactionFailed }; case ToastType.ReportedSpamAndBlocked: return { toastType: ToastType.ReportedSpamAndBlocked }; + case ToastType.StickerPackInstallFailed: + return { toastType: ToastType.StickerPackInstallFailed }; case ToastType.StoryMuted: return { toastType: ToastType.StoryMuted }; case ToastType.StoryReact: @@ -130,6 +158,10 @@ function getToast(toastType: ToastType): AnyToast { group: 'Hike Group 🏔', }, }; + case ToastType.VoiceNoteLimit: + return { toastType: ToastType.VoiceNoteLimit }; + case ToastType.VoiceNoteMustBeTheOnlyAttachment: + return { toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment }; case ToastType.WhoCanFindMeReadOnly: return { toastType: ToastType.WhoCanFindMeReadOnly }; default: @@ -137,8 +169,22 @@ function getToast(toastType: ToastType): AnyToast { } } -type Args = Omit & { +function getMegaphone(megaphoneType: MegaphoneType): AnyActionableMegaphone { + switch (megaphoneType) { + case MegaphoneType.UsernameOnboarding: + return { + type: megaphoneType, + onLearnMore: action('onLearnMore'), + onDismiss: action('onDismiss'), + }; + default: + throw missingCaseError(megaphoneType); + } +} + +type Args = Omit & { toastType: ToastType; + megaphoneType: MegaphoneType; }; export default { @@ -149,24 +195,34 @@ export default { options: ToastType, control: { type: 'select' }, }, + megaphoneType: { + options: MegaphoneType, + control: { type: 'select' }, + }, }, args: { hideToast: action('hideToast'), openFileInFolder: action('openFileInFolder'), + onShowDebugLog: action('onShowDebugLog'), onUndoArchive: action('onUndoArchive'), i18n, toastType: ToastType.AddingUserToGroup, + megaphoneType: MegaphoneType.UsernameOnboarding, OS: 'macOS', }, } satisfies Meta; // eslint-disable-next-line react/function-component-definition const Template: StoryFn = args => { - const { toastType, ...rest } = args; + const { toastType, megaphoneType, ...rest } = args; return ( <>

Select a toast type in controls

- + ); }; diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index 02cf24ba9..4abacb84d 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -1,29 +1,43 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import classNames from 'classnames'; import React from 'react'; +import { createPortal } from 'react-dom'; + import type { LocalizerType } from '../types/Util'; import { SECOND } from '../util/durations'; import { Toast } from './Toast'; +import { WidthBreakpoint } from './_util'; +import { UsernameMegaphone } from './UsernameMegaphone'; +import { assertDev } from '../util/assert'; import { missingCaseError } from '../util/missingCaseError'; import type { AnyToast } from '../types/Toast'; import { ToastType } from '../types/Toast'; +import type { AnyActionableMegaphone } from '../types/Megaphone'; +import { MegaphoneType } from '../types/Megaphone'; export type PropsType = { hideToast: () => unknown; i18n: LocalizerType; openFileInFolder: (target: string) => unknown; OS: string; + onShowDebugLog: () => unknown; onUndoArchive: (conversaetionId: string) => unknown; toast?: AnyToast; + megaphone?: AnyActionableMegaphone; + centerToast?: boolean; + containerWidthBreakpoint: WidthBreakpoint; + isCompositionAreaVisible?: boolean; }; const SHORT_TIMEOUT = 3 * SECOND; -export function ToastManager({ +export function renderToast({ hideToast, i18n, openFileInFolder, + onShowDebugLog, onUndoArchive, OS, toast, @@ -116,6 +130,16 @@ export function ToastManager({ ); } + if (toastType === ToastType.CaptchaFailed) { + return {i18n('icu:verificationFailed')}; + } + + if (toastType === ToastType.CaptchaSolved) { + return ( + {i18n('icu:verificationComplete')} + ); + } + if (toastType === ToastType.CannotStartGroupCall) { return ( @@ -184,6 +208,10 @@ export function ToastManager({ return {i18n('icu:dangerousFileType')}; } + if (toastType === ToastType.DebugLogError) { + return {i18n('icu:debugLogError')}; + } + if (toastType === ToastType.DeleteForEveryoneFailed) { return ( {i18n('icu:deleteForEveryoneFailed')} @@ -217,6 +245,22 @@ export function ToastManager({ ); } + if (toastType === ToastType.FailedToFetchPhoneNumber) { + return ( + + {i18n('icu:Toast--failed-to-fetch-phone-number')} + + ); + } + + if (toastType === ToastType.FailedToFetchUsername) { + return ( + + {i18n('icu:Toast--failed-to-fetch-username')} + + ); + } + if (toastType === ToastType.FileSaved) { return ( + {i18n('icu:GroupLinkManagement--clipboard')} + + ); + } + + if (toastType === ToastType.DecryptionError) { + assertDev( + toast.toastType === ToastType.DecryptionError, + 'Pacify typescript' + ); + const { parameters } = toast; + const { deviceId, name } = parameters; + + return ( + + {i18n('icu:decryptionErrorToast', { + name, + deviceId, + })} + + ); + } + if (toastType === ToastType.InvalidConversation) { return {i18n('icu:invalidConversation')}; } @@ -252,6 +331,14 @@ export function ToastManager({ return {i18n('icu:youLeftTheGroup')}; } + if (toastType === ToastType.LinkCopied) { + return {i18n('icu:debugLogLinkCopied')}; + } + + if (toastType === ToastType.LoadingFullLogs) { + return {i18n('icu:loading')}; + } + if (toastType === ToastType.MaxAttachments) { return {i18n('icu:maximumAttachments')}; } @@ -284,6 +371,14 @@ export function ToastManager({ ); } + if (toastType === ToastType.StickerPackInstallFailed) { + return ( + + {i18n('icu:stickers--toast--InstallFailed')} + + ); + } + if (toastType === ToastType.StoryMuted) { return ( @@ -392,6 +487,18 @@ export function ToastManager({ ); } + if (toastType === ToastType.VoiceNoteLimit) { + return {i18n('icu:voiceNoteLimit')}; + } + + if (toastType === ToastType.VoiceNoteMustBeTheOnlyAttachment) { + return ( + + {i18n('icu:voiceNoteMustBeOnlyAttachment')} + + ); + } + if (toastType === ToastType.WhoCanFindMeReadOnly) { return ( {i18n('icu:WhoCanFindMeReadOnlyToast')} @@ -400,3 +507,43 @@ export function ToastManager({ throw missingCaseError(toastType); } + +export function renderMegaphone({ + i18n, + megaphone, +}: PropsType): JSX.Element | null { + if (!megaphone) { + return null; + } + + if (megaphone.type === MegaphoneType.UsernameOnboarding) { + return ; + } + + throw missingCaseError(megaphone.type); +} + +export function ToastManager(props: PropsType): JSX.Element { + const { centerToast, containerWidthBreakpoint, isCompositionAreaVisible } = + props; + + const toast = renderToast(props); + + return ( +
+ {centerToast + ? createPortal( +
{toast}
, + document.body + ) + : toast} + {renderMegaphone(props)} +
+ ); +} diff --git a/ts/components/ToastStickerPackInstallFailed.stories.tsx b/ts/components/ToastStickerPackInstallFailed.stories.tsx deleted file mode 100644 index 09ec721d3..000000000 --- a/ts/components/ToastStickerPackInstallFailed.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastStickerPackInstallFailed'; -import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastStickerPackInstallFailed', -} satisfies Meta; - -export const _ToastStickerPackInstallFailed = (): JSX.Element => ( - -); diff --git a/ts/components/ToastStickerPackInstallFailed.tsx b/ts/components/ToastStickerPackInstallFailed.tsx deleted file mode 100644 index e396a684b..000000000 --- a/ts/components/ToastStickerPackInstallFailed.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastStickerPackInstallFailed({ - i18n, - onClose, -}: PropsType): JSX.Element { - return ( - - {i18n('icu:stickers--toast--InstallFailed')} - - ); -} diff --git a/ts/components/ToastVoiceNoteError.tsx b/ts/components/ToastVoiceNoteError.tsx deleted file mode 100644 index 645c8c213..000000000 --- a/ts/components/ToastVoiceNoteError.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastVoiceNoteLimit({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('icu:voiceNoteError')}; -} diff --git a/ts/components/ToastVoiceNoteLimit.stories.tsx b/ts/components/ToastVoiceNoteLimit.stories.tsx deleted file mode 100644 index c3a29f586..000000000 --- a/ts/components/ToastVoiceNoteLimit.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastVoiceNoteLimit'; -import { ToastVoiceNoteLimit } from './ToastVoiceNoteLimit'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastVoiceNoteLimit', -} satisfies Meta; - -export const _ToastVoiceNoteLimit = (): JSX.Element => ( - -); diff --git a/ts/components/ToastVoiceNoteLimit.tsx b/ts/components/ToastVoiceNoteLimit.tsx deleted file mode 100644 index a9d1e89dd..000000000 --- a/ts/components/ToastVoiceNoteLimit.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastVoiceNoteLimit({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('icu:voiceNoteLimit')}; -} diff --git a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.stories.tsx b/ts/components/ToastVoiceNoteMustBeOnlyAttachment.stories.tsx deleted file mode 100644 index 4b71ce879..000000000 --- a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { PropsType } from './ToastVoiceNoteMustBeOnlyAttachment'; -import { ToastVoiceNoteMustBeOnlyAttachment } from './ToastVoiceNoteMustBeOnlyAttachment'; -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastVoiceNoteMustBeOnlyAttachment', -} satisfies Meta; - -export const _ToastVoiceNoteMustBeOnlyAttachment = (): JSX.Element => ( - -); diff --git a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.tsx b/ts/components/ToastVoiceNoteMustBeOnlyAttachment.tsx deleted file mode 100644 index f07f4cf36..000000000 --- a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastVoiceNoteMustBeOnlyAttachment({ - i18n, - onClose, -}: PropsType): JSX.Element { - return ( - {i18n('icu:voiceNoteMustBeOnlyAttachment')} - ); -} diff --git a/ts/components/UsernameMegaphone.stories.tsx b/ts/components/UsernameMegaphone.stories.tsx new file mode 100644 index 000000000..9c8f2986b --- /dev/null +++ b/ts/components/UsernameMegaphone.stories.tsx @@ -0,0 +1,27 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import type { PropsType } from './UsernameMegaphone'; +import { UsernameMegaphone } from './UsernameMegaphone'; +import { type ComponentMeta } from '../storybook/types'; +import { setupI18n } from '../util/setupI18n'; +import enMessages from '../../_locales/en/messages.json'; + +const i18n = setupI18n('en', enMessages); + +export default { + title: 'Components/UsernameMegaphone', + component: UsernameMegaphone, + argTypes: {}, + args: { + i18n, + onLearnMore: action('onLearnMore'), + onDismiss: action('onDismiss'), + }, +} satisfies ComponentMeta; + +export function Defaults(args: PropsType): JSX.Element { + return ; +} diff --git a/ts/components/UsernameMegaphone.tsx b/ts/components/UsernameMegaphone.tsx new file mode 100644 index 000000000..b7f14738f --- /dev/null +++ b/ts/components/UsernameMegaphone.tsx @@ -0,0 +1,50 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import type { LocalizerType } from '../types/Util'; +import type { UsernameOnboardingActionableMegaphoneType } from '../types/Megaphone'; +import { Button, ButtonSize, ButtonVariant } from './Button'; + +export type PropsType = { + i18n: LocalizerType; +} & Omit; + +export function UsernameMegaphone({ + i18n, + onLearnMore, + onDismiss, +}: PropsType): JSX.Element { + return ( +
+
+ + +
+

{i18n('icu:UsernameMegaphone__title')}

+

{i18n('icu:UsernameMegaphone__body')}

+
+
+ +
+ + + +
+
+ ); +} diff --git a/ts/components/UsernameOnboardingModalBody.stories.tsx b/ts/components/UsernameOnboardingModal.stories.tsx similarity index 65% rename from ts/components/UsernameOnboardingModalBody.stories.tsx rename to ts/components/UsernameOnboardingModal.stories.tsx index ea5972210..5974cc286 100644 --- a/ts/components/UsernameOnboardingModalBody.stories.tsx +++ b/ts/components/UsernameOnboardingModal.stories.tsx @@ -8,23 +8,25 @@ import { action } from '@storybook/addon-actions'; import enMessages from '../../_locales/en/messages.json'; import { setupI18n } from '../util/setupI18n'; -import type { PropsType } from './UsernameOnboardingModalBody'; -import { UsernameOnboardingModalBody } from './UsernameOnboardingModalBody'; +import type { PropsType } from './UsernameOnboardingModal'; +import { UsernameOnboardingModal } from './UsernameOnboardingModal'; const i18n = setupI18n('en', enMessages); export default { - component: UsernameOnboardingModalBody, - title: 'Components/UsernameOnboardingModalBody', + component: UsernameOnboardingModal, + title: 'Components/UsernameOnboardingModal', args: { i18n, onNext: action('onNext'), + onSkip: action('onSkip'), + onClose: action('onClose'), }, } satisfies Meta; // eslint-disable-next-line react/function-component-definition const Template: StoryFn = args => { - return ; + return ; }; export const Normal = Template.bind({}); diff --git a/ts/components/UsernameOnboardingModal.tsx b/ts/components/UsernameOnboardingModal.tsx new file mode 100644 index 000000000..ece1db1aa --- /dev/null +++ b/ts/components/UsernameOnboardingModal.tsx @@ -0,0 +1,80 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; + +import type { LocalizerType } from '../types/Util'; +import { Button, ButtonVariant } from './Button'; +import { Modal } from './Modal'; + +export type PropsType = Readonly<{ + i18n: LocalizerType; + onNext: () => void; + onSkip: () => void; + onClose: () => void; +}>; + +export function UsernameOnboardingModal({ + i18n, + onNext, + onSkip, + onClose, +}: PropsType): JSX.Element { + return ( + +
+
+ {i18n('icu:UsernameOnboardingModalBody__title')} +
+ +
+
+ +
+

+ {i18n('icu:UsernameOnboardingModalBody__row__number__title')} +

+ {i18n('icu:UsernameOnboardingModalBody__row__number__body')} +
+
+ +
+
+ +
+

+ {i18n('icu:UsernameOnboardingModalBody__row__username__title')} +

+ {i18n('icu:UsernameOnboardingModalBody__row__username__body')} +
+
+ +
+
+ +
+

{i18n('icu:UsernameOnboardingModalBody__row__qr__title')}

+ {i18n('icu:UsernameOnboardingModalBody__row__qr__body')} +
+
+ + + + +
+ + ); +} diff --git a/ts/components/UsernameOnboardingModalBody.tsx b/ts/components/UsernameOnboardingModalBody.tsx deleted file mode 100644 index 93bd597ef..000000000 --- a/ts/components/UsernameOnboardingModalBody.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2023 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; - -import type { LocalizerType } from '../types/Util'; -import { Button } from './Button'; - -export type PropsType = Readonly<{ - i18n: LocalizerType; - onNext: () => void; -}>; - -const CLASS = 'UsernameOnboardingModalBody'; - -const SUPPORT_URL = 'https://support.signal.org/hc/articles/5389476324250'; - -export function UsernameOnboardingModalBody({ - i18n, - onNext, -}: PropsType): JSX.Element { - return ( -
-
- -
- {i18n('icu:UsernameOnboardingModalBody__title')} -
- -
-
- -
- {i18n('icu:UsernameOnboardingModalBody__row__number')} -
-
- -
-
- -
- {i18n('icu:UsernameOnboardingModalBody__row__link')} -
-
- -
-
- -
- {i18n('icu:UsernameOnboardingModalBody__row__lock')} -
-
- - - - -
- ); -} diff --git a/ts/components/conversation/AudioCapture.tsx b/ts/components/conversation/AudioCapture.tsx index a4a5a9484..372fa5399 100644 --- a/ts/components/conversation/AudioCapture.tsx +++ b/ts/components/conversation/AudioCapture.tsx @@ -1,11 +1,12 @@ // Copyright 2016 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useCallback, useState } from 'react'; +import React, { useCallback } from 'react'; +import type { ShowToastAction } from '../../state/ducks/toast'; import type { AttachmentDraftType } from '../../types/Attachment'; import type { LocalizerType } from '../../types/Util'; -import { ToastVoiceNoteMustBeOnlyAttachment } from '../ToastVoiceNoteMustBeOnlyAttachment'; +import { ToastType } from '../../types/Toast'; import { useStartRecordingShortcut, useKeyboardShortcuts, @@ -16,6 +17,7 @@ export type PropsType = { draftAttachments: ReadonlyArray; i18n: LocalizerType; startRecording: (id: string) => unknown; + showToast: ShowToastAction; }; export function AudioCapture({ @@ -23,9 +25,8 @@ export function AudioCapture({ draftAttachments, i18n, startRecording, + showToast, }: PropsType): JSX.Element { - const [showOnlyAttachmentToast, setShowOnlyAttachmentToast] = useState(false); - const recordConversation = useCallback( () => startRecording(conversationId), [conversationId, startRecording] @@ -33,40 +34,23 @@ export function AudioCapture({ const startRecordingShortcut = useStartRecordingShortcut(recordConversation); useKeyboardShortcuts(startRecordingShortcut); - const handleCloseToast = useCallback(() => { - setShowOnlyAttachmentToast(false); - }, []); - const handleClick = useCallback(() => { if (draftAttachments.length) { - setShowOnlyAttachmentToast(true); + showToast({ toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment }); } else { startRecording(conversationId); } - }, [ - conversationId, - draftAttachments, - setShowOnlyAttachmentToast, - startRecording, - ]); + }, [conversationId, draftAttachments, showToast, startRecording]); return ( - <> -
-
- {showOnlyAttachmentToast && ( - - )} - +
+
); } diff --git a/ts/state/ducks/app.ts b/ts/state/ducks/app.ts index 4824259cc..fe42c36b4 100644 --- a/ts/state/ducks/app.ts +++ b/ts/state/ducks/app.ts @@ -4,6 +4,8 @@ import type { ThunkAction } from 'redux-thunk'; import type { ReadonlyDeep } from 'type-fest'; import type { StateType as RootStateType } from '../reducer'; +import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; +import { useBoundActions } from '../../hooks/useBoundActions'; import * as log from '../../logging/log'; // State @@ -57,6 +59,9 @@ export const actions = { openStandalone, }; +export const useAppActions = (): BoundActionCreatorsMapObject => + useBoundActions(actions); + function initialLoadComplete(): InitialLoadCompleteActionType { return { type: INITIAL_LOAD_COMPLETE, diff --git a/ts/state/ducks/crashReports.ts b/ts/state/ducks/crashReports.ts index 14dffc949..9b9a24a22 100644 --- a/ts/state/ducks/crashReports.ts +++ b/ts/state/ducks/crashReports.ts @@ -2,11 +2,14 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ReadonlyDeep } from 'type-fest'; +import type { ThunkAction } from 'redux-thunk'; + import * as log from '../../logging/log'; -import { showToast } from '../../util/showToast'; import * as Errors from '../../types/errors'; -import { ToastLinkCopied } from '../../components/ToastLinkCopied'; -import { ToastDebugLogError } from '../../components/ToastDebugLogError'; +import { ToastType } from '../../types/Toast'; +import type { StateType as RootStateType } from '../reducer'; +import { showToast } from './toast'; +import type { ShowToastActionType } from './toast'; import type { PromiseAction } from '../util'; // State @@ -45,12 +48,43 @@ function setCrashReportCount(count: number): SetCrashReportCountActionType { return { type: SET_COUNT, payload: count }; } -function uploadCrashReports(): PromiseAction { - return { type: UPLOAD, payload: window.IPC.crashReports.upload() }; +function uploadCrashReports(): ThunkAction< + void, + RootStateType, + unknown, + PromiseAction | ShowToastActionType +> { + return dispatch => { + async function run() { + try { + await window.IPC.crashReports.upload(); + dispatch(showToast({ toastType: ToastType.LinkCopied })); + } catch (error) { + dispatch(showToast({ toastType: ToastType.DebugLogError })); + throw error; + } + } + dispatch({ type: UPLOAD, payload: run() }); + }; } -function eraseCrashReports(): PromiseAction { - return { type: ERASE, payload: window.IPC.crashReports.erase() }; +function eraseCrashReports(): ThunkAction< + void, + RootStateType, + unknown, + PromiseAction | ShowToastActionType +> { + return dispatch => { + async function run() { + try { + await window.IPC.crashReports.erase(); + } catch (error) { + dispatch(showToast({ toastType: ToastType.DebugLogError })); + throw error; + } + } + dispatch({ type: ERASE, payload: run() }); + }; } // Reducer @@ -87,9 +121,6 @@ export function reducer( action.type === `${UPLOAD}_FULFILLED` || action.type === `${ERASE}_FULFILLED` ) { - if (action.type === `${UPLOAD}_FULFILLED`) { - showToast(ToastLinkCopied); - } return { ...state, count: 0, @@ -107,8 +138,6 @@ export function reducer( `Failed to upload crash report due to error ${Errors.toLogFormat(error)}` ); - showToast(ToastDebugLogError); - return { ...state, count: 0, diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index 1a81ec0fa..aa1b18e28 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -16,10 +16,12 @@ import type { import type { MessagePropsType } from '../selectors/message'; import type { RecipientsByConversation } from './stories'; import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog'; +import type { EditState as ProfileEditorEditState } from '../../components/ProfileEditor'; import type { StateType as RootStateType } from '../reducer'; import * as Errors from '../../types/errors'; import * as SingleServePromise from '../../services/singleServePromise'; import * as Stickers from '../../types/Stickers'; +import { UsernameOnboardingState } from '../../types/globalModals'; import * as log from '../../logging/log'; import { getMessagePropsSelector } from '../selectors/message'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; @@ -96,7 +98,9 @@ export type GlobalModalsStateType = ReadonlyDeep<{ isSignalConnectionsVisible: boolean; isStoriesSettingsVisible: boolean; isWhatsNewVisible: boolean; + usernameOnboardingState: UsernameOnboardingState; profileEditorHasError: boolean; + profileEditorInitialEditState: ProfileEditorEditState | undefined; safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType; safetyNumberModalContactId?: string; sendEditWarningData?: SendEditWarningDataType; @@ -151,6 +155,7 @@ const CONFIRM_AUTH_ART_CREATOR_FULFILLED = 'globalModals/CONFIRM_AUTH_ART_CREATOR_FULFILLED'; const SHOW_EDIT_HISTORY_MODAL = 'globalModals/SHOW_EDIT_HISTORY_MODAL'; const CLOSE_EDIT_HISTORY_MODAL = 'globalModals/CLOSE_EDIT_HISTORY_MODAL'; +const TOGGLE_USERNAME_ONBOARDING = 'globalModals/TOGGLE_USERNAME_ONBOARDING'; export type ContactModalStateType = ReadonlyDeep<{ contactId: string; @@ -206,6 +211,9 @@ type ToggleForwardMessagesModalActionType = ReadonlyDeep<{ type ToggleProfileEditorActionType = ReadonlyDeep<{ type: typeof TOGGLE_PROFILE_EDITOR; + payload: { + initialEditState?: ProfileEditorEditState; + }; }>; export type ToggleProfileEditorErrorActionType = ReadonlyDeep<{ @@ -231,6 +239,10 @@ type ToggleConfirmationModalActionType = ReadonlyDeep<{ payload: boolean; }>; +type ToggleUsernameOnboardingActionType = ReadonlyDeep<{ + type: typeof TOGGLE_USERNAME_ONBOARDING; +}>; + type ShowStoriesSettingsActionType = ReadonlyDeep<{ type: typeof SHOW_STORIES_SETTINGS; }>; @@ -368,6 +380,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | ToggleProfileEditorErrorActionType | ToggleSafetyNumberModalActionType | ToggleSignalConnectionsModalActionType + | ToggleUsernameOnboardingActionType >; // Action Creators @@ -406,6 +419,7 @@ export const actions = { toggleProfileEditorHasError, toggleSafetyNumberModal, toggleSignalConnectionsModal, + toggleUsernameOnboarding, }; export const useGlobalModalActions = (): BoundActionCreatorsMapObject< @@ -585,8 +599,10 @@ function toggleForwardMessagesModal( }; } -function toggleProfileEditor(): ToggleProfileEditorActionType { - return { type: TOGGLE_PROFILE_EDITOR }; +function toggleProfileEditor( + initialEditState?: ProfileEditorEditState +): ToggleProfileEditorActionType { + return { type: TOGGLE_PROFILE_EDITOR, payload: { initialEditState } }; } function toggleProfileEditorHasError(): ToggleProfileEditorErrorActionType { @@ -626,6 +642,10 @@ function toggleConfirmationModal( }; } +function toggleUsernameOnboarding(): ToggleUsernameOnboardingActionType { + return { type: TOGGLE_USERNAME_ONBOARDING }; +} + function showBlockingSafetyNumberChangeDialog( untrustedByConversation: RecipientsByConversation, explodedPromise: ExplodePromiseResultType, @@ -861,7 +881,9 @@ export function getEmptyState(): GlobalModalsStateType { isSignalConnectionsVisible: false, isStoriesSettingsVisible: false, isWhatsNewVisible: false, + usernameOnboardingState: UsernameOnboardingState.NeverShown, profileEditorHasError: false, + profileEditorInitialEditState: undefined, }; } @@ -873,6 +895,7 @@ export function reducer( return { ...state, isProfileEditorVisible: !state.isProfileEditorVisible, + profileEditorInitialEditState: action.payload.initialEditState, }; } @@ -983,6 +1006,16 @@ export function reducer( }; } + if (action.type === TOGGLE_USERNAME_ONBOARDING) { + return { + ...state, + usernameOnboardingState: + state.usernameOnboardingState === UsernameOnboardingState.Open + ? UsernameOnboardingState.Closed + : UsernameOnboardingState.Open, + }; + } + if (action.type === SHOW_SEND_ANYWAY_DIALOG) { const { promiseUuid, source } = action.payload; diff --git a/ts/state/ducks/items.ts b/ts/state/ducks/items.ts index b4bbe81a5..d0c01a5c6 100644 --- a/ts/state/ducks/items.ts +++ b/ts/state/ducks/items.ts @@ -27,20 +27,7 @@ export type ItemsStateType = ReadonlyDeep< [key: string]: unknown; remoteConfig?: RemoteConfigType; serverTimeSkew?: number; - } & Partial< - Pick< - StorageAccessType, - | 'universalExpireTimer' - | 'defaultConversationColor' - | 'customColors' - | 'preferredLeftPaneWidth' - | 'navTabsCollapsed' - | 'preferredReactionEmoji' - | 'areWeASubscriber' - | 'usernameLinkColor' - | 'usernameLink' - > - > + } & Partial >; // Actions diff --git a/ts/state/ducks/toast.ts b/ts/state/ducks/toast.ts index b4d6879f3..b92ab3f5e 100644 --- a/ts/state/ducks/toast.ts +++ b/ts/state/ducks/toast.ts @@ -23,6 +23,7 @@ export const SHOW_TOAST = 'toast/SHOW_TOAST'; type HideToastActionType = ReadonlyDeep<{ type: typeof HIDE_TOAST; + payload: AnyToast | undefined; }>; // eslint-disable-next-line local-rules/type-alias-readonlydeep @@ -36,9 +37,12 @@ export type ToastActionType = HideToastActionType | ShowToastActionType; // Action Creators -function hideToast(): HideToastActionType { +export type HideToastAction = ReadonlyDeep<(toast?: AnyToast) => void>; + +function hideToast(toast?: AnyToast): HideToastActionType { return { type: HIDE_TOAST, + payload: toast, }; } @@ -80,6 +84,10 @@ export function reducer( action: Readonly ): ToastStateType { if (action.type === HIDE_TOAST) { + if (action.payload != null && state.toast !== action.payload) { + return state; + } + return { ...state, toast: undefined, diff --git a/ts/state/ducks/username.ts b/ts/state/ducks/username.ts index 23b692775..f7e5a994a 100644 --- a/ts/state/ducks/username.ts +++ b/ts/state/ducks/username.ts @@ -28,6 +28,8 @@ import { import { showToast } from './toast'; import { ToastType } from '../../types/Toast'; import type { ToastActionType } from './toast'; +import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; +import { useBoundActions } from '../../hooks/useBoundActions'; export type UsernameReservationStateType = ReadonlyDeep<{ state: UsernameReservationState; @@ -129,6 +131,10 @@ export const actions = { markCompletedUsernameLinkOnboarding, }; +export const useUsernameActions = (): BoundActionCreatorsMapObject< + typeof actions +> => useBoundActions(actions); + export function setUsernameEditState( editState: UsernameEditState ): SetUsernameEditStateActionType { diff --git a/ts/state/selectors/globalModals.ts b/ts/state/selectors/globalModals.ts index f5a5648a1..03dbe438f 100644 --- a/ts/state/selectors/globalModals.ts +++ b/ts/state/selectors/globalModals.ts @@ -5,6 +5,7 @@ import { createSelector } from 'reselect'; import type { StateType } from '../reducer'; import type { GlobalModalsStateType } from '../ducks/globalModals'; +import { UsernameOnboardingState } from '../../types/globalModals'; export const getGlobalModalsState = (state: StateType): GlobalModalsStateType => state.globalModals; @@ -12,5 +13,11 @@ export const getGlobalModalsState = (state: StateType): GlobalModalsStateType => export const isShowingAnyModal = createSelector( getGlobalModalsState, (globalModalsState): boolean => - Object.values(globalModalsState).some(value => Boolean(value)) + Object.entries(globalModalsState).some(([key, value]) => { + if (key === 'usernameOnboardingState') { + return value === UsernameOnboardingState.Open; + } + + return Boolean(value); + }) ); diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx index f1e10a192..3bd0f38e2 100644 --- a/ts/state/smart/App.tsx +++ b/ts/state/smart/App.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import type { VerificationTransport } from '../../types/VerificationTransport'; import { App } from '../../components/App'; @@ -12,18 +12,16 @@ import { SmartCallManager } from './CallManager'; import { SmartGlobalModalContainer } from './GlobalModalContainer'; import { SmartLightbox } from './Lightbox'; import { SmartStoryViewer } from './StoryViewer'; -import type { StateType } from '../reducer'; import { - getIntl, - getLocaleMessages, getTheme, getIsMainWindowMaximized, getIsMainWindowFullScreen, - getMenuOptions, } from '../selectors/user'; import { hasSelectedStoryData } from '../selectors/stories'; -import { getHideMenuBar } from '../selectors/items'; -import { mapDispatchToProps } from '../actions'; +import type { StateType } from '../reducer'; +import { useAppActions } from '../ducks/app'; +import { useConversationsActions } from '../ducks/conversations'; +import { useStoriesActions } from '../ducks/stories'; import { ErrorBoundary } from '../../components/ErrorBoundary'; import { ModalContainer } from '../../components/ModalContainer'; import { SmartInbox } from './Inbox'; @@ -32,58 +30,58 @@ function renderInbox(): JSX.Element { return ; } -const mapStateToProps = (state: StateType) => { - const i18n = getIntl(state); +export function SmartApp(): JSX.Element { + const app = useSelector((state: StateType) => state.app); - return { - ...state.app, - i18n, - localeMessages: getLocaleMessages(state), - isMaximized: getIsMainWindowMaximized(state), - isFullScreen: getIsMainWindowFullScreen(state), - menuOptions: getMenuOptions(state), - OS: OS.getName(), - osClassName: OS.getClassName(), - hideMenuBar: getHideMenuBar(state), - renderCallManager: () => ( - - - - ), - renderGlobalModalContainer: () => , - renderLightbox: () => , - hasSelectedStoryData: hasSelectedStoryData(state), - renderStoryViewer: (closeView: () => unknown) => ( - - - - ), - renderInbox, - requestVerification: ( - number: string, - captcha: string, - transport: VerificationTransport - ): Promise<{ sessionId: string }> => { - const { server } = window.textsecure; - strictAssert(server !== undefined, 'WebAPI not available'); + const { openInbox } = useAppActions(); - return server.requestVerification(number, captcha, transport); - }, - registerSingleDevice: ( - number: string, - code: string, - sessionId: string - ): Promise => { - return window - .getAccountManager() - .registerSingleDevice(number, code, sessionId); - }, - theme: getTheme(state), + const { scrollToMessage } = useConversationsActions(); - toast: state.toast.toast, - }; -}; + const { viewStory } = useStoriesActions(); -const smart = connect(mapStateToProps, mapDispatchToProps); + return ( + ( + + + + )} + renderGlobalModalContainer={() => } + renderLightbox={() => } + hasSelectedStoryData={useSelector(hasSelectedStoryData)} + renderStoryViewer={(closeView: () => unknown) => ( + + + + )} + renderInbox={renderInbox} + requestVerification={( + number: string, + captcha: string, + transport: VerificationTransport + ): Promise<{ sessionId: string }> => { + const { server } = window.textsecure; + strictAssert(server !== undefined, 'WebAPI not available'); -export const SmartApp = smart(App); + return server.requestVerification(number, captcha, transport); + }} + registerSingleDevice={( + number: string, + code: string, + sessionId: string + ): Promise => { + return window + .getAccountManager() + .registerSingleDevice(number, code, sessionId); + }} + theme={useSelector(getTheme)} + openInbox={openInbox} + scrollToMessage={scrollToMessage} + viewStory={viewStory} + /> + ); +} diff --git a/ts/state/smart/CallsTab.tsx b/ts/state/smart/CallsTab.tsx index 7fcc5313d..40d441ef8 100644 --- a/ts/state/smart/CallsTab.tsx +++ b/ts/state/smart/CallsTab.tsx @@ -9,6 +9,7 @@ import { getPreferredLeftPaneWidth, } from '../selectors/items'; import { getIntl, getRegionCode } from '../selectors/user'; +import type { WidthBreakpoint } from '../../components/_util'; import { CallsTab } from '../../components/CallsTab'; import { getAllConversations, @@ -23,6 +24,7 @@ import type { } from '../../types/CallDisposition'; import type { ConversationType } from '../ducks/conversations'; import { SmartConversationDetails } from './ConversationDetails'; +import { SmartToastManager } from './ToastManager'; import { useCallingActions } from '../ducks/calling'; import { getActiveCallState } from '../selectors/calling'; import { useCallHistoryActions } from '../ducks/callHistory'; @@ -80,6 +82,12 @@ function renderConversationDetails( ); } +function renderToastManager(props: { + containerWidthBreakpoint: WidthBreakpoint; +}): JSX.Element { + return ; +} + export function SmartCallsTab(): JSX.Element { const i18n = useSelector(getIntl); const navTabsCollapsed = useSelector(getNavTabsCollapsed); @@ -172,6 +180,7 @@ export function SmartCallsTab(): JSX.Element { onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation} preferredLeftPaneWidth={preferredLeftPaneWidth} renderConversationDetails={renderConversationDetails} + renderToastManager={renderToastManager} regionCode={regionCode} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} /> diff --git a/ts/state/smart/ChatsTab.tsx b/ts/state/smart/ChatsTab.tsx index f33797e8c..1af1d0aa0 100644 --- a/ts/state/smart/ChatsTab.tsx +++ b/ts/state/smart/ChatsTab.tsx @@ -14,10 +14,10 @@ import { usePrevious } from '../../hooks/usePrevious'; import { TargetedMessageSource } from '../ducks/conversationsEnums'; import type { ConversationsStateType } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations'; +import { useToastActions } from '../ducks/toast'; import type { StateType } from '../reducer'; import { strictAssert } from '../../util/assert'; -import { showToast } from '../../util/showToast'; -import { ToastStickerPackInstallFailed } from '../../components/ToastStickerPackInstallFailed'; +import { ToastType } from '../../types/Toast'; import { getNavTabsCollapsed } from '../selectors/items'; import { useItemsActions } from '../ducks/items'; import { getHasAnyFailedStorySends } from '../selectors/stories'; @@ -56,6 +56,7 @@ export function SmartChatsTab(): JSX.Element { } = useConversationsActions(); const { showWhatsNewModal } = useGlobalModalActions(); const { toggleNavTabsCollapse } = useItemsActions(); + const { showToast } = useToastActions(); const lastOpenedConversationId = useRef(); @@ -121,7 +122,7 @@ export function SmartChatsTab(): JSX.Element { } function packInstallFailed() { - showToast(ToastStickerPackInstallFailed); + showToast({ toastType: ToastType.StickerPackInstallFailed }); } window.Whisper.events.on('pack-install-failed', packInstallFailed); @@ -133,7 +134,7 @@ export function SmartChatsTab(): JSX.Element { window.Whisper.events.off('refreshConversation', refreshConversation); window.Whisper.events.off('setupAsNewDevice', unload); }; - }, [onConversationClosed, prevConversationId, showConversation]); + }, [onConversationClosed, prevConversationId, showConversation, showToast]); useEffect(() => { if (!selectedConversationId) { diff --git a/ts/state/smart/CompositionRecording.tsx b/ts/state/smart/CompositionRecording.tsx index 30b37e0e4..b859064db 100644 --- a/ts/state/smart/CompositionRecording.tsx +++ b/ts/state/smart/CompositionRecording.tsx @@ -7,6 +7,7 @@ import { CompositionRecording } from '../../components/CompositionRecording'; import { mapDispatchToProps } from '../actions'; import { useAudioRecorderActions } from '../ducks/audioRecorder'; import { useComposerActions } from '../ducks/composer'; +import { useToastActions } from '../ducks/toast'; import { getSelectedConversationId } from '../selectors/conversations'; import { getIntl } from '../selectors/user'; @@ -22,6 +23,7 @@ export function SmartCompositionRecording({ const { cancelRecording, completeRecording } = useAudioRecorderActions(); const { sendMultiMediaMessage } = useComposerActions(); + const { hideToast, showToast } = useToastActions(); const handleCancel = useCallback(() => { cancelRecording(); @@ -54,6 +56,8 @@ export function SmartCompositionRecording({ errorRecording={mapDispatchToProps.errorRecording} addAttachment={mapDispatchToProps.addAttachment} completeRecording={mapDispatchToProps.completeRecording} + showToast={showToast} + hideToast={hideToast} /> ); } diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx index 0215bf37e..21ac55c12 100644 --- a/ts/state/smart/GlobalModalContainer.tsx +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -13,6 +13,7 @@ import { SmartContactModal } from './ContactModal'; import { SmartEditHistoryMessagesModal } from './EditHistoryMessagesModal'; import { SmartForwardMessagesModal } from './ForwardMessagesModal'; import { SmartProfileEditorModal } from './ProfileEditorModal'; +import { SmartUsernameOnboardingModal } from './UsernameOnboardingModal'; import { SmartSafetyNumberModal } from './SafetyNumberModal'; import { SmartSendAnywayDialog } from './SendAnywayDialog'; import { SmartShortcutGuideModal } from './ShortcutGuideModal'; @@ -31,6 +32,10 @@ function renderProfileEditor(): JSX.Element { return ; } +function renderUsernameOnboarding(): JSX.Element { + return ; +} + function renderContactModal(): JSX.Element { return ; } @@ -77,6 +82,7 @@ export function SmartGlobalModalContainer(): JSX.Element { isSignalConnectionsVisible, isStoriesSettingsVisible, isWhatsNewVisible, + usernameOnboardingState, safetyNumberChangedBlockingData, safetyNumberModalContactId, sendEditWarningData, @@ -157,6 +163,7 @@ export function SmartGlobalModalContainer(): JSX.Element { renderDeleteMessagesModal={renderDeleteMessagesModal} renderForwardMessagesModal={renderForwardMessagesModal} renderProfileEditor={renderProfileEditor} + renderUsernameOnboarding={renderUsernameOnboarding} renderSafetyNumber={renderSafetyNumber} renderSendAnywayDialog={renderSendAnywayDialog} renderShortcutGuideModal={renderShortcutGuideModal} @@ -171,6 +178,7 @@ export function SmartGlobalModalContainer(): JSX.Element { theme={theme} toggleSignalConnectionsModal={toggleSignalConnectionsModal} userNotFoundModalState={userNotFoundModalState} + usernameOnboardingState={usernameOnboardingState} isAuthorizingArtCreator={isAuthorizingArtCreator} authArtCreatorData={authArtCreatorData} cancelAuthorizeArtCreator={cancelAuthorizeArtCreator} diff --git a/ts/state/smart/InstallScreen.tsx b/ts/state/smart/InstallScreen.tsx index 6e7535d3d..9c3a27264 100644 --- a/ts/state/smart/InstallScreen.tsx +++ b/ts/state/smart/InstallScreen.tsx @@ -24,6 +24,7 @@ import { } from '../../components/InstallScreen'; import { InstallError } from '../../components/installScreen/InstallScreenErrorStep'; import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep'; +import { WidthBreakpoint } from '../../components/_util'; import { HTTPError } from '../../textsecure/Errors'; import { isRecord } from '../../util/isRecord'; import * as Errors from '../../types/errors'; @@ -32,6 +33,7 @@ import OS from '../../util/os/osMain'; import { SECOND } from '../../util/durations'; import { BackOff } from '../../util/BackOff'; import { drop } from '../../util/drop'; +import { SmartToastManager } from './ToastManager'; type PropsType = ComponentProps; @@ -328,5 +330,13 @@ export function SmartInstallScreen(): ReactElement { throw missingCaseError(state); } - return ; + return ( + <> + + + + ); } diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index e312955bc..bb41385e4 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -77,6 +77,7 @@ import { SmartMessageSearchResult } from './MessageSearchResult'; import { SmartNetworkStatus } from './NetworkStatus'; import { SmartRelinkDialog } from './RelinkDialog'; import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog'; +import { SmartToastManager } from './ToastManager'; import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog'; import { SmartUpdateDialog } from './UpdateDialog'; import { SmartCaptchaDialog } from './CaptchaDialog'; @@ -116,6 +117,17 @@ function renderUnsupportedOSDialog( ): JSX.Element { return ; } +function renderToastManager(props: { + containerWidthBreakpoint: WidthBreakpoint; +}): JSX.Element { + return ; +} + +function renderToastManagerWithoutMegaphone(props: { + containerWidthBreakpoint: WidthBreakpoint; +}): JSX.Element { + return ; +} const getModeSpecificProps = ( state: StateType @@ -223,6 +235,10 @@ const mapStateToProps = (state: StateType) => { unsupportedOSDialogType = 'warning'; } + const composerStep = getComposerStep(state); + const showArchived = getShowArchived(state); + const hasSearchQuery = isSearching(state); + return { hasNetworkDialog: hasNetworkDialog(state), hasExpiredDialog, @@ -238,7 +254,7 @@ const mapStateToProps = (state: StateType) => { preferredWidthFromStorage: getPreferredLeftPaneWidth(state), selectedConversationId: getSelectedConversationId(state), targetedMessageId: getTargetedMessage(state)?.id, - showArchived: getShowArchived(state), + showArchived, getPreferredBadge: getPreferredBadgeSelector(state), i18n: getIntl(state), isMacOS: getIsMacOS(state), @@ -253,6 +269,10 @@ const mapStateToProps = (state: StateType) => { renderCrashReportDialog, renderExpiredBuildDialog, renderUnsupportedOSDialog, + renderToastManager: + composerStep == null && !showArchived && !hasSearchQuery + ? renderToastManager + : renderToastManagerWithoutMegaphone, lookupConversationWithoutServiceId, theme: getTheme(state), }; diff --git a/ts/state/smart/ProfileEditorModal.tsx b/ts/state/smart/ProfileEditorModal.tsx index 824ad5f9f..39c339ef3 100644 --- a/ts/state/smart/ProfileEditorModal.tsx +++ b/ts/state/smart/ProfileEditorModal.tsx @@ -14,7 +14,6 @@ import { getIntl } from '../selectors/user'; import { getEmojiSkinTone, getUsernamesEnabled, - getHasCompletedUsernameOnboarding, getHasCompletedUsernameLinkOnboarding, getUsernameCorrupted, getUsernameLinkColor, @@ -53,8 +52,6 @@ function mapStateToProps( const recentEmojis = selectRecentEmojis(state); const skinTone = getEmojiSkinTone(state); const isUsernameFlagEnabled = getUsernamesEnabled(state); - const hasCompletedUsernameOnboarding = - getHasCompletedUsernameOnboarding(state); const hasCompletedUsernameLinkOnboarding = getHasCompletedUsernameLinkOnboarding(state); const usernameEditState = getUsernameEditState(state); @@ -72,9 +69,9 @@ function mapStateToProps( conversationId, familyName, firstName: String(firstName), - hasCompletedUsernameOnboarding, hasCompletedUsernameLinkOnboarding, hasError: state.globalModals.profileEditorHasError, + initialEditState: state.globalModals.profileEditorInitialEditState, i18n: getIntl(state), isUsernameFlagEnabled, recentEmojis, diff --git a/ts/state/smart/StoriesTab.tsx b/ts/state/smart/StoriesTab.tsx index d63584875..323a3125a 100644 --- a/ts/state/smart/StoriesTab.tsx +++ b/ts/state/smart/StoriesTab.tsx @@ -7,6 +7,8 @@ import { useSelector } from 'react-redux'; import type { LocalizerType } from '../../types/Util'; import type { StateType } from '../reducer'; import { SmartStoryCreator } from './StoryCreator'; +import { SmartToastManager } from './ToastManager'; +import type { WidthBreakpoint } from '../../components/_util'; import { StoriesTab } from '../../components/StoriesTab'; import { getMaximumOutgoingAttachmentSizeInKb } from '../../types/AttachmentSize'; import type { ConfigKeyType } from '../../RemoteConfig'; @@ -38,6 +40,12 @@ function renderStoryCreator(): JSX.Element { return ; } +function renderToastManager(props: { + containerWidthBreakpoint: WidthBreakpoint; +}): JSX.Element { + return ; +} + export function SmartStoriesTab(): JSX.Element | null { const storiesActions = useStoriesActions(); const { @@ -122,6 +130,7 @@ export function SmartStoriesTab(): JSX.Element | null { preferredLeftPaneWidth={preferredLeftPaneWidth} preferredWidthFromStorage={preferredWidthFromStorage} renderStoryCreator={renderStoryCreator} + renderToastManager={renderToastManager} retryMessageSend={retryMessageSend} savePreferredLeftPaneWidth={savePreferredLeftPaneWidth} showConversation={showConversation} diff --git a/ts/state/smart/ToastManager.tsx b/ts/state/smart/ToastManager.tsx new file mode 100644 index 000000000..776eefa53 --- /dev/null +++ b/ts/state/smart/ToastManager.tsx @@ -0,0 +1,107 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { useSelector } from 'react-redux'; + +import type { AnyActionableMegaphone } from '../../types/Megaphone'; +import { MegaphoneType } from '../../types/Megaphone'; +import { UsernameOnboardingState } from '../../types/globalModals'; +import OS from '../../util/os/osMain'; +import { drop } from '../../util/drop'; +import { getIntl } from '../selectors/user'; +import { + getGlobalModalsState, + isShowingAnyModal as getIsShowingAnyModal, +} from '../selectors/globalModals'; +import { hasSelectedStoryData } from '../selectors/stories'; +import { shouldShowLightbox } from '../selectors/lightbox'; +import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling'; +import { getSelectedNavTab } from '../selectors/nav'; +import type { StateType } from '../reducer'; +import { useConversationsActions } from '../ducks/conversations'; +import type { ConversationsStateType } from '../ducks/conversations'; +import { useToastActions } from '../ducks/toast'; +import { useGlobalModalActions } from '../ducks/globalModals'; +import { NavTab } from '../ducks/nav'; +import { + getUsernamesEnabled, + getHasCompletedUsernameOnboarding, +} from '../selectors/items'; +import { ToastManager } from '../../components/ToastManager'; +import type { WidthBreakpoint } from '../../components/_util'; + +export type SmartPropsType = Readonly<{ + disableMegaphone?: boolean; + containerWidthBreakpoint: WidthBreakpoint; +}>; + +export function SmartToastManager({ + disableMegaphone = false, + containerWidthBreakpoint, +}: SmartPropsType): JSX.Element { + const i18n = useSelector(getIntl); + const isUsernameFlagEnabled = useSelector(getUsernamesEnabled); + const hasCompletedUsernameOnboarding = useSelector( + getHasCompletedUsernameOnboarding + ); + const toast = useSelector((state: StateType) => state.toast.toast); + const globalModals = useSelector(getGlobalModalsState); + const isShowingAnyModal = useSelector(getIsShowingAnyModal); + const isShowingStory = useSelector(hasSelectedStoryData); + const isShowingLightbox = useSelector(shouldShowLightbox); + const isInFullScreenCall = useSelector(getIsInFullScreenCall); + + const selectedNavTab = useSelector(getSelectedNavTab); + const { selectedConversationId } = useSelector< + StateType, + ConversationsStateType + >(state => state.conversations); + + const { onUndoArchive } = useConversationsActions(); + + const { openFileInFolder, hideToast } = useToastActions(); + + const { toggleUsernameOnboarding } = useGlobalModalActions(); + + let megaphone: AnyActionableMegaphone | undefined; + + if ( + isUsernameFlagEnabled && + !hasCompletedUsernameOnboarding && + globalModals.usernameOnboardingState === UsernameOnboardingState.NeverShown + ) { + megaphone = { + type: MegaphoneType.UsernameOnboarding, + onLearnMore: toggleUsernameOnboarding, + onDismiss: () => { + drop(window.storage.put('hasCompletedUsernameOnboarding', true)); + }, + }; + } + + const centerToast = + isShowingAnyModal || + isShowingStory || + isShowingLightbox || + isInFullScreenCall; + + const isCompositionAreaVisible = + selectedNavTab === NavTab.Chats && Boolean(selectedConversationId); + + return ( + window.IPC.showDebugLog()} + onUndoArchive={onUndoArchive} + openFileInFolder={openFileInFolder} + hideToast={hideToast} + centerToast={centerToast} + containerWidthBreakpoint={containerWidthBreakpoint} + isCompositionAreaVisible={isCompositionAreaVisible} + /> + ); +} diff --git a/ts/state/smart/UsernameOnboardingModal.tsx b/ts/state/smart/UsernameOnboardingModal.tsx new file mode 100644 index 000000000..d9a2c59bf --- /dev/null +++ b/ts/state/smart/UsernameOnboardingModal.tsx @@ -0,0 +1,43 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; + +import { UsernameOnboardingModal } from '../../components/UsernameOnboardingModal'; +import { EditState } from '../../components/ProfileEditor'; +import { getIntl } from '../selectors/user'; +import { useGlobalModalActions } from '../ducks/globalModals'; +import { useUsernameActions } from '../ducks/username'; + +export function SmartUsernameOnboardingModal(): JSX.Element { + const i18n = useSelector(getIntl); + const { toggleProfileEditor, toggleUsernameOnboarding } = + useGlobalModalActions(); + const { openUsernameReservationModal } = useUsernameActions(); + + const onNext = useCallback(async () => { + await window.storage.put('hasCompletedUsernameOnboarding', true); + openUsernameReservationModal(); + toggleProfileEditor(EditState.Username); + toggleUsernameOnboarding(); + }, [ + toggleProfileEditor, + toggleUsernameOnboarding, + openUsernameReservationModal, + ]); + + const onSkip = useCallback(async () => { + await window.storage.put('hasCompletedUsernameOnboarding', true); + toggleUsernameOnboarding(); + }, [toggleUsernameOnboarding]); + + return ( + + ); +} diff --git a/ts/test-both/state/selectors/items_test.ts b/ts/test-both/state/selectors/items_test.ts index 60da1e837..1b403eb5f 100644 --- a/ts/test-both/state/selectors/items_test.ts +++ b/ts/test-both/state/selectors/items_test.ts @@ -54,7 +54,8 @@ describe('both/state/selectors/items', () => { 0.1, 1.2, NaN, - ].forEach(skinTone => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- for testing + ].forEach((skinTone: any) => { const state = getRootState({ skinTone }); assert.strictEqual(getEmojiSkinTone(state), 0); }); diff --git a/ts/test-mock/pnp/username_test.ts b/ts/test-mock/pnp/username_test.ts index 4c0eebc07..3d20a83f7 100644 --- a/ts/test-mock/pnp/username_test.ts +++ b/ts/test-mock/pnp/username_test.ts @@ -156,9 +156,6 @@ describe('pnp/username', function (this: Mocha.Suite) { const profileEditor = window.locator('.ProfileEditor'); await profileEditor.locator('.ProfileEditor__row >> "Username"').click(); - debug('skipping onboarding'); - await profileEditor.locator('.module-Button >> "Continue"').click(); - debug('entering new username'); const usernameField = profileEditor.locator('.Input__input'); await usernameField.type(NICKNAME); diff --git a/ts/types/Megaphone.ts b/ts/types/Megaphone.ts new file mode 100644 index 000000000..304b5e160 --- /dev/null +++ b/ts/types/Megaphone.ts @@ -0,0 +1,20 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export enum MegaphoneType { + UsernameOnboarding = 'UsernameOnboarding', +} + +export type UsernameOnboardingMegaphoneType = { + type: MegaphoneType.UsernameOnboarding; +}; + +export type UsernameOnboardingActionableMegaphoneType = + UsernameOnboardingMegaphoneType & { + onLearnMore: () => void; + onDismiss: () => void; + }; + +export type AnyMegaphone = UsernameOnboardingMegaphoneType; + +export type AnyActionableMegaphone = UsernameOnboardingActionableMegaphoneType; diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index 5b507acb1..60fb454cb 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -8,6 +8,8 @@ export enum ToastType { Blocked = 'Blocked', BlockedGroup = 'BlockedGroup', CallHistoryCleared = 'CallHistoryCleared', + CaptchaFailed = 'CaptchaFailed', + CaptchaSolved = 'CaptchaSolved', CannotEditMessage = 'CannotEditMessage', CannotForwardEmptyMessage = 'CannotForwardEmptyMessage', CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments', @@ -21,20 +23,28 @@ export enum ToastType { CopiedUsername = 'CopiedUsername', CopiedUsernameLink = 'CopiedUsernameLink', DangerousFileType = 'DangerousFileType', + DecryptionError = 'DecryptionError', + DebugLogError = 'DebugLogError', DeleteForEveryoneFailed = 'DeleteForEveryoneFailed', Error = 'Error', Expired = 'Expired', FailedToDeleteUsername = 'FailedToDeleteUsername', + FailedToFetchPhoneNumber = 'FailedToFetchPhoneNumber', + FailedToFetchUsername = 'FailedToFetchUsername', FileSaved = 'FileSaved', FileSize = 'FileSize', + GroupLinkCopied = 'GroupLinkCopied', InvalidConversation = 'InvalidConversation', LeftGroup = 'LeftGroup', + LinkCopied = 'LinkCopied', + LoadingFullLogs = 'LoadingFullLogs', MaxAttachments = 'MaxAttachments', MessageBodyTooLong = 'MessageBodyTooLong', OriginalMessageNotFound = 'OriginalMessageNotFound', PinnedConversationsFull = 'PinnedConversationsFull', ReactionFailed = 'ReactionFailed', ReportedSpamAndBlocked = 'ReportedSpamAndBlocked', + StickerPackInstallFailed = 'StickerPackInstallFailed', StoryMuted = 'StoryMuted', StoryReact = 'StoryReact', StoryReply = 'StoryReply', @@ -48,6 +58,8 @@ export enum ToastType { UnsupportedMultiAttachment = 'UnsupportedMultiAttachment', UnsupportedOS = 'UnsupportedOS', UserAddedToGroup = 'UserAddedToGroup', + VoiceNoteLimit = 'VoiceNoteLimit', + VoiceNoteMustBeTheOnlyAttachment = 'VoiceNoteMustBeTheOnlyAttachment', WhoCanFindMeReadOnly = 'WhoCanFindMeReadOnly', } @@ -64,6 +76,8 @@ export type AnyToast = | { toastType: ToastType.CannotOpenGiftBadgeIncoming } | { toastType: ToastType.CannotOpenGiftBadgeOutgoing } | { toastType: ToastType.CannotStartGroupCall } + | { toastType: ToastType.CaptchaFailed } + | { toastType: ToastType.CaptchaSolved } | { toastType: ToastType.ConversationArchived; parameters: { conversationId: string }; @@ -74,23 +88,37 @@ export type AnyToast = | { toastType: ToastType.CopiedUsername } | { toastType: ToastType.CopiedUsernameLink } | { toastType: ToastType.DangerousFileType } + | { toastType: ToastType.DebugLogError } | { toastType: ToastType.DeleteForEveryoneFailed } | { toastType: ToastType.Error } | { toastType: ToastType.Expired } | { toastType: ToastType.FailedToDeleteUsername } + | { toastType: ToastType.FailedToFetchPhoneNumber } + | { toastType: ToastType.FailedToFetchUsername } | { toastType: ToastType.FileSaved; parameters: { fullPath: string } } | { toastType: ToastType.FileSize; parameters: { limit: number; units: string }; } + | { toastType: ToastType.GroupLinkCopied } + | { + toastType: ToastType.DecryptionError; + parameters: { + name: string; + deviceId: number; + }; + } | { toastType: ToastType.InvalidConversation } | { toastType: ToastType.LeftGroup } + | { toastType: ToastType.LinkCopied } + | { toastType: ToastType.LoadingFullLogs } | { toastType: ToastType.MaxAttachments } | { toastType: ToastType.MessageBodyTooLong } | { toastType: ToastType.OriginalMessageNotFound } | { toastType: ToastType.PinnedConversationsFull } | { toastType: ToastType.ReactionFailed } | { toastType: ToastType.ReportedSpamAndBlocked } + | { toastType: ToastType.StickerPackInstallFailed } | { toastType: ToastType.StoryMuted } | { toastType: ToastType.StoryReact } | { toastType: ToastType.StoryReply } @@ -110,4 +138,6 @@ export type AnyToast = toastType: ToastType.UserAddedToGroup; parameters: { contact: string; group: string }; } + | { toastType: ToastType.VoiceNoteLimit } + | { toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment } | { toastType: ToastType.WhoCanFindMeReadOnly }; diff --git a/ts/types/globalModals.ts b/ts/types/globalModals.ts new file mode 100644 index 000000000..03376b496 --- /dev/null +++ b/ts/types/globalModals.ts @@ -0,0 +1,8 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export enum UsernameOnboardingState { + NeverShown = 'NeverShown', + Open = 'Open', + Closed = 'Closed', +} diff --git a/ts/util/copyGroupLink.ts b/ts/util/copyGroupLink.ts index 9fcc998b0..e990d7052 100644 --- a/ts/util/copyGroupLink.ts +++ b/ts/util/copyGroupLink.ts @@ -1,10 +1,9 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { showToast } from './showToast'; -import { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied'; +import { ToastType } from '../types/Toast'; export async function copyGroupLink(groupLink: string): Promise { await window.navigator.clipboard.writeText(groupLink); - showToast(ToastGroupLinkCopied); + window.reduxActions.toast.showToast({ toastType: ToastType.GroupLinkCopied }); } diff --git a/ts/util/handleRetry.ts b/ts/util/handleRetry.ts index d77825a0b..2d9c8f0af 100644 --- a/ts/util/handleRetry.ts +++ b/ts/util/handleRetry.ts @@ -18,11 +18,7 @@ import * as RemoteConfig from '../RemoteConfig'; import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; import type { AciString, ServiceIdString } from '../types/ServiceId'; -import { - ToastInternalError, - ToastInternalErrorKind, -} from '../components/ToastInternalError'; -import { showToast } from './showToast'; +import { ToastType } from '../types/Toast'; import * as Errors from '../types/errors'; import type { ConversationModel } from '../models/conversations'; @@ -200,11 +196,12 @@ function maybeShowDecryptionToast( } log.info(`maybeShowDecryptionToast/${logId}: Showing decryption error toast`); - showToast(ToastInternalError, { - kind: ToastInternalErrorKind.DecryptionError, - deviceId, - name, - onShowDebugLog: () => window.IPC.showDebugLog(), + window.reduxActions.toast.showToast({ + toastType: ToastType.DecryptionError, + parameters: { + deviceId, + name, + }, }); } diff --git a/ts/util/lookupConversationWithoutServiceId.ts b/ts/util/lookupConversationWithoutServiceId.ts index 160fb9872..130ec07f0 100644 --- a/ts/util/lookupConversationWithoutServiceId.ts +++ b/ts/util/lookupConversationWithoutServiceId.ts @@ -3,14 +3,12 @@ import { usernames, LibSignalErrorBase } from '@signalapp/libsignal-client'; -import { ToastFailedToFetchUsername } from '../components/ToastFailedToFetchUsername'; -import { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber'; import type { UserNotFoundModalStateType } from '../state/ducks/globalModals'; import * as log from '../logging/log'; import type { AciString } from '../types/ServiceId'; import * as Errors from '../types/errors'; +import { ToastType } from '../types/Toast'; import { HTTPError } from '../textsecure/Errors'; -import { showToast } from './showToast'; import { strictAssert } from './assert'; import type { UUIDFetchStateKeyType } from './uuidFetchState'; import { getServiceIdsForE164s } from './getServiceIdsForE164s'; @@ -121,9 +119,13 @@ export async function lookupConversationWithoutServiceId( ); if (options.type === 'e164') { - showToast(ToastFailedToFetchPhoneNumber); + window.reduxActions.toast.showToast({ + toastType: ToastType.FailedToFetchPhoneNumber, + }); } else { - showToast(ToastFailedToFetchUsername); + window.reduxActions.toast.showToast({ + toastType: ToastType.FailedToFetchUsername, + }); } return undefined; diff --git a/ts/util/processAttachment.ts b/ts/util/processAttachment.ts index 016d68a20..bf66874f3 100644 --- a/ts/util/processAttachment.ts +++ b/ts/util/processAttachment.ts @@ -17,9 +17,8 @@ import { fileToBytes } from './fileToBytes'; import { handleImageAttachment } from './handleImageAttachment'; import { handleVideoAttachment } from './handleVideoAttachment'; import { isHeic, stringToMIMEType } from '../types/MIME'; +import { ToastType } from '../types/Toast'; import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome'; -import { showToast } from './showToast'; -import { ToastFileSize } from '../components/ToastFileSize'; export async function processAttachment( file: File, @@ -80,7 +79,10 @@ function isAttachmentSizeOkay(attachment: Readonly): boolean { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if ((attachment.data.byteLength / KIBIBYTE).toFixed(4) >= limitKb) { - showToast(ToastFileSize, getRenderDetailsForLimit(limitKb)); + window.reduxActions.toast.showToast({ + toastType: ToastType.FileSize, + parameters: getRenderDetailsForLimit(limitKb), + }); return false; } diff --git a/ts/util/showToast.tsx b/ts/util/showToast.tsx deleted file mode 100644 index 001f7f292..000000000 --- a/ts/util/showToast.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed'; -import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved'; -import type { - ToastInternalError, - ToastPropsType as ToastInternalErrorPropsType, -} from '../components/ToastInternalError'; -import type { - ToastFileSize, - ToastPropsType as ToastFileSizePropsType, -} from '../components/ToastFileSize'; -import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied'; -import type { ToastLinkCopied } from '../components/ToastLinkCopied'; -import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs'; - -import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed'; -import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit'; -import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment'; - -export function showToast(Toast: typeof ToastCaptchaFailed): void; -export function showToast(Toast: typeof ToastCaptchaSolved): void; -export function showToast( - Toast: typeof ToastInternalError, - props: ToastInternalErrorPropsType -): void; -export function showToast( - Toast: typeof ToastFileSize, - props: ToastFileSizePropsType -): void; -export function showToast(Toast: typeof ToastGroupLinkCopied): void; -export function showToast(Toast: typeof ToastLinkCopied): void; -export function showToast(Toast: typeof ToastLoadingFullLogs): void; -export function showToast(Toast: typeof ToastStickerPackInstallFailed): void; -export function showToast(Toast: typeof ToastVoiceNoteLimit): void; -export function showToast( - Toast: typeof ToastVoiceNoteMustBeOnlyAttachment -): void; - -// eslint-disable-next-line max-len -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types -export function showToast(Toast: any, props = {}): void { - const node = document.getElementById('toast'); - - function onClose() { - if (!node) { - return; - } - - unmountComponentAtNode(node); - } - - render(, node); -}