Conversation details screen for 1:1 chats

This commit is contained in:
Josh Perez 2021-10-20 19:46:41 -04:00 committed by GitHub
parent 3a507349cd
commit 2e438aa876
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1357 additions and 1102 deletions

View File

@ -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 wont know youve 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"

View File

@ -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 {

View File

@ -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');
}
}
}

View File

@ -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;
}
}
}

View File

@ -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';

View File

@ -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 => (
<React.Fragment key={variant}>
{[ButtonSize.Medium, ButtonSize.Small].map(size => (
<React.Fragment key={size}>

View File

@ -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, string>([
[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<HTMLButtonElement, PropsType>(
@ -78,10 +91,13 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
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<HTMLButtonElement, PropsType>(
'module-Button',
sizeClassName,
variantClassName,
`module-Button__icon--${icon}`,
className
)}
disabled={disabled}

View File

@ -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'}
/>
</div>
<div>

View File

@ -440,7 +440,7 @@ export const Preferences = ({
}}
right={
<div
className={`module-conversation-details__chat-color module-conversation-details__chat-color--${defaultConversationColor.color}`}
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${defaultConversationColor.color}`}
style={{
...getCustomColorStyle(
defaultConversationColor.customColorData?.value

View File

@ -39,7 +39,6 @@ const commonProps = {
onShowConversationDetails: action('onShowConversationDetails'),
onSetDisappearingMessages: action('onSetDisappearingMessages'),
onDeleteMessages: action('onDeleteMessages'),
onResetSession: action('onResetSession'),
onSearchInConversation: action('onSearchInConversation'),
onSetMuteNotifications: action('onSetMuteNotifications'),
onOutgoingAudioCallInConversation: action(
@ -49,8 +48,6 @@ const commonProps = {
'onOutgoingVideoCallInConversation'
),
onShowChatColorEditor: action('onShowChatColorEditor'),
onShowSafetyNumber: action('onShowSafetyNumber'),
onShowAllMedia: action('onShowAllMedia'),
onShowContactModal: action('onShowContactModal'),
onShowGroupMembers: action('onShowGroupMembers'),

View File

@ -68,15 +68,12 @@ export type PropsActionsType = {
onSetDisappearingMessages: (seconds: number) => 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<PropsType, StateType> {
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<PropsType, StateType> {
</MenuItem>
))}
</SubMenu>
{!isGroup ? (
<MenuItem onClick={onShowChatColorEditor}>
{i18n('showChatColorEditor')}
</MenuItem>
) : null}
{hasGV2AdminEnabled ? (
{!isGroup || hasGV2AdminEnabled ? (
<MenuItem onClick={onShowConversationDetails}>
{i18n('showConversationDetails')}
{isGroup
? i18n('showConversationDetails')
: i18n('showConversationDetails--direct')}
</MenuItem>
) : null}
{isGroup && !hasGV2AdminEnabled ? (
@ -500,14 +490,6 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
</MenuItem>
) : null}
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
{!isGroup && !isMe ? (
<MenuItem onClick={onShowSafetyNumber}>
{i18n('showSafetyNumber')}
</MenuItem>
) : null}
{!isGroup && acceptedMessageRequest ? (
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
) : null}
<MenuItem divider />
{!markedUnread ? (
<MenuItem onClick={onMarkUnread}>{i18n('markUnread')}</MenuItem>

View File

@ -61,7 +61,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n,
interactionMode: 'keyboard',
showSafetyNumber: action('onShowSafetyNumber'),
showSafetyNumber: action('showSafetyNumber'),
checkForAccount: action('checkForAccount'),
clearSelectedMessage: action('clearSelectedMessage'),

View File

@ -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', () => (
<ConversationDetails {...createProps()} isGroup={false} />
));

View File

@ -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<GroupV2Membership>;
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
setDisappearingMessages: (seconds: number) => void;
showAllMedia: () => void;
showGroupChatColorEditor: () => void;
showChatColorEditor: () => void;
showGroupLinkManagement: () => void;
showGroupV2Permissions: () => void;
showPendingInvites: () => void;
@ -79,7 +85,11 @@ export type StateProps = {
) => Promise<void>;
onBlock: () => void;
onLeave: () => void;
onUnblock: () => void;
userAvatarData: Array<AvatarDataType>;
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<Props> = ({
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<ModalState>(
@ -241,10 +260,45 @@ export const ConversationDetails: React.ComponentType<Props> = ({
/>
);
break;
case ModalState.MuteNotifications:
modalNode = (
<ConversationNotificationsModal
i18n={i18n}
muteExpiresAt={conversation.muteExpiresAt}
onClose={() => {
setModalState(ModalState.NothingOpen);
}}
setMuteExpiration={setMuteExpiration}
/>
);
break;
case ModalState.UnmuteNotifications:
modalNode = (
<ConfirmationDialog
actions={[
{
action: () => setMuteExpiration(0),
style: 'affirmative',
text: i18n('unmute'),
},
]}
hasXButton
i18n={i18n}
title={i18n('ConversationDetails__unmute--title')}
onClose={() => {
setModalState(ModalState.NothingOpen);
}}
>
{getMutedUntilText(Number(conversation.muteExpiresAt), i18n)}
</ConfirmationDialog>
);
break;
default:
throw missingCaseError(modalState);
}
const isConversationMuted = isMuted(conversation.muteExpiresAt);
return (
<div className="conversation-details-panel">
{membersMissingCapability && (
@ -261,6 +315,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
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<Props> = ({
}}
/>
<div className="ConversationDetails__header-buttons">
{!conversation.isMe && (
<>
<Button
icon={ButtonIconType.video}
onClick={onOutgoingVideoCallInConversation}
variant={ButtonVariant.Details}
>
{i18n('video')}
</Button>
{!isGroup && (
<Button
icon={ButtonIconType.audio}
onClick={onOutgoingAudioCallInConversation}
variant={ButtonVariant.Details}
>
{i18n('audio')}
</Button>
)}
</>
)}
<Button
icon={
isConversationMuted ? ButtonIconType.muted : ButtonIconType.unmuted
}
onClick={() => {
if (isConversationMuted) {
setModalState(ModalState.UnmuteNotifications);
} else {
setModalState(ModalState.MuteNotifications);
}
}}
variant={ButtonVariant.Details}
>
{isConversationMuted ? i18n('unmute') : i18n('mute')}
</Button>
<Button
icon={ButtonIconType.search}
onClick={() => {
searchInConversation(
conversation.id,
conversation.isMe ? i18n('noteToSelf') : conversation.title
);
}}
variant={ButtonVariant.Details}
>
{i18n('search')}
</Button>
</div>
<PanelSection>
{canEditGroupInfo ? (
{!isGroup || canEditGroupInfo ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n(
'ConversationDetails--disappearing-messages-label'
)}
icon="timer"
icon={IconType.timer}
/>
}
info={i18n('ConversationDetails--disappearing-messages-info')}
@ -297,86 +403,110 @@ export const ConversationDetails: React.ComponentType<Props> = ({
icon={
<ConversationDetailsIcon
ariaLabel={i18n('showChatColorEditor')}
icon="color"
icon={IconType.color}
/>
}
label={i18n('showChatColorEditor')}
onClick={showGroupChatColorEditor}
onClick={showChatColorEditor}
right={
<div
className={`module-conversation-details__chat-color module-conversation-details__chat-color--${conversation.conversationColor}`}
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${conversation.conversationColor}`}
style={{
...getCustomColorStyle(conversation.customColor),
}}
/>
}
/>
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetails--notifications')}
icon="notifications"
/>
}
label={i18n('ConversationDetails--notifications')}
onClick={showConversationNotificationsSettings}
right={
conversation.muteExpiresAt
? getMutedUntilText(conversation.muteExpiresAt, i18n)
: undefined
}
/>
</PanelSection>
<ConversationDetailsMembershipList
canAddNewMembers={canEditGroupInfo}
conversationId={conversation.id}
i18n={i18n}
memberships={memberships}
showContactModal={showContactModal}
startAddingNewMembers={() => {
setModalState(ModalState.AddingGroupMembers);
}}
/>
<PanelSection>
{isAdmin || hasGroupLink ? (
{isGroup && (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetails--group-link')}
icon="link"
ariaLabel={i18n('ConversationDetails--notifications')}
icon={IconType.notifications}
/>
}
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}
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetails--requests-and-invites')}
icon="invites"
)}
{!isGroup && !conversation.isMe && (
<>
<PanelRow
onClick={() => toggleSafetyNumberModal(conversation.id)}
icon={
<ConversationDetailsIcon
ariaLabel={i18n('verifyNewNumber')}
icon={IconType.verify}
/>
}
label={
<div className="ConversationDetails__safety-number">
{i18n('verifyNewNumber')}
</div>
}
/>
}
label={i18n('ConversationDetails--requests-and-invites')}
onClick={showPendingInvites}
right={invitesCount}
</>
)}
</PanelSection>
{isGroup && (
<ConversationDetailsMembershipList
canAddNewMembers={canEditGroupInfo}
conversationId={conversation.id}
i18n={i18n}
memberships={memberships}
showContactModal={showContactModal}
startAddingNewMembers={() => {
setModalState(ModalState.AddingGroupMembers);
}}
/>
{isAdmin ? (
)}
{isGroup && (
<PanelSection>
{isAdmin || hasGroupLink ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetails--group-link')}
icon={IconType.link}
/>
}
label={i18n('ConversationDetails--group-link')}
onClick={showGroupLinkManagement}
right={hasGroupLink ? i18n('on') : i18n('off')}
/>
) : null}
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('permissions')}
icon="lock"
ariaLabel={i18n('ConversationDetails--requests-and-invites')}
icon={IconType.invites}
/>
}
label={i18n('permissions')}
onClick={showGroupV2Permissions}
label={i18n('ConversationDetails--requests-and-invites')}
onClick={showPendingInvites}
right={invitesCount}
/>
) : null}
</PanelSection>
{isAdmin ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('permissions')}
icon={IconType.lock}
/>
}
label={i18n('permissions')}
onClick={showGroupV2Permissions}
/>
) : null}
</PanelSection>
)}
<ConversationDetailsMediaList
conversation={conversation}
@ -386,14 +516,19 @@ export const ConversationDetails: React.ComponentType<Props> = ({
showLightboxForMedia={showLightboxForMedia}
/>
<ConversationDetailsActions
i18n={i18n}
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
conversationTitle={conversation.title}
left={Boolean(conversation.left)}
onLeave={onLeave}
onBlock={onBlock}
/>
{!conversation.isMe && (
<ConversationDetailsActions
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
conversationTitle={conversation.title}
i18n={i18n}
isBlocked={Boolean(conversation.isBlocked)}
isGroup={isGroup}
left={Boolean(conversation.left)}
onBlock={onBlock}
onLeave={onLeave}
onUnblock={onUnblock}
/>
)}
{modalNode}
</div>

View File

@ -31,7 +31,10 @@ const createProps = (overrideProps: Partial<Props> = {}): 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 <ConversationDetailsActions {...props} />;
});
story.add('1:1', () => (
<ConversationDetailsActions {...createProps()} isGroup={false} />
));
story.add('1:1 Blocked', () => (
<ConversationDetailsActions {...createProps()} isGroup={false} isBlocked />
));

