Conversation details screen for 1:1 chats
This commit is contained in:
parent
3a507349cd
commit
2e438aa876
35 changed files with 1357 additions and 1102 deletions
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
621
stylesheets/components/ConversationDetails.scss
Normal file
621
stylesheets/components/ConversationDetails.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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} />
|
||||
));
|
||||
|
|
|
@ -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,25 +403,26 @@ 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),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{isGroup && (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetails--notifications')}
|
||||
icon="notifications"
|
||||
icon={IconType.notifications}
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationDetails--notifications')}
|
||||
|
@ -326,8 +433,28 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!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>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PanelSection>
|
||||
|
||||
{isGroup && (
|
||||
<ConversationDetailsMembershipList
|
||||
canAddNewMembers={canEditGroupInfo}
|
||||
conversationId={conversation.id}
|
||||
|
@ -338,14 +465,16 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
setModalState(ModalState.AddingGroupMembers);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGroup && (
|
||||
<PanelSection>
|
||||
{isAdmin || hasGroupLink ? (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetails--group-link')}
|
||||
icon="link"
|
||||
icon={IconType.link}
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationDetails--group-link')}
|
||||
|
@ -357,7 +486,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetails--requests-and-invites')}
|
||||
icon="invites"
|
||||
icon={IconType.invites}
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationDetails--requests-and-invites')}
|
||||
|
@ -369,7 +498,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('permissions')}
|
||||
icon="lock"
|
||||
icon={IconType.lock}
|
||||
/>
|
||||
}
|
||||
label={i18n('permissions')}
|
||||
|
@ -377,6 +506,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
/>
|
||||
) : null}
|
||||
</PanelSection>
|
||||
)}
|
||||
|
||||
<ConversationDetailsMediaList
|
||||
conversation={conversation}
|
||||
|
@ -386,14 +516,19 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
showLightboxForMedia={showLightboxForMedia}
|
||||
/>
|
||||
|
||||
{!conversation.isMe && (
|
||||
<ConversationDetailsActions
|
||||
i18n={i18n}
|
||||
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isBlocked={Boolean(conversation.isBlocked)}
|
||||
isGroup={isGroup}
|
||||
left={Boolean(conversation.left)}
|
||||
onLeave={onLeave}
|
||||
onBlock={onBlock}
|
||||
onLeave={onLeave}
|
||||
onUnblock={onUnblock}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalNode}
|
||||
</div>
|
||||
|
|
|
@ -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 />
|
||||
));
|
||||
|
|
|
@ -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 = (
|
||||
let blockNode: ReactNode;
|
||||
if (isGroup) {
|
||||
blockNode = (
|
||||
<PanelRow
|
||||
disabled={cannotLeaveBecauseYouAreLastAdmin}
|
||||
onClick={() => setConfirmingBlock(true)}
|
||||
onClick={() => gGroupBlock(true)}
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetailsActions--block-group')}
|
||||
icon="block"
|
||||
icon={IconType.block}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div className="module-conversation-details__block-group">
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 />
|
||||
));
|
||||
|
|
|
@ -16,22 +16,27 @@ 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 (isGroup) {
|
||||
if (conversation.groupDescription) {
|
||||
subtitle = (
|
||||
<GroupDescription
|
||||
|
@ -47,6 +52,9 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
memberships.length.toString(),
|
||||
]);
|
||||
}
|
||||
} else if (!isMe) {
|
||||
subtitle = conversation.phoneNumber;
|
||||
}
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
|
@ -54,6 +62,7 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
i18n={i18n}
|
||||
size={80}
|
||||
{...conversation}
|
||||
noteToSelf={isMe}
|
||||
onClick={() => setShowingAvatar(true)}
|
||||
sharedGroupNames={[]}
|
||||
/>
|
||||
|
@ -62,12 +71,13 @@ 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 ? (
|
||||
const avatarLightbox =
|
||||
showingAvatar && !isMe ? (
|
||||
<AvatarLightbox
|
||||
avatarColor={conversation.color}
|
||||
avatarPath={conversation.avatarPath}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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')}
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> = ({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in a new issue