diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6813abf4d..40c6bcbe9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1015,6 +1015,14 @@ "message": "Cannot Update", "description": "Shown as the title of our update error dialogs on windows" }, + "muted": { + "message": "Muted", + "description": "Shown in a button when a conversation is muted" + }, + "mute": { + "message": "Mute", + "description": "Shown in a button when a conversation is unmuted and can be muted" + }, "cannotUpdateDetail": { "message": "Signal Desktop failed to update, but there is a new version available. Please go to $url$ and install the new version manually, then either contact support or file a bug about this problem.", "description": "Shown if a general error happened while trying to install update package", @@ -1113,10 +1121,6 @@ "showMembers": { "message": "Show members" }, - "resetSession": { - "message": "Reset session", - "description": "This is a menu item for resetting the session, using the imperative case, as in a command." - }, "showSafetyNumber": { "message": "View safety number" }, @@ -3271,13 +3275,7 @@ }, "MessageRequests--message-group": { "message": "Join this group and share your name and photo with its members? They won’t know you’ve seen their messages until you accept.", - "description": "Shown as the message for a message request in a group", - "placeholders": { - "name": { - "content": "$1", - "example": "Cayce Pollard" - } - } + "description": "Shown as the message for a message request in a group" }, "MessageRequests--message-group-blocked": { "message": "Unblock this group and share your name and photo with its members? You won't receive any messages until you unblock them.", @@ -3303,23 +3301,11 @@ }, "MessageRequests--unblock-direct-confirm-body": { "message": "You will be able to message and call each other.", - "description": "Shown as the body in the confirmation modal for unblocking a private message request", - "placeholders": { - "name": { - "content": "$1", - "example": "Cayce Pollard" - } - } + "description": "Shown as the body in the confirmation modal for unblocking a private message request" }, "MessageRequests--unblock-group-confirm-body": { "message": "Group members will be able to add you to this group again.", - "description": "Shown as the body in the confirmation modal for unblocking a group message request", - "placeholders": { - "name": { - "content": "$1", - "example": "Cayce Pollard" - } - } + "description": "Shown as the body in the confirmation modal for unblocking a group message request" }, "MessageRequests--block-and-report-spam": { "message": "Report Spam and Block", @@ -5354,6 +5340,14 @@ "message": "Group settings", "description": "This is a button in the conversation context menu to show group settings" }, + "showConversationDetails--direct": { + "message": "Chat settings", + "description": "This is a button in the conversation context menu to show chat settings" + }, + "ConversationDetails__unmute--title": { + "message": "Unmute this chat?", + "description": "Title for the modal to unmute a chat" + }, "ConversationDetails--group-link": { "message": "Group link", "description": "This is the label for the group link management panel" diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index f33a4cd9e..12d95b33a 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2273,602 +2273,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', } // Brought this up here to add specificity -button.module-conversation-details__action-button { +button.ConversationDetails__action-button { margin-left: 16px; } -.module-conversation-details { - &-header { - &__root { - align-items: center; - display: flex; - flex-direction: column; - margin: 0 0 24px 0; - text-align: center; - width: 100%; - } - - &__root--editable { - @include button-reset(); - } - - &__root--editable { - cursor: pointer; - } - - &__title { - @include font-title-1; - align-items: center; - display: flex; - justify-content: center; - padding-bottom: 8px; - padding-top: 12px; - } - - &__subtitle { - @include font-body-1; - color: $color-gray-60; - justify-content: center; - padding-bottom: 6px; - - @include dark-theme { - color: $color-gray-25; - } - } - - &__root--editable &__title { - $icon: '../images/icons/v2/compose-solid-24.svg'; - - &::after { - $size: 24px; - - content: ''; - height: $size; - left: $size + 13px; - margin-left: -$size; - opacity: 0; - position: relative; - transition: opacity 100ms ease-out; - width: $size; - - @include light-theme { - @include color-svg($icon, $color-gray-60); - } - @include dark-theme { - @include color-svg($icon, $color-gray-25); - } - } - } - - &__root--editable:hover &__title::after { - opacity: 1; - } - } - - &__chat-color { - @include color-bubble(20px); - } - - &-membership-list { - &__add-members-icon { - @mixin plus-icon($color) { - @include color-svg('../images/icons/v2/plus-24.svg', $color); - content: ''; - display: block; - height: 16px; - width: 16px; - } - - align-items: center; - border-radius: 100%; - display: flex; - height: 32px; - justify-content: center; - width: 32px; - - @include light-theme { - background: $color-gray-02; - &::before { - @include plus-icon($color-black); - } - } - @include dark-theme { - background: $color-gray-90; - &::before { - @include plus-icon($color-gray-15); - } - } - } - } - - &__leave-group { - color: $color-accent-red; - - &--disabled { - @include light-theme { - color: $color-gray-60; - } - - @include dark-theme { - color: $color-gray-25; - } - } - } - - &__block-group { - color: $color-accent-red; - } - - &__tabs { - display: flex; - justify-content: space-around; - } - - &__tab { - @include font-body-1; - cursor: pointer; - padding: 15px; - - &:focus { - @include mouse-mode { - outline: none; - } - } - - &--selected { - @include font-body-1-bold; - border-bottom: 2px solid $color-black; - } - } - - &__pending--info { - @include font-subtitle; - @include light-theme { - color: $color-gray-60; - } - @include dark-theme { - color: $color-gray-25; - } - padding: 0 28px; - padding-top: 16px; - } - - &-icon { - &__button { - background: none; - border: none; - padding: none; - - &:focus { - @include mouse-mode { - outline: none; - } - } - } - - &__icon { - height: 32px; - width: 32px; - display: flex; - align-items: center; - justify-content: center; - - &::after { - display: block; - content: ''; - width: 24px; - height: 24px; - -webkit-mask-size: 100%; - } - - &--color { - &::after { - -webkit-mask: url(../images/icons/v2/color-outline-24.svg) no-repeat - center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--timer { - &::after { - -webkit-mask: url(../images/icons/v2/timer-disabled-outline-24.svg) - no-repeat center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--notifications { - &::after { - -webkit-mask: url('../images/icons/v2/sound-outline-24.svg') no-repeat - center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--mute { - &::after { - @include light-theme { - -webkit-mask: url('../images/icons/v2/bell-disabled-outline-24.svg') - no-repeat center; - background-color: $color-gray-75; - } - - @include dark-theme { - -webkit-mask: url('../images/icons/v2/bell-disabled-solid-24.svg') - no-repeat center; - background-color: $color-gray-15; - } - } - } - - &--mention { - &::after { - -webkit-mask: url('../images/icons/v2/at-24.svg') no-repeat center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--lock { - &::after { - -webkit-mask: url(../images/icons/v2/lock-outline-24.svg) no-repeat - center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--approve { - &::after { - -webkit-mask: url(../images/icons/v2/check-24.svg) no-repeat center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--link { - &::after { - -webkit-mask: url(../images/icons/v2/link-16.svg) no-repeat center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--share { - &::after { - -webkit-mask: url(../images/icons/v2/share-ios-24.svg) no-repeat - center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--reset { - &::after { - -webkit-mask: url(../images/icons/v2/refresh-24.svg) no-repeat center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--trash { - &::after { - -webkit-mask: url(../images/icons/v2/trash-outline-24.svg) no-repeat - center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--invites { - &::after { - -webkit-mask: url(../images/icons/v2/pending-invite-24.svg) no-repeat - center; - - @include light-theme { - background-color: $color-gray-75; - } - - @include dark-theme { - background-color: $color-gray-15; - } - } - } - - &--down { - border-radius: 18px; - @include light-theme { - background-color: $color-gray-02; - } - - @include dark-theme { - background-color: $color-gray-90; - } - - &::after { - -webkit-mask: url(../images/icons/v2/chevron-down-16.svg) no-repeat - center; - - @include light-theme { - background-color: $color-gray-60; - } - - @include dark-theme { - background-color: $color-gray-25; - } - } - } - - &--leave { - &::after { - -webkit-mask: url(../images/icons/v2/leave-24.svg) no-repeat center; - background-color: $color-accent-red; - } - - &--disabled::after { - @include light-theme { - background-color: $color-gray-60; - } - - @include dark-theme { - background-color: $color-gray-25; - } - } - } - - &--block { - &::after { - -webkit-mask: url(../images/icons/v2/block-24.svg) no-repeat center; - background-color: $color-accent-red; - } - } - } - } - - &-media-list { - &__root { - display: flex; - justify-content: center; - padding: 0 20px; - padding-bottom: 24px; - - .module-media-grid-item { - border-radius: 4px; - height: auto; - margin: 0 4px; - max-height: 94px; - overflow: hidden; - width: calc(100% / 6); - - .module-media-grid-item__icon { - &::before { - content: ''; - display: block; - padding-top: 100%; - } - } - - .module-media-grid-item__image-container, - img { - margin: 0; - } - } - } - - &__show-all { - background: none; - border: none; - padding: 0; - - @include light-theme { - color: $color-gray-95; - } - @include dark-theme { - color: $color-gray-05; - } - } - } - - &-panel-row { - $row-root-selector: '#{&}__root'; - &__root { - align-items: center; - border-radius: 5px; - border: 2px solid transparent; - display: flex; - padding: 8px 24px; - user-select: none; - width: 100%; - - &--button { - color: inherit; - background: none; - - &:hover:not(:disabled) { - @include light-theme { - background-color: $color-gray-02; - } - - @include dark-theme { - background-color: $color-gray-90; - } - - & .module-conversation-details-panel-row__actions { - opacity: 1; - } - } - } - - &:focus { - outline: none; - } - - @mixin keyboard-focus-state($color) { - &:focus { - border-color: $color; - } - } - - @include keyboard-mode { - @include keyboard-focus-state($color-ultramarine); - } - @include dark-keyboard-mode { - @include keyboard-focus-state($color-ultramarine-light); - } - } - - &__icon { - margin-right: 12px; - flex-shrink: 0; - } - - &__label { - flex-grow: 1; - text-align: left; - margin-right: 12px; - } - - &__info { - @include font-body-2; - margin-top: 4px; - - @include light-theme { - color: $color-gray-60; - } - - @include dark-theme { - color: $color-gray-25; - } - } - - &__right { - position: relative; - color: $color-gray-45; - min-width: 143px; - } - - &__actions { - margin-left: 12px; - overflow: hidden; - opacity: 0; - - #{$row-root-selector}:hover &, - #{$row-root-selector}:focus-within & { - opacity: 1; - } - } - } - - &-panel-section { - &__root { - position: relative; - - &:not(:first-child)::before { - border-top: 1px solid transparent; - - @include light-theme { - border-top-color: $color-gray-15; - } - - @include dark-theme { - border-top-color: $color-gray-65; - } - - content: ''; - display: block; - left: 0; - margin: 0; - position: absolute; - right: 0; - top: 0; - } - - &--borderless { - &:not(:first-child)::before { - border-top: none; - } - } - } - - &__header { - display: flex; - justify-content: space-between; - padding: 18px 24px 12px; - - &--center { - justify-content: center; - } - } - - &__title { - @include font-body-1-bold; - } - } -} - // Module: Media Gallery .module-media-gallery { diff --git a/stylesheets/components/Button.scss b/stylesheets/components/Button.scss index cb628a467..3b53004f1 100644 --- a/stylesheets/components/Button.scss +++ b/stylesheets/components/Button.scss @@ -196,4 +196,75 @@ @include hover-and-active-states($background-color, $color-white); } } + + &--details { + align-items: center; + border-radius: 8px; + display: flex; + flex-direction: column; + font-size: 9px; + justify-content: center; + line-height: 14px; + min-height: 44px; + min-width: 60px; + padding: 0 8px; + + @include light-theme { + background-color: $color-gray-05; + color: $color-black; + } + + @include dark-theme { + background-color: $color-gray-65; + color: $color-gray-05; + } + + &:focus { + box-shadow: 0 0 0 2px $color-ultramarine; + } + } + + &__icon { + @mixin button-icon($icon) { + content: ''; + display: block; + height: 18px; + width: 18px; + + @include light-theme { + @include color-svg($icon, $color-black); + } + @include dark-theme { + @include color-svg($icon, $color-gray-05); + } + } + + &--audio::before { + @include button-icon('../images/icons/v2/phone-right-outline-24.svg'); + } + + &--muted::before { + @include button-icon('../images/icons/v2/bell-disabled-outline-24.svg'); + } + + &--photo::before { + @include button-icon('../images/icons/v2/photo-album-outline-24.svg'); + } + + &--search::before { + @include button-icon('../images/icons/v2/search-16.svg'); + } + + &--text::before { + @include button-icon('../images/icons/v2/text-24.svg'); + } + + &--unmuted::before { + @include button-icon('../images/icons/v2/bell-outline-24.svg'); + } + + &--video::before { + @include button-icon('../images/icons/v2/video-outline-24.svg'); + } + } } diff --git a/stylesheets/components/ConversationDetails.scss b/stylesheets/components/ConversationDetails.scss new file mode 100644 index 000000000..1ceb2d402 --- /dev/null +++ b/stylesheets/components/ConversationDetails.scss @@ -0,0 +1,621 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.ConversationDetails { + &-header { + &__root { + align-items: center; + display: flex; + flex-direction: column; + margin: 0 0 24px 0; + text-align: center; + width: 100%; + } + + &__root--editable { + @include button-reset(); + } + + &__root--editable { + cursor: pointer; + } + + &__title { + @include font-title-1; + align-items: center; + display: flex; + justify-content: center; + padding-bottom: 8px; + padding-top: 12px; + } + + &__subtitle { + @include font-body-1; + color: $color-gray-60; + justify-content: center; + padding-bottom: 6px; + + @include dark-theme { + color: $color-gray-25; + } + } + + &__root--editable &__title { + $icon: '../images/icons/v2/compose-solid-24.svg'; + + &::after { + $size: 24px; + + content: ''; + height: $size; + left: $size + 13px; + margin-left: -$size; + opacity: 0; + position: relative; + transition: opacity 100ms ease-out; + width: $size; + + @include light-theme { + @include color-svg($icon, $color-gray-60); + } + @include dark-theme { + @include color-svg($icon, $color-gray-25); + } + } + } + + &__root--editable:hover &__title::after { + opacity: 1; + } + } + + &__chat-color { + @include color-bubble(20px); + } + + &-membership-list { + &__add-members-icon { + @mixin plus-icon($color) { + @include color-svg('../images/icons/v2/plus-24.svg', $color); + content: ''; + display: block; + height: 16px; + width: 16px; + } + + align-items: center; + border-radius: 100%; + display: flex; + height: 32px; + justify-content: center; + width: 32px; + + @include light-theme { + background: $color-gray-02; + &::before { + @include plus-icon($color-black); + } + } + @include dark-theme { + background: $color-gray-90; + &::before { + @include plus-icon($color-gray-15); + } + } + } + } + + &__leave-group { + color: $color-accent-red; + + &--disabled { + @include light-theme { + color: $color-gray-60; + } + + @include dark-theme { + color: $color-gray-25; + } + } + } + + &__block-group { + color: $color-accent-red; + } + + &__tabs { + display: flex; + justify-content: space-around; + } + + &__tab { + @include font-body-1; + cursor: pointer; + padding: 15px; + + &:focus { + @include mouse-mode { + outline: none; + } + } + + &--selected { + @include font-body-1-bold; + border-bottom: 2px solid $color-black; + } + } + + &__pending--info { + @include font-subtitle; + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } + padding: 0 28px; + padding-top: 16px; + } + + &-icon { + &__button { + background: none; + border: none; + padding: none; + + &:focus { + @include mouse-mode { + outline: none; + } + } + } + + &__icon { + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; + + &::after { + display: block; + content: ''; + width: 24px; + height: 24px; + -webkit-mask-size: 100%; + } + + &--color { + &::after { + -webkit-mask: url(../images/icons/v2/color-outline-24.svg) no-repeat + center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--timer { + &::after { + -webkit-mask: url(../images/icons/v2/timer-disabled-outline-24.svg) + no-repeat center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--notifications { + &::after { + -webkit-mask: url('../images/icons/v2/sound-outline-24.svg') no-repeat + center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--mute { + &::after { + @include light-theme { + -webkit-mask: url('../images/icons/v2/bell-disabled-outline-24.svg') + no-repeat center; + background-color: $color-gray-75; + } + + @include dark-theme { + -webkit-mask: url('../images/icons/v2/bell-disabled-solid-24.svg') + no-repeat center; + background-color: $color-gray-15; + } + } + } + + &--mention { + &::after { + -webkit-mask: url('../images/icons/v2/at-24.svg') no-repeat center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--lock { + &::after { + -webkit-mask: url(../images/icons/v2/lock-outline-24.svg) no-repeat + center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--approve { + &::after { + -webkit-mask: url(../images/icons/v2/check-24.svg) no-repeat center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--link { + &::after { + -webkit-mask: url(../images/icons/v2/link-16.svg) no-repeat center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--share { + &::after { + -webkit-mask: url(../images/icons/v2/share-ios-24.svg) no-repeat + center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--reset { + &::after { + -webkit-mask: url(../images/icons/v2/refresh-24.svg) no-repeat center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--trash { + &::after { + -webkit-mask: url(../images/icons/v2/trash-outline-24.svg) no-repeat + center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--invites { + &::after { + -webkit-mask: url(../images/icons/v2/pending-invite-24.svg) no-repeat + center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + + &--down { + border-radius: 18px; + @include light-theme { + background-color: $color-gray-02; + } + + @include dark-theme { + background-color: $color-gray-90; + } + + &::after { + -webkit-mask: url(../images/icons/v2/chevron-down-16.svg) no-repeat + center; + + @include light-theme { + background-color: $color-gray-60; + } + + @include dark-theme { + background-color: $color-gray-25; + } + } + } + + &--leave { + &::after { + -webkit-mask: url(../images/icons/v2/leave-24.svg) no-repeat center; + background-color: $color-accent-red; + } + + &--disabled::after { + @include light-theme { + background-color: $color-gray-60; + } + + @include dark-theme { + background-color: $color-gray-25; + } + } + } + + &--block { + &::after { + -webkit-mask: url(../images/icons/v2/block-24.svg) no-repeat center; + background-color: $color-accent-red; + } + } + + &--verify { + &::after { + -webkit-mask: url(../images/icons/v2/safety-number-outline-24.svg) + no-repeat center; + + @include light-theme { + background-color: $color-gray-75; + } + + @include dark-theme { + background-color: $color-gray-15; + } + } + } + } + } + + &-media-list { + &__root { + display: flex; + justify-content: center; + padding: 0 20px; + padding-bottom: 24px; + + .module-media-grid-item { + border-radius: 4px; + height: auto; + margin: 0 4px; + max-height: 94px; + overflow: hidden; + width: calc(100% / 6); + + .module-media-grid-item__icon { + &::before { + content: ''; + display: block; + padding-top: 100%; + } + } + + .module-media-grid-item__image-container, + img { + margin: 0; + } + } + } + + &__show-all { + background: none; + border: none; + padding: 0; + + @include light-theme { + color: $color-gray-95; + } + @include dark-theme { + color: $color-gray-05; + } + } + } + + &-panel-row { + $row-root-selector: '#{&}__root'; + &__root { + align-items: center; + border-radius: 5px; + border: 2px solid transparent; + display: flex; + padding: 8px 24px; + user-select: none; + width: 100%; + + &--button { + color: inherit; + background: none; + + &:hover:not(:disabled) { + @include light-theme { + background-color: $color-gray-02; + } + + @include dark-theme { + background-color: $color-gray-90; + } + + & .ConversationDetails-panel-row__actions { + opacity: 1; + } + } + } + + &:focus { + outline: none; + } + + @mixin keyboard-focus-state($color) { + &:focus { + border-color: $color; + } + } + + @include keyboard-mode { + @include keyboard-focus-state($color-ultramarine); + } + @include dark-keyboard-mode { + @include keyboard-focus-state($color-ultramarine-light); + } + } + + &__icon { + margin-right: 12px; + flex-shrink: 0; + } + + &__label { + flex-grow: 1; + text-align: left; + margin-right: 12px; + } + + &__info { + @include font-body-2; + margin-top: 4px; + + @include light-theme { + color: $color-gray-60; + } + + @include dark-theme { + color: $color-gray-25; + } + } + + &__right { + position: relative; + color: $color-gray-45; + min-width: 143px; + } + + &__actions { + margin-left: 12px; + overflow: hidden; + opacity: 0; + + #{$row-root-selector}:hover &, + #{$row-root-selector}:focus-within & { + opacity: 1; + } + } + } + + &-panel-section { + &__root { + position: relative; + + &:not(:first-child)::before { + border-top: 1px solid transparent; + + @include light-theme { + border-top-color: $color-gray-15; + } + + @include dark-theme { + border-top-color: $color-gray-65; + } + + content: ''; + display: block; + margin: 8px 0; + } + + &--borderless { + &:not(:first-child)::before { + border-top: none; + } + } + } + + &__header { + display: flex; + justify-content: space-between; + padding: 18px 24px 12px; + + &--center { + justify-content: center; + } + } + + &__title { + @include font-body-1-bold; + } + } + + &__header-buttons { + display: flex; + justify-content: center; + margin-bottom: 24px; + + .module-Button { + margin: 0 8px; + } + } + + &__radio { + &__container { + padding: 12px 0; + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 9f4013c93..c1d3032b3 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -48,6 +48,7 @@ @import './components/ContactPills.scss'; @import './components/ContactSpoofingReviewDialog.scss'; @import './components/ContactSpoofingReviewDialogPerson.scss'; +@import './components/ConversationDetails.scss'; @import './components/ConversationHeader.scss'; @import './components/ConversationView.scss'; @import './components/CustomColorEditor.scss'; diff --git a/ts/components/Button.stories.tsx b/ts/components/Button.stories.tsx index 92ecd5bc3..cee653a2e 100644 --- a/ts/components/Button.stories.tsx +++ b/ts/components/Button.stories.tsx @@ -11,15 +11,7 @@ const story = storiesOf('Components/Button', module); story.add('Kitchen sink', () => ( <> - {[ - ButtonVariant.Primary, - ButtonVariant.Secondary, - ButtonVariant.SecondaryAffirmative, - ButtonVariant.SecondaryDestructive, - ButtonVariant.Destructive, - ButtonVariant.Calling, - ButtonVariant.SystemMessage, - ].map(variant => ( + {Object.values(ButtonVariant).map(variant => ( {[ButtonSize.Medium, ButtonSize.Small].map(size => ( diff --git a/ts/components/Button.tsx b/ts/components/Button.tsx index f4cd8ea55..c1ae3395b 100644 --- a/ts/components/Button.tsx +++ b/ts/components/Button.tsx @@ -12,18 +12,30 @@ export enum ButtonSize { } export enum ButtonVariant { + Calling = 'Calling', + Destructive = 'Destructive', + Details = 'Details', Primary = 'Primary', Secondary = 'Secondary', SecondaryAffirmative = 'SecondaryAffirmative', SecondaryDestructive = 'SecondaryDestructive', - Destructive = 'Destructive', - Calling = 'Calling', SystemMessage = 'SystemMessage', } +export enum ButtonIconType { + audio = 'audio', + muted = 'muted', + photo = 'photo', + search = 'search', + text = 'text', + unmuted = 'unmuted', + video = 'video', +} + type PropsType = { className?: string; disabled?: boolean; + icon?: ButtonIconType; size?: ButtonSize; style?: CSSProperties; tabIndex?: number; @@ -70,6 +82,7 @@ const VARIANT_CLASS_NAMES = new Map([ [ButtonVariant.Destructive, 'module-Button--destructive'], [ButtonVariant.Calling, 'module-Button--calling'], [ButtonVariant.SystemMessage, 'module-Button--system-message'], + [ButtonVariant.Details, 'module-Button--details'], ]); export const Button = React.forwardRef( @@ -78,10 +91,13 @@ export const Button = React.forwardRef( children, className, disabled = false, - size = ButtonSize.Medium, + icon, style, tabIndex, variant = ButtonVariant.Primary, + size = variant === ButtonVariant.Details + ? ButtonSize.Small + : ButtonSize.Medium, } = props; const ariaLabel = props['aria-label']; @@ -108,6 +124,7 @@ export const Button = React.forwardRef( 'module-Button', sizeClassName, variantClassName, + `module-Button__icon--${icon}`, className )} disabled={disabled} diff --git a/ts/components/Checkbox.tsx b/ts/components/Checkbox.tsx index b67315604..efa7dfec0 100644 --- a/ts/components/Checkbox.tsx +++ b/ts/components/Checkbox.tsx @@ -10,6 +10,7 @@ export type PropsType = { checked?: boolean; description?: string; disabled?: boolean; + isRadio?: boolean; label: string; moduleClassName?: string; name: string; @@ -20,6 +21,7 @@ export const Checkbox = ({ checked, description, disabled, + isRadio, label, moduleClassName, name, @@ -37,7 +39,7 @@ export const Checkbox = ({ id={id} name={name} onChange={ev => onChange(ev.target.checked)} - type="checkbox" + type={isRadio ? 'radio' : 'checkbox'} />
diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index f23a98b83..217093086 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -440,7 +440,7 @@ export const Preferences = ({ }} right={
void; onShowContactModal: (contactId: string) => void; onDeleteMessages: () => void; - onResetSession: () => void; onSearchInConversation: () => void; onOutgoingAudioCallInConversation: () => void; onOutgoingVideoCallInConversation: () => void; onSetPin: (value: boolean) => void; - onShowChatColorEditor: () => void; onShowConversationDetails: () => void; - onShowSafetyNumber: () => void; onShowAllMedia: () => void; onShowGroupMembers: () => void; onGoBack: () => void; @@ -369,32 +366,28 @@ export class ConversationHeader extends React.Component { private renderMenu(triggerId: string): ReactNode { const { - i18n, acceptedMessageRequest, canChangeTimer, expireTimer, + groupVersion, + i18n, isArchived, - isMe, + isMissingMandatoryProfileSharing, isPinned, - type, + left, markedUnread, muteExpiresAt, - isMissingMandatoryProfileSharing, - left, - groupVersion, + onArchive, onDeleteMessages, - onResetSession, + onMarkUnread, + onMoveToInbox, onSetDisappearingMessages, onSetMuteNotifications, + onSetPin, onShowAllMedia, - onShowChatColorEditor, onShowConversationDetails, onShowGroupMembers, - onShowSafetyNumber, - onArchive, - onMarkUnread, - onSetPin, - onMoveToInbox, + type, } = this.props; const muteOptions = getMuteOptions(muteExpiresAt, i18n); @@ -484,14 +477,11 @@ export class ConversationHeader extends React.Component { ))} - {!isGroup ? ( - - {i18n('showChatColorEditor')} - - ) : null} - {hasGV2AdminEnabled ? ( + {!isGroup || hasGV2AdminEnabled ? ( - {i18n('showConversationDetails')} + {isGroup + ? i18n('showConversationDetails') + : i18n('showConversationDetails--direct')} ) : null} {isGroup && !hasGV2AdminEnabled ? ( @@ -500,14 +490,6 @@ export class ConversationHeader extends React.Component { ) : null} {i18n('viewRecentMedia')} - {!isGroup && !isMe ? ( - - {i18n('showSafetyNumber')} - - ) : null} - {!isGroup && acceptedMessageRequest ? ( - {i18n('resetSession')} - ) : null} {!markedUnread ? ( {i18n('markUnread')} diff --git a/ts/components/conversation/MessageDetail.stories.tsx b/ts/components/conversation/MessageDetail.stories.tsx index 674a802c0..9db441c29 100644 --- a/ts/components/conversation/MessageDetail.stories.tsx +++ b/ts/components/conversation/MessageDetail.stories.tsx @@ -61,7 +61,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ i18n, interactionMode: 'keyboard', - showSafetyNumber: action('onShowSafetyNumber'), + showSafetyNumber: action('showSafetyNumber'), checkForAccount: action('checkForAccount'), clearSelectedMessage: action('clearSelectedMessage'), diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx index ac9d2951e..aadbf5426 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx @@ -46,6 +46,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({ hasGroupLink, i18n, isAdmin: false, + isGroup: true, loadRecentMediaItems: action('loadRecentMediaItems'), memberships: times(32, i => ({ isAdmin: i === 1, @@ -63,7 +64,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({ setDisappearingMessages: action('setDisappearingMessages'), showAllMedia: action('showAllMedia'), showContactModal: action('showContactModal'), - showGroupChatColorEditor: action('showGroupChatColorEditor'), + showChatColorEditor: action('showChatColorEditor'), showGroupLinkManagement: action('showGroupLinkManagement'), showGroupV2Permissions: action('showGroupV2Permissions'), showConversationNotificationsSettings: action( @@ -76,10 +77,20 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({ }, onBlock: action('onBlock'), onLeave: action('onLeave'), + onUnblock: action('onUnblock'), deleteAvatarFromDisk: action('deleteAvatarFromDisk'), replaceAvatar: action('replaceAvatar'), saveAvatarToDisk: action('saveAvatarToDisk'), + setMuteExpiration: action('setMuteExpiration'), userAvatarData: [], + toggleSafetyNumberModal: action('toggleSafetyNumberModal'), + onOutgoingAudioCallInConversation: action( + 'onOutgoingAudioCallInConversation' + ), + onOutgoingVideoCallInConversation: action( + 'onOutgoingVideoCallInConversation' + ), + searchInConversation: action('searchInConversation'), }); story.add('Basic', () => { @@ -157,3 +168,7 @@ story.add('Group add with missing capabilities', () => ( }} /> )); + +story.add('1:1', () => ( + +)); diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index b55ec30c4..1a4299427 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -3,6 +3,7 @@ import React, { useState, ReactNode } from 'react'; +import { Button, ButtonIconType, ButtonVariant } from '../../Button'; import { ConversationType } from '../../../state/ducks/conversations'; import { assert } from '../../../util/assert'; import { getMutedUntilText } from '../../../util/getMutedUntilText'; @@ -19,7 +20,7 @@ import { PanelSection } from './PanelSection'; import { AddGroupMembersModal } from './AddGroupMembersModal'; import { ConversationDetailsActions } from './ConversationDetailsActions'; import { ConversationDetailsHeader } from './ConversationDetailsHeader'; -import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; import { ConversationDetailsMediaList } from './ConversationDetailsMediaList'; import { ConversationDetailsMembershipList, @@ -33,18 +34,22 @@ import { EditConversationAttributesModal } from './EditConversationAttributesMod import { RequestState } from './util'; import { getCustomColorStyle } from '../../../util/getCustomColorStyle'; import { ConfirmationDialog } from '../../ConfirmationDialog'; +import { ConversationNotificationsModal } from './ConversationNotificationsModal'; import { AvatarDataType, DeleteAvatarFromDiskActionType, ReplaceAvatarActionType, SaveAvatarToDiskActionType, } from '../../../types/Avatar'; +import { isMuted } from '../../../util/isMuted'; enum ModalState { NothingOpen, EditingGroupDescription, EditingGroupTitle, AddingGroupMembers, + MuteNotifications, + UnmuteNotifications, } export type StateProps = { @@ -55,13 +60,14 @@ export type StateProps = { hasGroupLink: boolean; i18n: LocalizerType; isAdmin: boolean; + isGroup: boolean; loadRecentMediaItems: (limit: number) => void; memberships: Array; pendingApprovalMemberships: ReadonlyArray; pendingMemberships: ReadonlyArray; setDisappearingMessages: (seconds: number) => void; showAllMedia: () => void; - showGroupChatColorEditor: () => void; + showChatColorEditor: () => void; showGroupLinkManagement: () => void; showGroupV2Permissions: () => void; showPendingInvites: () => void; @@ -79,7 +85,11 @@ export type StateProps = { ) => Promise; onBlock: () => void; onLeave: () => void; + onUnblock: () => void; userAvatarData: Array; + setMuteExpiration: (muteExpiresAt: undefined | number) => unknown; + onOutgoingAudioCallInConversation: () => unknown; + onOutgoingVideoCallInConversation: () => unknown; }; type ActionProps = { @@ -87,6 +97,8 @@ type ActionProps = { replaceAvatar: ReplaceAvatarActionType; saveAvatarToDisk: SaveAvatarToDiskActionType; showContactModal: (contactId: string, conversationId: string) => void; + toggleSafetyNumberModal: (conversationId: string) => unknown; + searchInConversation: (id: string, title: string) => unknown; }; export type Props = StateProps & ActionProps; @@ -96,28 +108,35 @@ export const ConversationDetails: React.ComponentType = ({ canEditGroupInfo, candidateContactsToAdd, conversation, + deleteAvatarFromDisk, hasGroupLink, i18n, isAdmin, + isGroup, loadRecentMediaItems, memberships, - pendingApprovalMemberships, - pendingMemberships, - setDisappearingMessages, - showAllMedia, - showContactModal, - showGroupChatColorEditor, - showGroupLinkManagement, - showGroupV2Permissions, - showPendingInvites, - showLightboxForMedia, - showConversationNotificationsSettings, - updateGroupAttributes, onBlock, onLeave, - deleteAvatarFromDisk, + onOutgoingAudioCallInConversation, + onOutgoingVideoCallInConversation, + onUnblock, + pendingApprovalMemberships, + pendingMemberships, replaceAvatar, saveAvatarToDisk, + searchInConversation, + setDisappearingMessages, + setMuteExpiration, + showAllMedia, + showChatColorEditor, + showContactModal, + showConversationNotificationsSettings, + showGroupLinkManagement, + showGroupV2Permissions, + showLightboxForMedia, + showPendingInvites, + toggleSafetyNumberModal, + updateGroupAttributes, userAvatarData, }) => { const [modalState, setModalState] = useState( @@ -241,10 +260,45 @@ export const ConversationDetails: React.ComponentType = ({ /> ); break; + case ModalState.MuteNotifications: + modalNode = ( + { + setModalState(ModalState.NothingOpen); + }} + setMuteExpiration={setMuteExpiration} + /> + ); + break; + case ModalState.UnmuteNotifications: + modalNode = ( + setMuteExpiration(0), + style: 'affirmative', + text: i18n('unmute'), + }, + ]} + hasXButton + i18n={i18n} + title={i18n('ConversationDetails__unmute--title')} + onClose={() => { + setModalState(ModalState.NothingOpen); + }} + > + {getMutedUntilText(Number(conversation.muteExpiresAt), i18n)} + + ); + break; default: throw missingCaseError(modalState); } + const isConversationMuted = isMuted(conversation.muteExpiresAt); + return (
{membersMissingCapability && ( @@ -261,6 +315,8 @@ export const ConversationDetails: React.ComponentType = ({ canEdit={canEditGroupInfo} conversation={conversation} i18n={i18n} + isMe={conversation.isMe} + isGroup={isGroup} memberships={memberships} startEditing={(isGroupTitle: boolean) => { setModalState( @@ -271,15 +327,65 @@ export const ConversationDetails: React.ComponentType = ({ }} /> +
+ {!conversation.isMe && ( + <> + + {!isGroup && ( + + )} + + )} + + +
+ - {canEditGroupInfo ? ( + {!isGroup || canEditGroupInfo ? ( } info={i18n('ConversationDetails--disappearing-messages-info')} @@ -297,86 +403,110 @@ export const ConversationDetails: React.ComponentType = ({ icon={ } label={i18n('showChatColorEditor')} - onClick={showGroupChatColorEditor} + onClick={showChatColorEditor} right={
} /> - - } - label={i18n('ConversationDetails--notifications')} - onClick={showConversationNotificationsSettings} - right={ - conversation.muteExpiresAt - ? getMutedUntilText(conversation.muteExpiresAt, i18n) - : undefined - } - /> - - - { - setModalState(ModalState.AddingGroupMembers); - }} - /> - - - {isAdmin || hasGroupLink ? ( + {isGroup && ( } - label={i18n('ConversationDetails--group-link')} - onClick={showGroupLinkManagement} - right={hasGroupLink ? i18n('on') : i18n('off')} + label={i18n('ConversationDetails--notifications')} + onClick={showConversationNotificationsSettings} + right={ + conversation.muteExpiresAt + ? getMutedUntilText(conversation.muteExpiresAt, i18n) + : undefined + } /> - ) : null} - + toggleSafetyNumberModal(conversation.id)} + icon={ + + } + label={ +
+ {i18n('verifyNewNumber')} +
+ } /> - } - label={i18n('ConversationDetails--requests-and-invites')} - onClick={showPendingInvites} - right={invitesCount} + + )} +
+ + {isGroup && ( + { + setModalState(ModalState.AddingGroupMembers); + }} /> - {isAdmin ? ( + )} + + {isGroup && ( + + {isAdmin || hasGroupLink ? ( + + } + label={i18n('ConversationDetails--group-link')} + onClick={showGroupLinkManagement} + right={hasGroupLink ? i18n('on') : i18n('off')} + /> + ) : null} } - label={i18n('permissions')} - onClick={showGroupV2Permissions} + label={i18n('ConversationDetails--requests-and-invites')} + onClick={showPendingInvites} + right={invitesCount} /> - ) : null} - + {isAdmin ? ( + + } + label={i18n('permissions')} + onClick={showGroupV2Permissions} + /> + ) : null} + + )} = ({ showLightboxForMedia={showLightboxForMedia} /> - + {!conversation.isMe && ( + + )} {modalNode}
diff --git a/ts/components/conversation/conversation-details/ConversationDetailsActions.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetailsActions.stories.tsx index 419387864..b080ea17f 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsActions.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsActions.stories.tsx @@ -31,7 +31,10 @@ const createProps = (overrideProps: Partial = {}): Props => ({ left: isBoolean(overrideProps.left) ? overrideProps.left : false, onBlock: action('onBlock'), onLeave: action('onLeave'), + onUnblock: action('onUnblock'), i18n, + isBlocked: false, + isGroup: true, }); story.add('Basic', () => { @@ -51,3 +54,11 @@ story.add('Cannot leave because you are the last admin', () => { return ; }); + +story.add('1:1', () => ( + +)); + +story.add('1:1 Blocked', () => ( + +)); diff --git a/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx b/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx index 54e8880c1..8c6bc1486 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsActions.tsx @@ -1,7 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { ReactNode } from 'react'; +import React, { ReactNode, useState } from 'react'; import classNames from 'classnames'; import { LocalizerType } from '../../../types/Util'; @@ -10,48 +10,55 @@ import { Tooltip, TooltipPlacement } from '../../Tooltip'; import { PanelRow } from './PanelRow'; import { PanelSection } from './PanelSection'; -import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; export type Props = { cannotLeaveBecauseYouAreLastAdmin: boolean; conversationTitle: string; + i18n: LocalizerType; + isBlocked: boolean; + isGroup: boolean; left: boolean; onBlock: () => void; onLeave: () => void; - i18n: LocalizerType; + onUnblock: () => void; }; export const ConversationDetailsActions: React.ComponentType = ({ cannotLeaveBecauseYouAreLastAdmin, conversationTitle, + i18n, + isBlocked, + isGroup, left, onBlock, onLeave, - i18n, + onUnblock, }) => { - const [confirmingLeave, setConfirmingLeave] = React.useState(false); - const [confirmingBlock, setConfirmingBlock] = React.useState(false); + const [confirmLeave, gLeave] = useState(false); + const [confirmGroupBlock, gGroupBlock] = useState(false); + const [confirmDirectBlock, gDirectBlock] = useState(false); + const [confirmDirectUnblock, gDirectUnblock] = useState(false); let leaveGroupNode: ReactNode; - let blockGroupNode: ReactNode; - if (!left) { + if (isGroup && !left) { leaveGroupNode = ( setConfirmingLeave(true)} + onClick={() => gLeave(true)} icon={ } label={
{i18n('ConversationDetailsActions--leave-group')} @@ -73,32 +80,49 @@ export const ConversationDetailsActions: React.ComponentType = ({ } } - blockGroupNode = ( - setConfirmingBlock(true)} - icon={ - - } - label={ -
- {i18n('ConversationDetailsActions--block-group')} -
- } - /> - ); + let blockNode: ReactNode; + if (isGroup) { + blockNode = ( + gGroupBlock(true)} + icon={ + + } + label={ +
+ {i18n('ConversationDetailsActions--block-group')} +
+ } + /> + ); + } else { + const label = isBlocked + ? i18n('MessageRequests--unblock') + : i18n('MessageRequests--block'); + blockNode = ( + (isBlocked ? gDirectUnblock(true) : gDirectBlock(true))} + icon={ + + } + label={
{label}
} + /> + ); + } + if (cannotLeaveBecauseYouAreLastAdmin) { - blockGroupNode = ( + blockNode = ( - {blockGroupNode} + {blockNode} ); } @@ -107,10 +131,10 @@ export const ConversationDetailsActions: React.ComponentType = ({ <> {leaveGroupNode} - {blockGroupNode} + {blockNode} - {confirmingLeave && ( + {confirmLeave && ( = ({ }, ]} i18n={i18n} - onClose={() => setConfirmingLeave(false)} + onClose={() => gLeave(false)} title={i18n('ConversationDetailsActions--leave-group-modal-title')} > {i18n('ConversationDetailsActions--leave-group-modal-content')} )} - {confirmingBlock && ( + {confirmGroupBlock && ( = ({ }, ]} i18n={i18n} - onClose={() => setConfirmingBlock(false)} + onClose={() => gGroupBlock(false)} title={i18n('ConversationDetailsActions--block-group-modal-title', [ conversationTitle, ])} @@ -149,6 +173,44 @@ export const ConversationDetailsActions: React.ComponentType = ({ {i18n('ConversationDetailsActions--block-group-modal-content')} )} + + {confirmDirectBlock && ( + gDirectBlock(false)} + title={i18n('MessageRequests--block-direct-confirm-title', [ + conversationTitle, + ])} + > + {i18n('MessageRequests--block-direct-confirm-body')} + + )} + + {confirmDirectUnblock && ( + gDirectUnblock(false)} + title={i18n('MessageRequests--unblock-direct-confirm-title', [ + conversationTitle, + ])} + > + {i18n('MessageRequests--unblock-direct-confirm-body')} + + )} ); }; diff --git a/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx index a454326f8..634a3efe3 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx @@ -36,6 +36,8 @@ const createProps = (overrideProps: Partial = {}): Props => ({ canEdit: false, startEditing: action('startEditing'), memberships: new Array(number('conversation members length', 0)), + isGroup: false, + isMe: false, ...overrideProps, }); @@ -78,3 +80,11 @@ story.add('Editable no-description', () => { /> ); }); + +story.add('1:1', () => ( + +)); + +story.add('Note to self', () => ( + +)); diff --git a/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx b/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx index d12de1dbb..70348aafb 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx @@ -16,36 +16,44 @@ export type Props = { canEdit: boolean; conversation: ConversationType; i18n: LocalizerType; + isGroup: boolean; + isMe: boolean; memberships: Array; startEditing: (isGroupTitle: boolean) => void; }; -const bem = bemGenerator('module-conversation-details-header'); +const bem = bemGenerator('ConversationDetails-header'); export const ConversationDetailsHeader: React.ComponentType = ({ canEdit, conversation, i18n, + isGroup, + isMe, memberships, startEditing, }) => { const [showingAvatar, setShowingAvatar] = useState(false); let subtitle: ReactNode; - if (conversation.groupDescription) { - subtitle = ( - - ); - } else if (canEdit) { - subtitle = i18n('ConversationDetailsHeader--add-group-description'); - } else { - subtitle = i18n('ConversationDetailsHeader--members', [ - memberships.length.toString(), - ]); + if (isGroup) { + if (conversation.groupDescription) { + subtitle = ( + + ); + } else if (canEdit) { + subtitle = i18n('ConversationDetailsHeader--add-group-description'); + } else { + subtitle = i18n('ConversationDetailsHeader--members', [ + memberships.length.toString(), + ]); + } + } else if (!isMe) { + subtitle = conversation.phoneNumber; } const avatar = ( @@ -54,6 +62,7 @@ export const ConversationDetailsHeader: React.ComponentType = ({ i18n={i18n} size={80} {...conversation} + noteToSelf={isMe} onClick={() => setShowingAvatar(true)} sharedGroupNames={[]} /> @@ -62,21 +71,22 @@ export const ConversationDetailsHeader: React.ComponentType = ({ const contents = (
- +
); - const avatarLightbox = showingAvatar ? ( - setShowingAvatar(false)} - /> - ) : null; + const avatarLightbox = + showingAvatar && !isMe ? ( + setShowingAvatar(false)} + /> + ) : null; if (canEdit) { return ( diff --git a/ts/components/conversation/conversation-details/ConversationDetailsIcon.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetailsIcon.stories.tsx index ecbcbe852..53a872503 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsIcon.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsIcon.stories.tsx @@ -6,7 +6,11 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { ConversationDetailsIcon, Props } from './ConversationDetailsIcon'; +import { + ConversationDetailsIcon, + Props, + IconType, +} from './ConversationDetailsIcon'; const story = storiesOf( 'Components/Conversation/ConversationDetails/ConversationDetailIcon', @@ -15,12 +19,12 @@ const story = storiesOf( const createProps = (overrideProps: Partial): Props => ({ ariaLabel: overrideProps.ariaLabel || '', - icon: overrideProps.icon || '', + icon: overrideProps.icon || IconType.timer, onClick: overrideProps.onClick, }); story.add('All', () => { - const icons = ['timer', 'trash', 'invites', 'block', 'leave', 'down']; + const icons = Object.values(IconType); return icons.map(icon => ( @@ -28,7 +32,14 @@ story.add('All', () => { }); story.add('Clickable Icons', () => { - const icons = ['timer', 'trash', 'invites', 'block', 'leave', 'down']; + const icons = [ + IconType.timer, + IconType.trash, + IconType.invites, + IconType.block, + IconType.leave, + IconType.down, + ]; const onClick = action('onClick'); diff --git a/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx b/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx index 9feef009a..23da4a24f 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx @@ -6,14 +6,32 @@ import classNames from 'classnames'; import { bemGenerator } from './util'; +export enum IconType { + 'block' = 'block', + 'color' = 'color', + 'down' = 'down', + 'invites' = 'invites', + 'leave' = 'leave', + 'link' = 'link', + 'lock' = 'lock', + 'mention' = 'mention', + 'mute' = 'mute', + 'notifications' = 'notifications', + 'reset' = 'reset', + 'share' = 'share', + 'timer' = 'timer', + 'trash' = 'trash', + 'verify' = 'verify', +} + export type Props = { ariaLabel: string; disabled?: boolean; - icon: string; + icon: IconType; onClick?: () => void; }; -const bem = bemGenerator('module-conversation-details-icon'); +const bem = bemGenerator('ConversationDetails-icon'); export const ConversationDetailsIcon: React.ComponentType = ({ ariaLabel, diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx index c13232053..f1ca58ccb 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx @@ -25,7 +25,7 @@ export type Props = { const MEDIA_ITEM_LIMIT = 6; -const bem = bemGenerator('module-conversation-details-media-list'); +const bem = bemGenerator('ConversationDetails-media-list'); export const ConversationDetailsMediaList: React.ComponentType = ({ conversation, @@ -36,11 +36,13 @@ export const ConversationDetailsMediaList: React.ComponentType = ({ }) => { const mediaItems = conversation.recentMediaItems || []; + const mediaItemsLength = mediaItems.length; + React.useEffect(() => { loadRecentMediaItems(MEDIA_ITEM_LIMIT); - }, [loadRecentMediaItems]); + }, [loadRecentMediaItems, mediaItemsLength]); - if (mediaItems.length === 0) { + if (mediaItemsLength === 0) { return null; } diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx index 2502cc6cf..fdf705e5d 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.tsx @@ -7,7 +7,7 @@ import { LocalizerType } from '../../../types/Util'; import { Avatar } from '../../Avatar'; import { Emojify } from '../Emojify'; -import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; import { ConversationType } from '../../../state/ducks/conversations'; import { PanelRow } from './PanelRow'; import { PanelSection } from './PanelSection'; @@ -94,7 +94,7 @@ export const ConversationDetailsMembershipList: React.ComponentType = ({ {canAddNewMembers && ( +
} label={i18n('ConversationDetailsMembershipList--add-members')} onClick={() => startAddingNewMembers?.()} @@ -118,11 +118,11 @@ export const ConversationDetailsMembershipList: React.ComponentType = ({ ))} {showAllMembers === false && shouldHideRestMembers && ( } onClick={() => setShowAllMembers(true)} diff --git a/ts/components/conversation/conversation-details/ConversationNotificationsModal.tsx b/ts/components/conversation/conversation-details/ConversationNotificationsModal.tsx new file mode 100644 index 000000000..c06ff6012 --- /dev/null +++ b/ts/components/conversation/conversation-details/ConversationNotificationsModal.tsx @@ -0,0 +1,78 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { useMemo, useState } from 'react'; + +import { LocalizerType } from '../../../types/Util'; +import { getMuteOptions } from '../../../util/getMuteOptions'; +import { parseIntOrThrow } from '../../../util/parseIntOrThrow'; +import { Checkbox } from '../../Checkbox'; +import { Modal } from '../../Modal'; +import { Button, ButtonVariant } from '../../Button'; + +type PropsType = { + i18n: LocalizerType; + muteExpiresAt: undefined | number; + onClose: () => unknown; + setMuteExpiration: (muteExpiresAt: undefined | number) => unknown; +}; + +export const ConversationNotificationsModal = ({ + i18n, + muteExpiresAt, + onClose, + setMuteExpiration, +}: PropsType): JSX.Element => { + const muteOptions = useMemo( + () => + getMuteOptions(muteExpiresAt, i18n).map(({ disabled, name, value }) => ({ + disabled, + text: name, + value, + })), + [i18n, muteExpiresAt] + ); + + const [muteExpirationValue, setMuteExpirationValue] = useState(muteExpiresAt); + + const onMuteChange = () => { + const ms = parseIntOrThrow( + muteExpirationValue, + 'NotificationSettings: mute ms was not an integer' + ); + setMuteExpiration(ms); + onClose(); + }; + + return ( + + {muteOptions + .filter(x => x.value > 0) + .map(option => ( + value && setMuteExpirationValue(option.value)} + /> + ))} + + + + + + ); +}; diff --git a/ts/components/conversation/conversation-details/ConversationNotificationsSettings.tsx b/ts/components/conversation/conversation-details/ConversationNotificationsSettings.tsx index 02d51702e..9f5e0228e 100644 --- a/ts/components/conversation/conversation-details/ConversationNotificationsSettings.tsx +++ b/ts/components/conversation/conversation-details/ConversationNotificationsSettings.tsx @@ -7,10 +7,9 @@ import { ConversationTypeType } from '../../../state/ducks/conversations'; import { LocalizerType } from '../../../types/Util'; import { PanelSection } from './PanelSection'; import { PanelRow } from './PanelRow'; -import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; import { Select } from '../../Select'; import { isMuted } from '../../../util/isMuted'; -import { assert } from '../../../util/assert'; import { getMuteOptions } from '../../../util/getMuteOptions'; import { parseIntOrThrow } from '../../../util/parseIntOrThrow'; @@ -33,13 +32,6 @@ export const ConversationNotificationsSettings: FunctionComponent = ( setMuteExpiration, setDontNotifyForMentionsIfMuted, }) => { - // This assertion is here to prevent accidental usage of this component in an untested - // context. - assert( - conversationType === 'group', - ' SHOULD work for non-group conversations, but it has not been tested there' - ); - const muteOptions = useMemo( () => [ ...(isMuted(muteExpiresAt) @@ -81,7 +73,7 @@ export const ConversationNotificationsSettings: FunctionComponent = ( icon={ } label={i18n('muteNotificationsTitle')} @@ -96,7 +88,7 @@ export const ConversationNotificationsSettings: FunctionComponent = ( ariaLabel={i18n( 'ConversationNotificationsSettings__mentions__label' )} - icon="mention" + icon={IconType.mention} /> } label={i18n('ConversationNotificationsSettings__mentions__label')} diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx index e8a82ea3a..cb8b8fc30 100644 --- a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx +++ b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx @@ -3,7 +3,7 @@ import React from 'react'; -import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; import { SignalService as Proto } from '../../../protobuf'; import { ConversationType } from '../../../state/ducks/conversations'; import { LocalizerType } from '../../../types/Util'; @@ -86,7 +86,7 @@ export const GroupLinkManagement: React.ComponentType = ({ icon={ } label={i18n('GroupLinkManagement--share')} @@ -101,7 +101,7 @@ export const GroupLinkManagement: React.ComponentType = ({ icon={ } label={i18n('GroupLinkManagement--reset')} diff --git a/ts/components/conversation/conversation-details/PanelRow.stories.tsx b/ts/components/conversation/conversation-details/PanelRow.stories.tsx index 0533a0dc2..ae624cf08 100644 --- a/ts/components/conversation/conversation-details/PanelRow.stories.tsx +++ b/ts/components/conversation/conversation-details/PanelRow.stories.tsx @@ -7,7 +7,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { boolean, text } from '@storybook/addon-knobs'; -import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; import { PanelRow, Props } from './PanelRow'; const story = storiesOf( @@ -17,7 +17,7 @@ const story = storiesOf( const createProps = (overrideProps: Partial = {}): Props => ({ icon: boolean('with icon', overrideProps.icon !== undefined) ? ( - + ) : null, label: text('label', (overrideProps.label as string) || ''), info: text('info', overrideProps.info || ''), @@ -25,7 +25,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ actions: boolean('with action', overrideProps.actions !== undefined) ? ( ) : null, diff --git a/ts/components/conversation/conversation-details/PanelRow.tsx b/ts/components/conversation/conversation-details/PanelRow.tsx index 6bde283ef..f1cd725a1 100644 --- a/ts/components/conversation/conversation-details/PanelRow.tsx +++ b/ts/components/conversation/conversation-details/PanelRow.tsx @@ -17,7 +17,7 @@ export type Props = { onClick?: () => void; }; -const bem = bemGenerator('module-conversation-details-panel-row'); +const bem = bemGenerator('ConversationDetails-panel-row'); export const PanelRow: React.ComponentType = ({ alwaysShowActions, diff --git a/ts/components/conversation/conversation-details/PanelSection.tsx b/ts/components/conversation/conversation-details/PanelSection.tsx index 58ee3168b..a06e6a8c0 100644 --- a/ts/components/conversation/conversation-details/PanelSection.tsx +++ b/ts/components/conversation/conversation-details/PanelSection.tsx @@ -12,7 +12,7 @@ export type Props = { title?: string; }; -const bem = bemGenerator('module-conversation-details-panel-section'); +const bem = bemGenerator('ConversationDetails-panel-section'); const borderlessClass = bem('root', 'borderless'); export const PanelSection: React.ComponentType = ({ diff --git a/ts/components/conversation/conversation-details/PendingInvites.tsx b/ts/components/conversation/conversation-details/PendingInvites.tsx index 1eca1c727..4a4cc93d6 100644 --- a/ts/components/conversation/conversation-details/PendingInvites.tsx +++ b/ts/components/conversation/conversation-details/PendingInvites.tsx @@ -11,7 +11,7 @@ import { Avatar } from '../../Avatar'; import { ConfirmationDialog } from '../../ConfirmationDialog'; import { PanelSection } from './PanelSection'; import { PanelRow } from './PanelRow'; -import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; export type PropsType = { readonly conversation?: ConversationType; @@ -73,12 +73,11 @@ export const PendingInvites: React.ComponentType = ({ return (
-
+
{ setSelectedTab(Tab.Requests); @@ -98,9 +97,8 @@ export const PendingInvites: React.ComponentType = ({
{ setSelectedTab(Tab.Pending); @@ -323,7 +321,7 @@ function MembersPendingAdminApproval({ <>