View File

@ -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<Props> = ({
cannotLeaveBecauseYouAreLastAdmin,
conversationTitle,
i18n,
isBlocked,
isGroup,
left,
onBlock,
onLeave,
i18n,
onUnblock,
}) => {
const [confirmingLeave, setConfirmingLeave] = React.useState<boolean>(false);
const [confirmingBlock, setConfirmingBlock] = React.useState<boolean>(false);
const [confirmLeave, gLeave] = useState<boolean>(false);
const [confirmGroupBlock, gGroupBlock] = useState<boolean>(false);
const [confirmDirectBlock, gDirectBlock] = useState<boolean>(false);
const [confirmDirectUnblock, gDirectUnblock] = useState<boolean>(false);
let leaveGroupNode: ReactNode;
let blockGroupNode: ReactNode;
if (!left) {
if (isGroup && !left) {
leaveGroupNode = (
<PanelRow
disabled={cannotLeaveBecauseYouAreLastAdmin}
onClick={() => setConfirmingLeave(true)}
onClick={() => gLeave(true)}
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetailsActions--leave-group')}
disabled={cannotLeaveBecauseYouAreLastAdmin}
icon="leave"
icon={IconType.leave}
/>
}
label={
<div
className={classNames(
'module-conversation-details__leave-group',
'ConversationDetails__leave-group',
cannotLeaveBecauseYouAreLastAdmin &&
'module-conversation-details__leave-group--disabled'
'ConversationDetails__leave-group--disabled'
)}
>
{i18n('ConversationDetailsActions--leave-group')}
@ -73,32 +80,49 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
}
}
blockGroupNode = (
<PanelRow
disabled={cannotLeaveBecauseYouAreLastAdmin}
onClick={() => setConfirmingBlock(true)}
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetailsActions--block-group')}
icon="block"
/>
}
label={
<div className="module-conversation-details__block-group">
{i18n('ConversationDetailsActions--block-group')}
</div>
}
/>
);
let blockNode: ReactNode;
if (isGroup) {
blockNode = (
<PanelRow
disabled={cannotLeaveBecauseYouAreLastAdmin}
onClick={() => gGroupBlock(true)}
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetailsActions--block-group')}
icon={IconType.block}
/>
}
label={
<div className="ConversationDetails__block-group">
{i18n('ConversationDetailsActions--block-group')}
</div>
}
/>
);
} else {
const label = isBlocked
? i18n('MessageRequests--unblock')
: i18n('MessageRequests--block');
blockNode = (
<PanelRow
onClick={() => (isBlocked ? gDirectUnblock(true) : gDirectBlock(true))}
icon={
<ConversationDetailsIcon ariaLabel={label} icon={IconType.block} />
}
label={<div className="ConversationDetails__block-group">{label}</div>}
/>
);
}
if (cannotLeaveBecauseYouAreLastAdmin) {
blockGroupNode = (
blockNode = (
<Tooltip
content={i18n(
'ConversationDetailsActions--leave-group-must-choose-new-admin'
)}
direction={TooltipPlacement.Top}
>
{blockGroupNode}
{blockNode}
</Tooltip>
);
}
@ -107,10 +131,10 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
<>
<PanelSection>
{leaveGroupNode}
{blockGroupNode}
{blockNode}
</PanelSection>
{confirmingLeave && (
{confirmLeave && (
<ConfirmationDialog
actions={[
{
@ -122,14 +146,14 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
},
]}
i18n={i18n}
onClose={() => setConfirmingLeave(false)}
onClose={() => gLeave(false)}
title={i18n('ConversationDetailsActions--leave-group-modal-title')}
>
{i18n('ConversationDetailsActions--leave-group-modal-content')}
</ConfirmationDialog>
)}
{confirmingBlock && (
{confirmGroupBlock && (
<ConfirmationDialog
actions={[
{
@ -141,7 +165,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
},
]}
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<Props> = ({
{i18n('ConversationDetailsActions--block-group-modal-content')}
</ConfirmationDialog>
)}
{confirmDirectBlock && (
<ConfirmationDialog
actions={[
{
text: i18n('MessageRequests--block'),
action: onBlock,
style: 'affirmative',
},
]}
i18n={i18n}
onClose={() => gDirectBlock(false)}
title={i18n('MessageRequests--block-direct-confirm-title', [
conversationTitle,
])}
>
{i18n('MessageRequests--block-direct-confirm-body')}
</ConfirmationDialog>
)}
{confirmDirectUnblock && (
<ConfirmationDialog
actions={[
{
text: i18n('MessageRequests--unblock'),
action: onUnblock,
style: 'affirmative',
},
]}
i18n={i18n}
onClose={() => gDirectUnblock(false)}
title={i18n('MessageRequests--unblock-direct-confirm-title', [
conversationTitle,
])}
>
{i18n('MessageRequests--unblock-direct-confirm-body')}
</ConfirmationDialog>
)}
</>
);
};

View File

@ -36,6 +36,8 @@ const createProps = (overrideProps: Partial<Props> = {}): 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', () => (
<ConversationDetailsHeader {...createProps()} isGroup={false} />
));
story.add('Note to self', () => (
<ConversationDetailsHeader {...createProps()} isMe />
));

View File

@ -16,36 +16,44 @@ export type Props = {
canEdit: boolean;
conversation: ConversationType;
i18n: LocalizerType;
isGroup: boolean;
isMe: boolean;
memberships: Array<GroupV2Membership>;
startEditing: (isGroupTitle: boolean) => void;
};
const bem = bemGenerator('module-conversation-details-header');
const bem = bemGenerator('ConversationDetails-header');
export const ConversationDetailsHeader: React.ComponentType<Props> = ({
canEdit,
conversation,
i18n,
isGroup,
isMe,
memberships,
startEditing,
}) => {
const [showingAvatar, setShowingAvatar] = useState(false);
let subtitle: ReactNode;
if (conversation.groupDescription) {
subtitle = (
<GroupDescription
i18n={i18n}
text={conversation.groupDescription}
title={conversation.title}
/>
);
} else if (canEdit) {
subtitle = i18n('ConversationDetailsHeader--add-group-description');
} else {
subtitle = i18n('ConversationDetailsHeader--members', [
memberships.length.toString(),
]);
if (isGroup) {
if (conversation.groupDescription) {
subtitle = (
<GroupDescription
i18n={i18n}
text={conversation.groupDescription}
title={conversation.title}
/>
);
} 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<Props> = ({
i18n={i18n}
size={80}
{...conversation}
noteToSelf={isMe}
onClick={() => setShowingAvatar(true)}
sharedGroupNames={[]}
/>
@ -62,21 +71,22 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
const contents = (
<div>
<div className={bem('title')}>
<Emojify text={conversation.title} />
<Emojify text={isMe ? i18n('noteToSelf') : conversation.title} />
</div>
</div>
);
const avatarLightbox = showingAvatar ? (
<AvatarLightbox
avatarColor={conversation.color}
avatarPath={conversation.avatarPath}
conversationTitle={conversation.title}
i18n={i18n}
isGroup
onClose={() => setShowingAvatar(false)}
/>
) : null;
const avatarLightbox =
showingAvatar && !isMe ? (
<AvatarLightbox
avatarColor={conversation.color}
avatarPath={conversation.avatarPath}
conversationTitle={conversation.title}
i18n={i18n}
isGroup
onClose={() => setShowingAvatar(false)}
/>
) : null;
if (canEdit) {
return (

View File

@ -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>): 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 => (
<ConversationDetailsIcon {...createProps({ 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');

View File

@ -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<Props> = ({
ariaLabel,

View File

@ -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<Props> = ({
conversation,
@ -36,11 +36,13 @@ export const ConversationDetailsMediaList: React.ComponentType<Props> = ({
}) => {
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;
}

View File

@ -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<Props> = ({
{canAddNewMembers && (
<PanelRow
icon={
<div className="module-conversation-details-membership-list__add-members-icon" />
<div className="ConversationDetails-membership-list__add-members-icon" />
}
label={i18n('ConversationDetailsMembershipList--add-members')}
onClick={() => startAddingNewMembers?.()}
@ -118,11 +118,11 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
))}
{showAllMembers === false && shouldHideRestMembers && (
<PanelRow
className="module-conversation-details-membership-list--show-all"
className="ConversationDetails-membership-list--show-all"
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetailsMembershipList--show-all')}
icon="down"
icon={IconType.down}
/>
}
onClick={() => setShowAllMembers(true)}

View File

@ -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 (
<Modal
hasStickyButtons
hasXButton
onClose={onClose}
i18n={i18n}
title={i18n('muteNotificationsTitle')}
>
{muteOptions
.filter(x => x.value > 0)
.map(option => (
<Checkbox
checked={muteExpirationValue === option.value}
disabled={option.disabled}
isRadio
label={option.text}
moduleClassName="ConversationDetails__radio"
name="mute"
onChange={value => value && setMuteExpirationValue(option.value)}
/>
))}
<Modal.ButtonFooter>
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
{i18n('cancel')}
</Button>
<Button onClick={onMuteChange} variant={ButtonVariant.Primary}>
{i18n('mute')}
</Button>
</Modal.ButtonFooter>
</Modal>
);
};

View File

@ -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<PropsType> = (
setMuteExpiration,
setDontNotifyForMentionsIfMuted,
}) => {
// This assertion is here to prevent accidental usage of this component in an untested
// context.
assert(
conversationType === 'group',
'<ConversationNotificationsSettings> 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<PropsType> = (
icon={
<ConversationDetailsIcon
ariaLabel={i18n('muteNotificationsTitle')}
icon="mute"
icon={IconType.mute}
/>
}
label={i18n('muteNotificationsTitle')}
@ -96,7 +88,7 @@ export const ConversationNotificationsSettings: FunctionComponent<PropsType> = (
ariaLabel={i18n(
'ConversationNotificationsSettings__mentions__label'
)}
icon="mention"
icon={IconType.mention}
/>
}
label={i18n('ConversationNotificationsSettings__mentions__label')}

View File

@ -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<PropsType> = ({
icon={
<ConversationDetailsIcon
ariaLabel={i18n('GroupLinkManagement--share')}
icon="share"
icon={IconType.share}
/>
}
label={i18n('GroupLinkManagement--share')}
@ -101,7 +101,7 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
icon={
<ConversationDetailsIcon
ariaLabel={i18n('GroupLinkManagement--reset')}
icon="reset"
icon={IconType.reset}
/>
}
label={i18n('GroupLinkManagement--reset')}

View File

@ -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> = {}): Props => ({
icon: boolean('with icon', overrideProps.icon !== undefined) ? (
<ConversationDetailsIcon ariaLabel="timer" icon="timer" />
<ConversationDetailsIcon ariaLabel="timer" icon={IconType.timer} />
) : null,
label: text('label', (overrideProps.label as string) || ''),
info: text('info', overrideProps.info || ''),
@ -25,7 +25,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
actions: boolean('with action', overrideProps.actions !== undefined) ? (
<ConversationDetailsIcon
ariaLabel="trash"
icon="trash"
icon={IconType.trash}
onClick={action('action onClick')}
/>
) : null,

View File

@ -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<Props> = ({
alwaysShowActions,

View File

@ -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<Props> = ({

View File

@ -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<PropsType> = ({
return (
<div className="conversation-details-panel">
<div className="module-conversation-details__tabs">
<div className="ConversationDetails__tabs">
<div
className={classNames({
'module-conversation-details__tab': true,
'module-conversation-details__tab--selected':
selectedTab === Tab.Requests,
ConversationDetails__tab: true,
'ConversationDetails__tab--selected': selectedTab === Tab.Requests,
})}
onClick={() => {
setSelectedTab(Tab.Requests);
@ -98,9 +97,8 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
<div
className={classNames({
'module-conversation-details__tab': true,
'module-conversation-details__tab--selected':
selectedTab === Tab.Pending,
ConversationDetails__tab: true,
'ConversationDetails__tab--selected': selectedTab === Tab.Pending,
})}
onClick={() => {
setSelectedTab(Tab.Pending);
@ -323,7 +321,7 @@ function MembersPendingAdminApproval({
<>
<button
type="button"
className="module-button__small module-conversation-details__action-button"
className="module-button__small ConversationDetails__action-button"
onClick={() => {
setStagedMemberships([
{
@ -337,7 +335,7 @@ function MembersPendingAdminApproval({
</button>
<button
type="button"
className="module-button__small module-conversation-details__action-button"
className="module-button__small ConversationDetails__action-button"
onClick={() => {
setStagedMemberships([
{
@ -354,7 +352,7 @@ function MembersPendingAdminApproval({
}
/>
))}
<div className="module-conversation-details__pending--info">
<div className="ConversationDetails__pending--info">
{i18n('PendingRequests--info', [conversation.title])}
</div>
</PanelSection>
@ -414,7 +412,7 @@ function MembersPendingProfileKey({
conversation.areWeAdmin ? (
<ConversationDetailsIcon
ariaLabel={i18n('PendingInvites--revoke-for-label')}
icon="trash"
icon={IconType.trash}
onClick={() => {
setStagedMemberships([
{
@ -451,7 +449,7 @@ function MembersPendingProfileKey({
conversation.areWeAdmin ? (
<ConversationDetailsIcon
ariaLabel={i18n('PendingInvites--revoke-for-label')}
icon="trash"
icon={IconType.trash}
onClick={() => {
setStagedMemberships(
pendingMemberships.map(membership => ({
@ -467,7 +465,7 @@ function MembersPendingProfileKey({
))}
</PanelSection>
)}
<div className="module-conversation-details__pending--info">
<div className="ConversationDetails__pending--info">
{i18n('PendingInvites--info')}
</div>
</PanelSection>

View File

@ -61,7 +61,7 @@ import { getConversationMembers } from '../util/getConversationMembers';
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { ReadStatus } from '../messages/MessageReadStatus';
import { SendState, SendStatus } from '../messages/MessageSendState';
import { SendStatus } from '../messages/MessageSendState';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as durations from '../util/durations';
import {
@ -4285,51 +4285,6 @@ export class ConversationModel extends window.Backbone
return !this.get('left');
}
async endSession(): Promise<void> {
if (isDirectConversation(this.attributes)) {
const now = Date.now();
const pendingSendState: SendState = {
status: SendStatus.Pending,
updatedAt: now,
};
const messageAttributes: Partial<MessageAttributesType> = {
conversationId: this.id,
type: 'outgoing',
sent_at: now,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: now,
sendStateByConversationId: {
[this.id]: pendingSendState,
[window.ConversationController.getOurConversationIdOrThrow()]: pendingSendState,
},
flags: Proto.DataMessage.Flags.END_SESSION,
};
// TODO: DESKTOP-722
const model = new window.Whisper.Message(
messageAttributes as MessageAttributesType
);
const id = await window.Signal.Data.saveMessage(model.attributes);
model.set({ id });
const message = window.MessageController.register(model.id, model);
this.addSingleMessage(message);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const uuid = this.get('uuid')!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const e164 = this.get('e164')!;
message.sendUtilityMessageWithRetry({
type: 'session-reset',
uuid,
e164,
now,
});
}
}
async leaveGroup(): Promise<void> {
const now = Date.now();
if (this.get('type') === 'group') {

View File

@ -6,7 +6,6 @@ import {
CustomError,
GroupV1Update,
MessageAttributesType,
RetryOptions,
ReactionAttributesType,
ShallowChallengeError,
QuotedMessageType,
@ -1209,17 +1208,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
async retrySend(): Promise<void> {
const retryOptions = this.get('retryOptions');
if (retryOptions) {
if (!window.textsecure.messaging) {
log.error('retrySend: Cannot retry since we are offline!');
return;
}
this.unset('errors');
this.unset('retryOptions');
return this.sendUtilityMessageWithRetry(retryOptions);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = this.getConversation()!;
@ -1524,48 +1512,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
updateLeftPane();
}
// Currently used only for messages that have to be retried when the server
// responds with 428 and we have to retry sending the message on challenge
// solution.
//
// Supported types of messages:
// * `session-reset` see `endSession` in `ts/models/conversations.ts`
async sendUtilityMessageWithRetry(options: RetryOptions): Promise<void> {
if (options.type === 'session-reset') {
const conv = this.getConversation();
if (!conv) {
throw new Error(
`Failed to find conversation for message: ${this.idForLogging()}`
);
}
if (!window.textsecure.messaging) {
throw new Error('Offline');
}
this.set({
retryOptions: options,
});
const sendOptions = await getSendOptions(conv.attributes);
await this.send(
handleMessageSend(
window.textsecure.messaging.resetSession(
options.uuid,
options.e164,
options.now,
sendOptions
),
{ messageIds: [], sendType: 'resetSession' }
)
);
return;
}
throw new Error(`Unsupported retriable type: ${options.type}`);
}
async sendSyncMessageOnly(
dataMessage: Uint8Array,
saveErrors?: (errors: Array<Error>) => void

View File

@ -25,7 +25,7 @@ export type SmartConversationDetailsProps = {
loadRecentMediaItems: (limit: number) => void;
setDisappearingMessages: (seconds: number) => void;
showAllMedia: () => void;
showGroupChatColorEditor: () => void;
showChatColorEditor: () => void;
showGroupLinkManagement: () => void;
showGroupV2Permissions: () => void;
showConversationNotificationsSettings: () => void;
@ -42,6 +42,10 @@ export type SmartConversationDetailsProps = {
) => Promise<void>;
onBlock: () => void;
onLeave: () => void;
onUnblock: () => void;
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
onOutgoingAudioCallInConversation: () => unknown;
onOutgoingVideoCallInConversation: () => unknown;
};
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
@ -75,6 +79,7 @@ const mapStateToProps = (
...getGroupMemberships(conversation, conversationSelector),
userAvatarData: conversation.avatars || [],
hasGroupLink,
isGroup: conversation.type === 'group',
};
};

View File

@ -33,17 +33,14 @@ export type OwnProps = {
onMoveToInbox: () => void;
onOutgoingAudioCallInConversation: () => void;
onOutgoingVideoCallInConversation: () => void;
onResetSession: () => void;
onSearchInConversation: () => void;
onSetDisappearingMessages: (seconds: number) => void;
onSetMuteNotifications: (seconds: number) => void;
onSetPin: (value: boolean) => void;
onShowAllMedia: () => void;
onShowChatColorEditor: () => void;
onShowContactModal: (contactId: string) => void;
onShowConversationDetails: () => void;
onShowGroupMembers: () => void;
onShowSafetyNumber: () => void;
};
const getOutgoingCallButtonStyle = (

View File

@ -20,7 +20,6 @@ import { assert } from '../util/assert';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { SenderKeys } from '../LibSignalStores';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { MIMETypeToString } from '../types/MIME';
@ -1684,84 +1683,6 @@ export default class MessageSender {
});
}
async resetSession(
uuid: string,
e164: string,
timestamp: number,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
log.info('resetSession: start');
const proto = new Proto.DataMessage();
proto.body = 'TERMINATE';
proto.flags = Proto.DataMessage.Flags.END_SESSION;
proto.timestamp = timestamp;
const identifier = uuid || e164;
const theirUuid = uuid ? new UUID(uuid) : UUID.checkedLookup(e164);
const logError = (prefix: string) => (error: Error) => {
log.error(prefix, error && error.stack ? error.stack : error);
throw error;
};
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
const sendToContactPromise = window.textsecure.storage.protocol
.archiveAllSessions(theirUuid)
.catch(logError('resetSession/archiveAllSessions1 error:'))
.then(async () => {
log.info(
'resetSession: finished closing local sessions, now sending to contact'
);
return handleMessageSend(
this.sendIndividualProto({
identifier,
proto,
timestamp,
contentHint: ContentHint.RESENDABLE,
options,
}),
{
messageIds: [],
sendType: 'resetSession',
}
).catch(logError('resetSession/sendToContact error:'));
})
.then(async result => {
await window.textsecure.storage.protocol
.archiveAllSessions(theirUuid)
.catch(logError('resetSession/archiveAllSessions2 error:'));
return result;
});
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
// We already sent the reset session to our other devices in the code above!
if ((e164 && e164 === myNumber) || (uuid && uuid === myUuid)) {
return sendToContactPromise;
}
const encodedDataMessage = Proto.DataMessage.encode(proto).finish();
const sendSyncPromise = this.sendSyncMessage({
encodedDataMessage,
timestamp,
destination: e164,
destinationUuid: uuid,
expirationStartTimestamp: null,
conversationIdsSentTo: [],
conversationIdsWithSealedSender: new Set(),
options,
}).catch(logError('resetSession/sendSync error:'));
const responses = await Promise.all([
sendToContactPromise,
sendSyncPromise,
]);
return responses[0];
}
async sendExpirationTimerUpdateToIdentifier(
identifier: string,
expireTimer: number | undefined,

View File

@ -388,7 +388,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
onSetDisappearingMessages: (seconds: number) =>
this.setDisappearingMessages(seconds),
onDeleteMessages: () => this.destroyMessages(),
onResetSession: () => this.endSession(),
onSearchInConversation: () => {
const { searchInConversation } = window.reduxActions.search;
const name = isMe(this.model.attributes)
@ -400,65 +399,16 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
onSetPin: this.setPin.bind(this),
// These are view only and don't update the Conversation model, so they
// need a manual update call.
onOutgoingAudioCallInConversation: async () => {
log.info(
'onOutgoingAudioCallInConversation: about to start an audio call'
);
onOutgoingAudioCallInConversation: this.onOutgoingAudioCallInConversation.bind(
this
),
onOutgoingVideoCallInConversation: this.onOutgoingVideoCallInConversation.bind(
this
),
const isVideoCall = false;
if (await this.isCallSafe()) {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
log.info('onOutgoingAudioCallInConversation: started the call');
} else {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onOutgoingVideoCallInConversation: async () => {
log.info(
'onOutgoingVideoCallInConversation: about to start a video call'
);
const isVideoCall = true;
if (this.model.get('announcementsOnly') && !this.model.areWeAdmin()) {
showToast(ToastCannotStartGroupCall);
return;
}
if (await this.isCallSafe()) {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
log.info('onOutgoingVideoCallInConversation: started the call');
} else {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onShowChatColorEditor: () => {
this.showChatColorEditor();
},
onShowConversationDetails: () => {
this.showConversationDetails();
},
onShowSafetyNumber: () => {
this.showSafetyNumber();
},
onShowAllMedia: () => {
this.showAllMedia();
},
@ -841,6 +791,52 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.$('.ConversationView__template').append(this.conversationView.el);
}
async onOutgoingVideoCallInConversation(): Promise<void> {
log.info('onOutgoingVideoCallInConversation: about to start a video call');
const isVideoCall = true;
if (this.model.get('announcementsOnly') && !this.model.areWeAdmin()) {
showToast(ToastCannotStartGroupCall);
return;
}
if (await this.isCallSafe()) {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
log.info('onOutgoingVideoCallInConversation: started the call');
} else {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
);
}
}
async onOutgoingAudioCallInConversation(): Promise<void> {
log.info('onOutgoingAudioCallInConversation: about to start an audio call');
const isVideoCall = false;
if (await this.isCallSafe()) {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
log.info('onOutgoingAudioCallInConversation: started the call');
} else {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
}
async longRunningTaskWrapper<T>({
name,
task,
@ -2626,7 +2622,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
setDisappearingMessages: this.setDisappearingMessages.bind(this),
showAllMedia: this.showAllMedia.bind(this),
showContactModal: this.showContactModal.bind(this),
showGroupChatColorEditor: this.showChatColorEditor.bind(this),
showChatColorEditor: this.showChatColorEditor.bind(this),
showGroupLinkManagement: this.showGroupLinkManagement.bind(this),
showGroupV2Permissions: this.showGroupV2Permissions.bind(this),
showConversationNotificationsSettings: this.showConversationNotificationsSettings.bind(
@ -2639,6 +2635,20 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
),
onLeave,
onBlock,
onUnblock: () => {
this.syncMessageRequestResponse(
'onUnblock',
this.model,
messageRequestEnum.ACCEPT
);
},
setMuteExpiration: this.setMuteExpiration.bind(this),
onOutgoingAudioCallInConversation: this.onOutgoingAudioCallInConversation.bind(
this
),
onOutgoingVideoCallInConversation: this.onOutgoingVideoCallInConversation.bind(
this
),
};
const view = new Whisper.ReactWrapperView({
@ -2811,12 +2821,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
);
}
endSession(): void {
const { model }: { model: ConversationModel } = this;
model.endSession();
}
async loadRecentMediaItems(limit: number): Promise<void> {
const { model }: { model: ConversationModel } = this;