Conversation details changes for PNP
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
1a74da0c26
commit
eb82ace2de
49 changed files with 1660 additions and 699 deletions
|
@ -1350,6 +1350,18 @@
|
|||
"icu:showSafetyNumber": {
|
||||
"messageformat": "View safety number"
|
||||
},
|
||||
"icu:AboutContactModal__title": {
|
||||
"messageformat": "About",
|
||||
"description": "Title of About modal"
|
||||
},
|
||||
"icu:AboutContactModal__signal-connection": {
|
||||
"messageformat": "Signal Connection",
|
||||
"description": "Text of a button on About modal leading to an education modal"
|
||||
},
|
||||
"icu:AboutContactModal__system-contact": {
|
||||
"messageformat": "{name} is in your system contacts",
|
||||
"description": "Text of a row in the About modal describing that the contact is in system contacts"
|
||||
},
|
||||
"icu:ContactModal__showSafetyNumber": {
|
||||
"messageformat": "View safety number",
|
||||
"description": "Contact modal, label for button to show safety number modal"
|
||||
|
@ -5188,7 +5200,7 @@
|
|||
"description": "Title for the contact name spoofing review dialog in groups"
|
||||
},
|
||||
"icu:ContactSpoofingReviewDialog__group__description": {
|
||||
"messageformat": "{count, plural, one {# group member} other {# group members}} have similar names. Review the members below or choose to take action.",
|
||||
"messageformat": "{count, plural, one {# group member} other {# group members}} have the same name, review the members below or choose to take action.",
|
||||
"description": "Description for the group contact spoofing review dialog"
|
||||
},
|
||||
"icu:ContactSpoofingReviewDialog__group__multiple-conflicts__description": {
|
||||
|
@ -5197,7 +5209,15 @@
|
|||
},
|
||||
"icu:ContactSpoofingReviewDialog__group__members-header": {
|
||||
"messageformat": "Members",
|
||||
"description": "Header in the group contact spoofing review dialog. After this header, there will be a list of members"
|
||||
"description": "(Deleted 01/31/2024) Header in the group contact spoofing review dialog. After this header, there will be a list of members"
|
||||
},
|
||||
"icu:ContactSpoofingReviewDialog__group__members__no-shared-groups": {
|
||||
"messageformat": "No other groups in common",
|
||||
"description": "Informational text displayed next to a contact on ContactSpoofingReviewDialog"
|
||||
},
|
||||
"icu:ContactSpoofingReviewDialog__signal-connection": {
|
||||
"messageformat": "Signal Connection",
|
||||
"description": "Text of a button on ContactSpoofingReviewDialog leading to an education modal"
|
||||
},
|
||||
"icu:ContactSpoofingReviewDialog__group__name-change-info": {
|
||||
"messageformat": "Recently changed their profile name from {oldName} to {newName}",
|
||||
|
|
3
images/icons/v3/chevron/chevron-right-bold.svg
Normal file
3
images/icons/v3/chevron/chevron-right-bold.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.79289 4.29289C9.18342 3.90237 9.81658 3.90237 10.2071 4.29289L17.2071 11.2929C17.5976 11.6834 17.5976 12.3166 17.2071 12.7071L10.2071 19.7071C9.81658 20.0976 9.18342 20.0976 8.79289 19.7071C8.40237 19.3166 8.40237 18.6834 8.79289 18.2929L15.0858 12L8.79289 5.70711C8.40237 5.31658 8.40237 4.68342 8.79289 4.29289Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 485 B |
8
images/icons/v3/connections/connections.svg
Normal file
8
images/icons/v3/connections/connections.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.94995 1.25C5.87888 1.25 4.19995 2.92893 4.19995 5C4.19995 7.07107 5.87888 8.75 7.94995 8.75C10.021 8.75 11.7 7.07107 11.7 5C11.7 2.92893 10.021 1.25 7.94995 1.25ZM5.94995 5C5.94995 3.89543 6.84538 3 7.94995 3C9.05452 3 9.94995 3.89543 9.94995 5C9.94995 6.10457 9.05452 7 7.94995 7C6.84538 7 5.94995 6.10457 5.94995 5Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.94995 15.25C5.87888 15.25 4.19995 16.9289 4.19995 19C4.19995 21.0711 5.87888 22.75 7.94995 22.75C10.021 22.75 11.7 21.0711 11.7 19C11.7 16.9289 10.021 15.25 7.94995 15.25ZM5.94995 19C5.94995 17.8954 6.84538 17 7.94995 17C9.05452 17 9.94995 17.8954 9.94995 19C9.94995 20.1046 9.05452 21 7.94995 21C6.84538 21 5.94995 20.1046 5.94995 19Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3 5C12.3 2.92893 13.979 1.25 16.05 1.25C18.1211 1.25 19.8 2.92893 19.8 5C19.8 7.07107 18.1211 8.75 16.05 8.75C13.979 8.75 12.3 7.07107 12.3 5ZM16.05 3C14.9455 3 14.05 3.89543 14.05 5C14.05 6.10457 14.9455 7 16.05 7C17.1546 7 18.05 6.10457 18.05 5C18.05 3.89543 17.1546 3 16.05 3Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.05 15.25C13.979 15.25 12.3 16.9289 12.3 19C12.3 21.0711 13.979 22.75 16.05 22.75C18.1211 22.75 19.8 21.0711 19.8 19C19.8 16.9289 18.1211 15.25 16.05 15.25ZM14.05 19C14.05 17.8954 14.9455 17 16.05 17C17.1546 17 18.05 17.8954 18.05 19C18.05 20.1046 17.1546 21 16.05 21C14.9455 21 14.05 20.1046 14.05 19Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.375 12C16.375 9.92893 18.0539 8.25 20.125 8.25C22.1961 8.25 23.875 9.92893 23.875 12C23.875 14.0711 22.1961 15.75 20.125 15.75C18.0539 15.75 16.375 14.0711 16.375 12ZM20.125 10C19.0204 10 18.125 10.8954 18.125 12C18.125 13.1046 19.0204 14 20.125 14C21.2296 14 22.125 13.1046 22.125 12C22.125 10.8954 21.2296 10 20.125 10Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.875 8.25C1.80393 8.25 0.125 9.92893 0.125 12C0.125 14.0711 1.80393 15.75 3.875 15.75C5.94607 15.75 7.625 14.0711 7.625 12C7.625 9.92893 5.94607 8.25 3.875 8.25ZM1.875 12C1.875 10.8954 2.77043 10 3.875 10C4.97957 10 5.875 10.8954 5.875 12C5.875 13.1046 4.97957 14 3.875 14C2.77043 14 1.875 13.1046 1.875 12Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -250,6 +250,7 @@ $rtl-icon-map: (
|
|||
'chevron-shallow-right.svg': 'chevron-shallow-left.svg',
|
||||
'chevron-left-compact-bold.svg': 'chevron-right-compact-bold.svg',
|
||||
'chevron-right-compact-bold.svg': 'chevron-left-compact-bold.svg',
|
||||
'chevron-right-bold.svg': 'chevron-left-bold.svg',
|
||||
'arrow-left.svg': 'arrow-right.svg',
|
||||
'arrow-right.svg': 'arrow-left.svg',
|
||||
|
||||
|
|
101
stylesheets/components/AboutContactModal.scss
Normal file
101
stylesheets/components/AboutContactModal.scss
Normal file
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.AboutContactModal {
|
||||
&__headerTitle.module-Modal__headerTitle {
|
||||
// No padding between header and avatar
|
||||
padding-block-end: 0;
|
||||
}
|
||||
|
||||
&__body_inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-inline: 8px;
|
||||
padding-block-end: 20px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__row--centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include font-title-2;
|
||||
|
||||
margin: 0;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
&__row__icon {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
vertical-align: text-top;
|
||||
flex-shrink: 0;
|
||||
|
||||
@mixin about-modal-icon($url) {
|
||||
@include light-theme {
|
||||
@include color-svg($url, $color-black);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg($url, $color-gray-05);
|
||||
}
|
||||
}
|
||||
|
||||
&--profile {
|
||||
@include about-modal-icon('../images/icons/v3/person/person-compact.svg');
|
||||
}
|
||||
|
||||
&--connections {
|
||||
@include about-modal-icon(
|
||||
'../images/icons/v3/connections/connections.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--person {
|
||||
@include about-modal-icon(
|
||||
'../images/icons/v3/person/person-circle-compact.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--phone {
|
||||
@include about-modal-icon('../images/icons/v3/phone/phone-compact.svg');
|
||||
}
|
||||
|
||||
&--group {
|
||||
@include about-modal-icon('../images/icons/v3/group/group.svg');
|
||||
}
|
||||
|
||||
&--about {
|
||||
@include about-modal-icon('../images/icons/v3/edit/edit.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&__signal-connection {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@include button-reset();
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-gray-45
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
28
stylesheets/components/CollidingAvatars.scss
Normal file
28
stylesheets/components/CollidingAvatars.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.CollidingAvatars {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
&__avatar {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
&__avatar:nth-child(1) {
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path#clip-source
|
||||
clip-path: var(--clip-path);
|
||||
}
|
||||
|
||||
&__avatar:nth-child(2) {
|
||||
inset-block-start: 12px;
|
||||
inset-inline-start: 12px;
|
||||
}
|
||||
|
||||
&__clip_svg {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
|
@ -10,8 +10,38 @@
|
|||
margin-bottom: 16px;
|
||||
|
||||
&__name {
|
||||
@include font-title-2;
|
||||
@include button-reset();
|
||||
@include font-title-1;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
margin-top: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__name__chevron {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
// Align with the text
|
||||
position: relative;
|
||||
inset-block-start: 2px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-gray-90
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-gray-05
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
&__description {
|
||||
margin-top: 4px;
|
||||
margin-block: 0 16px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
@ -29,19 +29,19 @@
|
|||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
margin-block: 20px;
|
||||
margin-block: 12px;
|
||||
margin-inline: 0;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-90;
|
||||
background: $color-gray-75;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin-top: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
.module-Button:not(:last-child) {
|
||||
margin-inline-end: 12px;
|
||||
|
|
|
@ -3,16 +3,21 @@
|
|||
|
||||
.module-ContactSpoofingReviewDialogPerson {
|
||||
display: flex;
|
||||
padding-block: 8px;
|
||||
gap: 16px;
|
||||
|
||||
&:is(button) {
|
||||
@include button-reset;
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-inline-start: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&__contact-name {
|
||||
@include font-body-1-bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__property {
|
||||
|
@ -26,10 +31,68 @@
|
|||
color: $color-gray-05;
|
||||
}
|
||||
|
||||
&--callout {
|
||||
@include font-body-2-italic;
|
||||
margin-block: 12px;
|
||||
margin-inline: 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
&__icon {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
vertical-align: text-top;
|
||||
flex-shrink: 0;
|
||||
|
||||
@mixin contact-spoofing-icon($url) {
|
||||
@include light-theme {
|
||||
@include color-svg($url, $color-gray-90);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg($url, $color-gray-05);
|
||||
}
|
||||
}
|
||||
|
||||
&--connections {
|
||||
@include contact-spoofing-icon(
|
||||
'../images/icons/v3/connections/connections.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--person {
|
||||
@include contact-spoofing-icon(
|
||||
'../images/icons/v3/person/person.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--phone {
|
||||
@include contact-spoofing-icon(
|
||||
'../images/icons/v3/phone/phone-compact.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--group {
|
||||
@include contact-spoofing-icon('../images/icons/v3/group/group.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&__signal-connection {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@include button-reset();
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-gray-45
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,82 +2,6 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.ConversationDetails {
|
||||
&-header {
|
||||
&__root {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-block: 0 20px;
|
||||
margin-inline: 0;
|
||||
padding-block: 0;
|
||||
padding-inline: 24px;
|
||||
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;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
@include font-body-1;
|
||||
color: $color-gray-60;
|
||||
justify-content: center;
|
||||
padding-bottom: 6px;
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
|
||||
&__about,
|
||||
&__phone-number {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
&__root--editable &__title {
|
||||
$icon: '../images/icons/v3/edit/edit.svg';
|
||||
|
||||
&::after {
|
||||
$size: 24px;
|
||||
|
||||
content: '';
|
||||
height: $size;
|
||||
inset-inline-start: $size + 13px;
|
||||
margin-inline-start: -$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(32px);
|
||||
}
|
||||
|
|
98
stylesheets/components/ConversationDetailsHeader.scss
Normal file
98
stylesheets/components/ConversationDetailsHeader.scss
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.ConversationDetailsHeader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-block: 0 20px;
|
||||
margin-inline: 0;
|
||||
padding-block: 0;
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&__edit-button,
|
||||
&__about-button {
|
||||
@include button-reset();
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include font-title-1;
|
||||
font-weight: 400;
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 12px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
@include font-body-1;
|
||||
color: $color-gray-60;
|
||||
justify-content: center;
|
||||
padding-bottom: 6px;
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
|
||||
&__about,
|
||||
&__phone-number {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-button &__title {
|
||||
$icon: '../images/icons/v3/edit/edit.svg';
|
||||
|
||||
&::after {
|
||||
$size: 24px;
|
||||
|
||||
content: '';
|
||||
height: $size;
|
||||
inset-inline-start: $size + 13px;
|
||||
margin-inline-start: -$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-button:hover &__title::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&__about-icon {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
// Align with the text
|
||||
position: relative;
|
||||
inset-block-start: 2px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-black
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-gray-05
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,39 @@
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include button-reset();
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__title span {
|
||||
@include font-title-1;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__title__chevron {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
// Align with the text
|
||||
position: relative;
|
||||
inset-block-start: 2px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-gray-90
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right-bold.svg',
|
||||
$color-gray-05
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__profile-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -17,6 +50,7 @@
|
|||
|
||||
@include font-title-2;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 0;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
|
@ -31,7 +65,7 @@
|
|||
@include font-body-2;
|
||||
margin-block: 0;
|
||||
margin-inline: auto;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 500px;
|
||||
|
||||
@include light-theme {
|
||||
|
@ -43,70 +77,110 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__note-to-self {
|
||||
@include font-body-2;
|
||||
|
||||
padding-block: 0;
|
||||
padding-inline: 16px;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
&__membership {
|
||||
@include font-body-2;
|
||||
user-select: none;
|
||||
|
||||
padding-block: 0;
|
||||
max-width: 255px;
|
||||
margin-inline: auto;
|
||||
padding-block: 16px;
|
||||
padding-inline: 20px;
|
||||
|
||||
padding-inline: 16px;
|
||||
border-radius: 18px;
|
||||
border-style: solid;
|
||||
border-width: 1.5px;
|
||||
|
||||
@include light-theme() {
|
||||
border-color: $color-gray-05;
|
||||
}
|
||||
@include dark-theme() {
|
||||
border-color: $color-gray-80;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
color: $color-gray-90;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
color: $color-gray-02;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
vertical-align: text-top;
|
||||
margin-inline-end: 8px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v3/group/group.svg', $color-black);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/group/group.svg',
|
||||
$color-gray-05
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include font-body-2-bold;
|
||||
// Cancel bold
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
&__message-request-warning {
|
||||
@include font-body-2;
|
||||
&__warning {
|
||||
line-height: 20px;
|
||||
|
||||
&__message {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
|
||||
&::before {
|
||||
&__icon {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 14px;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
margin-inline-end: 8px;
|
||||
width: 14px;
|
||||
width: 18px;
|
||||
vertical-align: middle;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/info/info.svg',
|
||||
$color-gray-60
|
||||
$color-gray-90
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/info/info.svg',
|
||||
$color-gray-25
|
||||
$color-gray-02
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__learn-more {
|
||||
@include button-reset();
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__linkNotification {
|
||||
@include font-body-2;
|
||||
|
||||
margin-top: 15px;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
|
||||
|
|
|
@ -2,27 +2,63 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.SignalConnectionsModal {
|
||||
padding-inline: 8px;
|
||||
padding-block-end: 20px;
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/connections/connections-display.svg',
|
||||
$color-ultramarine-light
|
||||
);
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/connections/connections-display.svg',
|
||||
$color-gray-90
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/connections/connections-display.svg',
|
||||
$color-gray-25
|
||||
);
|
||||
}
|
||||
display: block;
|
||||
height: 69px;
|
||||
height: 48px;
|
||||
margin-block: 0;
|
||||
margin-inline: auto;
|
||||
margin-bottom: 24px;
|
||||
width: 75px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
margin-block: 16px;
|
||||
margin-block: 20px;
|
||||
margin-inline: 0;
|
||||
padding-inline-start: 12px;
|
||||
|
||||
li {
|
||||
margin-block: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
list-style: none;
|
||||
margin-block: 16px;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
li::before {
|
||||
display: block;
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
border-radius: 6px;
|
||||
|
||||
@include light-theme {
|
||||
background-color: $color-gray-20;
|
||||
}
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
padding-block: 10px;
|
||||
padding-inline: 16px;
|
||||
min-height: 56px;
|
||||
user-select: none;
|
||||
|
||||
border-top-width: 1px;
|
||||
|
@ -72,4 +74,8 @@
|
|||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__custom-info {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
// New style: components
|
||||
@import './components/About.scss';
|
||||
@import './components/AboutContactModal.scss';
|
||||
@import './components/AddGroupMembersModal.scss';
|
||||
@import './components/AddUserToAnotherGroupModal.scss';
|
||||
@import './components/AnnouncementsOnlyGroupBanner.scss';
|
||||
|
@ -54,6 +55,7 @@
|
|||
@import './components/ChatColorPicker.scss';
|
||||
@import './components/Checkbox.scss';
|
||||
@import './components/CircleCheckbox.scss';
|
||||
@import './components/CollidingAvatars.scss';
|
||||
@import './components/CompositionArea.scss';
|
||||
@import './components/CompositionRecording.scss';
|
||||
@import './components/CompositionRecordingDraft.scss';
|
||||
|
@ -68,6 +70,7 @@
|
|||
@import './components/ContactSpoofingReviewDialogPerson.scss';
|
||||
@import './components/ContextMenu.scss';
|
||||
@import './components/ConversationDetails.scss';
|
||||
@import './components/ConversationDetailsHeader.scss';
|
||||
@import './components/ConversationHeader.scss';
|
||||
@import './components/ConversationHero.scss';
|
||||
@import './components/ConversationMergeNotification.scss';
|
||||
|
|
|
@ -36,6 +36,7 @@ export enum AvatarBlur {
|
|||
|
||||
export enum AvatarSize {
|
||||
TWENTY = 20,
|
||||
TWENTY_FOUR = 24,
|
||||
TWENTY_EIGHT = 28,
|
||||
THIRTY_TWO = 32,
|
||||
THIRTY_SIX = 36,
|
||||
|
@ -44,6 +45,7 @@ export enum AvatarSize {
|
|||
FIFTY_TWO = 52,
|
||||
EIGHTY = 80,
|
||||
NINETY_SIX = 96,
|
||||
TWO_HUNDRED_SIXTEEN = 216,
|
||||
}
|
||||
|
||||
type BadgePlacementType = { bottom: number; right: number };
|
||||
|
|
29
ts/components/CollidingAvatars.stories.tsx
Normal file
29
ts/components/CollidingAvatars.stories.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { PropsType } from './CollidingAvatars';
|
||||
import { CollidingAvatars } from './CollidingAvatars';
|
||||
import { type ComponentMeta } from '../storybook/types';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const alice = getDefaultConversation();
|
||||
const bob = getDefaultConversation();
|
||||
|
||||
export default {
|
||||
title: 'Components/CollidingAvatars',
|
||||
component: CollidingAvatars,
|
||||
argTypes: {},
|
||||
args: {
|
||||
i18n,
|
||||
conversations: [alice, bob],
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
export function Defaults(args: PropsType): JSX.Element {
|
||||
return <CollidingAvatars {...args} />;
|
||||
}
|
80
ts/components/CollidingAvatars.tsx
Normal file
80
ts/components/CollidingAvatars.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
conversations: ReadonlyArray<ConversationType>;
|
||||
}>;
|
||||
|
||||
const MAX_AVATARS = 2;
|
||||
|
||||
export function CollidingAvatars({
|
||||
i18n,
|
||||
conversations,
|
||||
}: PropsType): JSX.Element {
|
||||
const clipId = useMemo(() => uuid(), []);
|
||||
const onRef = useCallback(
|
||||
(elem: HTMLDivElement | null): void => {
|
||||
if (elem) {
|
||||
// Note that these cannot be set through html attributes
|
||||
elem.style.setProperty('--clip-path', `url(#${clipId})`);
|
||||
}
|
||||
},
|
||||
[clipId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="CollidingAvatars" ref={onRef}>
|
||||
{conversations.slice(0, MAX_AVATARS).map(({ id, type, ...convo }) => {
|
||||
return (
|
||||
<Avatar
|
||||
key={id}
|
||||
className="CollidingAvatars__avatar"
|
||||
i18n={i18n}
|
||||
size={AvatarSize.TWENTY_FOUR}
|
||||
conversationType={type}
|
||||
badge={undefined}
|
||||
{...convo}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/*
|
||||
This clip path is a rectangle with the right-bottom corner cut off
|
||||
by a circle:
|
||||
|
||||
AAAAAAA
|
||||
AAAAAAA
|
||||
AAAAA
|
||||
AAA
|
||||
AAA
|
||||
AA
|
||||
AA
|
||||
|
||||
The idea is that we cut a circle away from the top avatar so that there
|
||||
is a bit of transparent area between two avatars:
|
||||
|
||||
AAAAAAA
|
||||
AAAAAAA
|
||||
AAAAA
|
||||
AAA B
|
||||
AAA BB
|
||||
AA BBB
|
||||
AA BBB
|
||||
|
||||
See CollidingAvatars.scss for how this clipPath is applied.
|
||||
*/}
|
||||
<svg width={0} height={0} className="CollidingAvatars__clip_svg">
|
||||
<clipPath id={clipId} clipPathUnits="objectBoundingBox">
|
||||
<path d="M0 0 h1 v0.4166 A0.54166 0.54166 0 0 0.4166 1 H0 Z" />
|
||||
</clipPath>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -23,6 +23,8 @@ import { ConfirmationDialog } from './ConfirmationDialog';
|
|||
import { FormattingWarningModal } from './FormattingWarningModal';
|
||||
import { SendEditWarningModal } from './SendEditWarningModal';
|
||||
import { SignalConnectionsModal } from './SignalConnectionsModal';
|
||||
import { AboutContactModal } from './conversation/AboutContactModal';
|
||||
import type { ExternalPropsType as AboutContactModalPropsType } from './conversation/AboutContactModal';
|
||||
import { WhatsNewModal } from './WhatsNewModal';
|
||||
|
||||
// NOTE: All types should be required for this component so that the smart
|
||||
|
@ -73,6 +75,9 @@ export type PropsType = {
|
|||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible: boolean;
|
||||
toggleSignalConnectionsModal: () => unknown;
|
||||
// AboutContactModal
|
||||
aboutContactModalProps: AboutContactModalPropsType | undefined;
|
||||
toggleAboutContactModal: () => unknown;
|
||||
// StickerPackPreviewModal
|
||||
stickerPackPreviewId: string | undefined;
|
||||
renderStickerPreviewModal: () => JSX.Element | null;
|
||||
|
@ -139,6 +144,9 @@ export function GlobalModalContainer({
|
|||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible,
|
||||
toggleSignalConnectionsModal,
|
||||
// AboutContactModal
|
||||
aboutContactModalProps,
|
||||
toggleAboutContactModal,
|
||||
// StickerPackPreviewModal
|
||||
stickerPackPreviewId,
|
||||
renderStickerPreviewModal,
|
||||
|
@ -185,10 +193,6 @@ export function GlobalModalContainer({
|
|||
return renderAddUserToAnotherGroup();
|
||||
}
|
||||
|
||||
if (contactModalState) {
|
||||
return renderContactModal();
|
||||
}
|
||||
|
||||
if (editHistoryMessages) {
|
||||
return renderEditHistoryMessagesModal();
|
||||
}
|
||||
|
@ -252,6 +256,20 @@ export function GlobalModalContainer({
|
|||
);
|
||||
}
|
||||
|
||||
if (aboutContactModalProps) {
|
||||
return (
|
||||
<AboutContactModal
|
||||
i18n={i18n}
|
||||
onClose={toggleAboutContactModal}
|
||||
{...aboutContactModalProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (contactModalState) {
|
||||
return renderContactModal();
|
||||
}
|
||||
|
||||
if (isStoriesSettingsVisible) {
|
||||
return renderStoriesSettings();
|
||||
}
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Intl } from './Intl';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
export type PropsType = {
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
}>;
|
||||
|
||||
export function SignalConnectionsModal({
|
||||
i18n,
|
||||
|
@ -48,12 +47,6 @@ export function SignalConnectionsModal({
|
|||
<div className="SignalConnectionsModal__description">
|
||||
{i18n('icu:SignalConnectionsModal__footer')}
|
||||
</div>
|
||||
|
||||
<div className="SignalConnectionsModal__button">
|
||||
<Button onClick={onClose} variant={ButtonVariant.Primary}>
|
||||
{i18n('icu:Confirmation--confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
60
ts/components/conversation/AboutContactModal.stories.tsx
Normal file
60
ts/components/conversation/AboutContactModal.stories.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { PropsType } from './AboutContactModal';
|
||||
import { AboutContactModal } from './AboutContactModal';
|
||||
import { type ComponentMeta } from '../../storybook/types';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const conversation = getDefaultConversation();
|
||||
const conversationWithAbout = getDefaultConversation({
|
||||
aboutText: '😀 About Me',
|
||||
});
|
||||
const systemContact = getDefaultConversation({
|
||||
systemGivenName: 'Alice',
|
||||
phoneNumber: '+1 555 123-4567',
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/AboutContactModal',
|
||||
component: AboutContactModal,
|
||||
argTypes: {
|
||||
isSignalConnection: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnections'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
conversation,
|
||||
isSignalConnection: false,
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
export function Defaults(args: PropsType): JSX.Element {
|
||||
return <AboutContactModal {...args} />;
|
||||
}
|
||||
|
||||
export function WithAbout(args: PropsType): JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={conversationWithAbout} />;
|
||||
}
|
||||
|
||||
export function SignalConnection(args: PropsType): JSX.Element {
|
||||
return <AboutContactModal {...args} isSignalConnection />;
|
||||
}
|
||||
|
||||
export function SystemContact(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={systemContact}
|
||||
isSignalConnection
|
||||
/>
|
||||
);
|
||||
}
|
135
ts/components/conversation/AboutContactModal.tsx
Normal file
135
ts/components/conversation/AboutContactModal.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { Modal } from '../Modal';
|
||||
import { UserText } from '../UserText';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { About } from './About';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
}> &
|
||||
ExternalPropsType;
|
||||
|
||||
export type ExternalPropsType = Readonly<{
|
||||
conversation: ConversationType;
|
||||
isSignalConnection: boolean;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
updateSharedGroups: (id: string) => void;
|
||||
}>;
|
||||
|
||||
export function AboutContactModal({
|
||||
i18n,
|
||||
conversation,
|
||||
isSignalConnection,
|
||||
toggleSignalConnectionsModal,
|
||||
updateSharedGroups,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
useEffect(() => {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateSharedGroups(conversation.id);
|
||||
}, [conversation.id, updateSharedGroups]);
|
||||
|
||||
const onSignalConnectionClick = useCallback(
|
||||
(ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
toggleSignalConnectionsModal();
|
||||
},
|
||||
[toggleSignalConnectionsModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
key="main"
|
||||
modalName="AboutContactModal"
|
||||
moduleClassName="AboutContactModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="AboutContactModal__row AboutContactModal__row--centered">
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarPath={conversation.avatarPath}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.TWO_HUNDRED_SIXTEEN}
|
||||
title={conversation.title}
|
||||
unblurredAvatarPath={conversation.unblurredAvatarPath}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<h3 className="AboutContactModal__title">
|
||||
{i18n('icu:AboutContactModal__title')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--profile" />
|
||||
<UserText text={conversation.title} />
|
||||
</div>
|
||||
|
||||
{conversation.about ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--about" />
|
||||
<About
|
||||
className="AboutContactModal__about"
|
||||
text={conversation.about}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isSignalConnection ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--connections" />
|
||||
<button
|
||||
type="button"
|
||||
className="AboutContactModal__signal-connection"
|
||||
onClick={onSignalConnectionClick}
|
||||
>
|
||||
{i18n('icu:AboutContactModal__signal-connection')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isInSystemContacts(conversation) ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--person" />
|
||||
{i18n('icu:AboutContactModal__system-contact', {
|
||||
name: conversation.firstName || conversation.title,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{conversation.phoneNumber ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--phone" />
|
||||
<UserText text={conversation.phoneNumber} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--group" />
|
||||
<div>
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -45,6 +45,7 @@ export default {
|
|||
removeMemberFromGroup: action('removeMemberFromGroup'),
|
||||
showConversation: action('showConversation'),
|
||||
theme: ThemeType.light,
|
||||
toggleAboutContactModal: action('AboutContactModal'),
|
||||
toggleAdmin: action('toggleAdmin'),
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
updateConversationModelSharedGroups: action(
|
||||
|
|
|
@ -43,6 +43,7 @@ type PropsActionType = {
|
|||
removeMemberFromGroup: (conversationId: string, contactId: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
toggleAdmin: (conversationId: string, contactId: string) => void;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
toggleAddUserToAnotherGroupModal: (conversationId: string) => void;
|
||||
updateConversationModelSharedGroups: (conversationId: string) => void;
|
||||
|
@ -77,6 +78,7 @@ export function ContactModal({
|
|||
removeMemberFromGroup,
|
||||
showConversation,
|
||||
theme,
|
||||
toggleAboutContactModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
toggleAdmin,
|
||||
toggleSafetyNumberModal,
|
||||
|
@ -208,9 +210,17 @@ export function ContactModal({
|
|||
title={contact.title}
|
||||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
/>
|
||||
<div className="ContactModal__name">
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__name"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(contact.id);
|
||||
}}
|
||||
>
|
||||
<UserText text={contact.title} />
|
||||
</div>
|
||||
<i className="ContactModal__name__chevron" />
|
||||
</button>
|
||||
<div className="module-about__container">
|
||||
<About text={contact.about} />
|
||||
</div>
|
||||
|
|
|
@ -30,6 +30,7 @@ const getCommonProps = () => ({
|
|||
i18n,
|
||||
onClose: action('onClose'),
|
||||
showContactModal: action('showContactModal'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnectionsModal'),
|
||||
removeMember: action('removeMember'),
|
||||
theme: ThemeType.light,
|
||||
});
|
||||
|
@ -39,13 +40,19 @@ export function DirectConversationsWithSameTitle(): JSX.Element {
|
|||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
type={ContactSpoofingType.DirectConversationWithSameTitle}
|
||||
possiblyUnsafeConversation={getDefaultConversation()}
|
||||
safeConversation={getDefaultConversation()}
|
||||
possiblyUnsafe={{
|
||||
conversation: getDefaultConversation(),
|
||||
isSignalConnection: false,
|
||||
}}
|
||||
safe={{
|
||||
conversation: getDefaultConversation(),
|
||||
isSignalConnection: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NotAdmin(): JSX.Element {
|
||||
export function NotAdminMany(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
|
@ -57,12 +64,15 @@ export function NotAdmin(): JSX.Element {
|
|||
collisionInfoByTitle={{
|
||||
Alice: times(2, () => ({
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
})),
|
||||
Bob: times(3, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Bob' }),
|
||||
})),
|
||||
Charlie: times(5, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Charlie' }),
|
||||
})),
|
||||
}}
|
||||
|
@ -70,7 +80,34 @@ export function NotAdmin(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function Admin(): JSX.Element {
|
||||
export function NotAdminOne(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
||||
group={{
|
||||
...getDefaultConversation(),
|
||||
areWeAdmin: false,
|
||||
}}
|
||||
collisionInfoByTitle={{
|
||||
Alice: [
|
||||
{
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
{
|
||||
oldName: 'Alice',
|
||||
isSignalConnection: true,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminMany(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
|
@ -82,15 +119,44 @@ export function Admin(): JSX.Element {
|
|||
collisionInfoByTitle={{
|
||||
Alice: times(2, () => ({
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
})),
|
||||
Bob: times(3, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Bob' }),
|
||||
})),
|
||||
Charlie: times(5, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Charlie' }),
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminOne(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
||||
group={{
|
||||
...getDefaultConversation(),
|
||||
areWeAdmin: true,
|
||||
}}
|
||||
collisionInfoByTitle={{
|
||||
Alice: [
|
||||
{
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
{
|
||||
isSignalConnection: true,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,11 +17,35 @@ import { Modal } from '../Modal';
|
|||
import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog';
|
||||
import { ContactSpoofingReviewDialogPerson } from './ContactSpoofingReviewDialogPerson';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { Intl } from '../Intl';
|
||||
import { assertDev } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
import { UserText } from '../UserText';
|
||||
|
||||
export type ReviewPropsType = Readonly<
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
possiblyUnsafe: {
|
||||
conversation: ConversationType;
|
||||
isSignalConnection: boolean;
|
||||
};
|
||||
safe: {
|
||||
conversation: ConversationType;
|
||||
isSignalConnection: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
group: ConversationType;
|
||||
collisionInfoByTitle: Record<
|
||||
string,
|
||||
Array<{
|
||||
oldName?: string;
|
||||
isSignalConnection: boolean;
|
||||
conversation: ConversationType;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
|
@ -29,6 +53,7 @@ export type PropsType = {
|
|||
blockAndReportSpam: (conversationId: string) => unknown;
|
||||
blockConversation: (conversationId: string) => unknown;
|
||||
deleteConversation: (conversationId: string) => unknown;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
|
@ -38,24 +63,7 @@ export type PropsType = {
|
|||
memberConversationId: string
|
||||
) => unknown;
|
||||
theme: ThemeType;
|
||||
} & (
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
group: ConversationType;
|
||||
collisionInfoByTitle: Record<
|
||||
string,
|
||||
Array<{
|
||||
oldName?: string;
|
||||
conversation: ConversationType;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
);
|
||||
} & ReviewPropsType;
|
||||
|
||||
enum ConfirmationStateType {
|
||||
ConfirmingDelete,
|
||||
|
@ -70,6 +78,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
blockConversation,
|
||||
conversationId,
|
||||
deleteConversation,
|
||||
toggleSignalConnectionsModal,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
onClose,
|
||||
|
@ -169,13 +178,13 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
|
||||
switch (props.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle: {
|
||||
const { possiblyUnsafeConversation, safeConversation } = props;
|
||||
const { possiblyUnsafe, safe } = props;
|
||||
assertDev(
|
||||
possiblyUnsafeConversation.type === 'direct',
|
||||
possiblyUnsafe.conversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialog> expected a direct conversation for the "possibly unsafe" conversation'
|
||||
);
|
||||
assertDev(
|
||||
safeConversation.type === 'direct',
|
||||
safe.conversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialog> expected a direct conversation for the "safe" conversation'
|
||||
);
|
||||
|
||||
|
@ -187,10 +196,13 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
{i18n('icu:ContactSpoofingReviewDialog__possibly-unsafe-title')}
|
||||
</h2>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
conversation={possiblyUnsafeConversation}
|
||||
conversation={possiblyUnsafe.conversation}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
isSignalConnection={possiblyUnsafe.isSignalConnection}
|
||||
oldName={undefined}
|
||||
>
|
||||
<div className="module-ContactSpoofingReviewDialog__buttons">
|
||||
<Button
|
||||
|
@ -198,7 +210,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingDelete,
|
||||
affectedConversation: possiblyUnsafeConversation,
|
||||
affectedConversation: possiblyUnsafe.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -209,7 +221,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingBlock,
|
||||
affectedConversation: possiblyUnsafeConversation,
|
||||
affectedConversation: possiblyUnsafe.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -220,13 +232,16 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
<hr />
|
||||
<h2>{i18n('icu:ContactSpoofingReviewDialog__safe-title')}</h2>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
conversation={safeConversation}
|
||||
conversation={safe.conversation}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
showContactModal(safeConversation.id);
|
||||
showContactModal(safe.conversation.id);
|
||||
}}
|
||||
theme={theme}
|
||||
isSignalConnection={safe.isSignalConnection}
|
||||
oldName={undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -257,117 +272,83 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
})}
|
||||
</p>
|
||||
|
||||
{Object.values(collisionInfoByTitle).map(
|
||||
(conversationInfos, titleIdx) => {
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
{i18n(
|
||||
'icu:ContactSpoofingReviewDialog__group__members-header'
|
||||
)}
|
||||
</h2>
|
||||
{conversationInfos.map(
|
||||
(conversationInfo, conversationIdx) => {
|
||||
let button: ReactNode;
|
||||
if (group.areWeAdmin) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingGroupRemoval,
|
||||
affectedConversation:
|
||||
conversationInfo.conversation,
|
||||
group,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:RemoveGroupMemberConfirmation__remove-button'
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
} else if (conversationInfo.conversation.isBlocked) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
acceptConversation(
|
||||
conversationInfo.conversation.id
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
);
|
||||
} else if (
|
||||
!isInSystemContacts(conversationInfo.conversation)
|
||||
) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingBlock,
|
||||
affectedConversation:
|
||||
conversationInfo.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--block')}
|
||||
</Button>
|
||||
);
|
||||
{Object.values(collisionInfoByTitle)
|
||||
.map((conversationInfos, titleIdx) =>
|
||||
conversationInfos.map((conversationInfo, conversationIdx) => {
|
||||
let button: ReactNode;
|
||||
if (group.areWeAdmin) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingGroupRemoval,
|
||||
affectedConversation: conversationInfo.conversation,
|
||||
group,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:RemoveGroupMemberConfirmation__remove-button')}
|
||||
</Button>
|
||||
);
|
||||
} else if (conversationInfo.conversation.isBlocked) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
acceptConversation(conversationInfo.conversation.id);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
);
|
||||
} else if (!isInSystemContacts(conversationInfo.conversation)) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingBlock,
|
||||
affectedConversation: conversationInfo.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--block')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const { oldName, isSignalConnection } = conversationInfo;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
key={conversationInfo.conversation.id}
|
||||
conversation={conversationInfo.conversation}
|
||||
toggleSignalConnectionsModal={
|
||||
toggleSignalConnectionsModal
|
||||
}
|
||||
|
||||
const { oldName } = conversationInfo;
|
||||
const newName =
|
||||
conversationInfo.conversation.profileName ||
|
||||
conversationInfo.conversation.title;
|
||||
|
||||
let callout: JSX.Element | undefined;
|
||||
if (oldName && oldName !== newName) {
|
||||
callout = (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property module-ContactSpoofingReviewDialogPerson__info__property--callout">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofingReviewDialog__group__name-change-info"
|
||||
components={{
|
||||
oldName: <UserText text={oldName} />,
|
||||
newName: <UserText text={newName} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
key={conversationInfo.conversation.id}
|
||||
conversation={conversationInfo.conversation}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
>
|
||||
{callout}
|
||||
{button && (
|
||||
<div className="module-ContactSpoofingReviewDialog__buttons">
|
||||
{button}
|
||||
</div>
|
||||
)}
|
||||
</ContactSpoofingReviewDialogPerson>
|
||||
{titleIdx < sharedTitles.length - 1 ||
|
||||
conversationIdx < conversationInfos.length - 1 ? (
|
||||
<hr />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
)}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
oldName={oldName}
|
||||
isSignalConnection={isSignalConnection}
|
||||
>
|
||||
{button && (
|
||||
<div className="module-ContactSpoofingReviewDialog__buttons">
|
||||
{button}
|
||||
</div>
|
||||
)}
|
||||
</ContactSpoofingReviewDialogPerson>
|
||||
{titleIdx < sharedTitles.length - 1 ||
|
||||
conversationIdx < conversationInfos.length - 1 ? (
|
||||
<hr />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
})
|
||||
)
|
||||
.flat()}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { ThemeType } from '../../types/Util';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
import type { PropsType } from './ContactSpoofingReviewDialogPerson';
|
||||
import { ContactSpoofingReviewDialogPerson } from './ContactSpoofingReviewDialogPerson';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
component: ContactSpoofingReviewDialogPerson,
|
||||
title: 'Components/Conversation/ContactSpoofingReviewDialogPerson',
|
||||
argTypes: {
|
||||
oldName: { control: { type: 'text' } },
|
||||
isSignalConnection: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
onClick: action('onClick'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnectionsModal'),
|
||||
getPreferredBadge: () => undefined,
|
||||
conversation: getDefaultConversation(),
|
||||
theme: ThemeType.light,
|
||||
oldName: undefined,
|
||||
isSignalConnection: false,
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = args => {
|
||||
return <ContactSpoofingReviewDialogPerson {...args} />;
|
||||
};
|
||||
|
||||
export const Normal = Template.bind({});
|
||||
|
||||
export const SignalConnection = Template.bind({});
|
||||
SignalConnection.args = {
|
||||
isSignalConnection: true,
|
||||
};
|
||||
|
||||
export const ProfileNameChanged = Template.bind({});
|
||||
ProfileNameChanged.args = {
|
||||
oldName: 'Imposter',
|
||||
};
|
||||
|
||||
export const WithSharedGroups = Template.bind({});
|
||||
WithSharedGroups.args = {
|
||||
conversation: getDefaultConversation({
|
||||
sharedGroupNames: ['A', 'B', 'C'],
|
||||
}),
|
||||
};
|
|
@ -12,15 +12,20 @@ import { assertDev } from '../../util/assert';
|
|||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { UserText } from '../UserText';
|
||||
import { Intl } from '../Intl';
|
||||
|
||||
type PropsType = {
|
||||
export type PropsType = Readonly<{
|
||||
children?: ReactNode;
|
||||
conversation: ConversationType;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
onClick?: () => void;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
oldName: string | undefined;
|
||||
isSignalConnection: boolean;
|
||||
}>;
|
||||
|
||||
export function ContactSpoofingReviewDialogPerson({
|
||||
children,
|
||||
|
@ -28,13 +33,44 @@ export function ContactSpoofingReviewDialogPerson({
|
|||
getPreferredBadge,
|
||||
i18n,
|
||||
onClick,
|
||||
toggleSignalConnectionsModal,
|
||||
theme,
|
||||
oldName,
|
||||
isSignalConnection,
|
||||
}: PropsType): JSX.Element {
|
||||
assertDev(
|
||||
conversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialogPerson> expected a direct conversation'
|
||||
);
|
||||
|
||||
const newName = conversation.profileName || conversation.title;
|
||||
|
||||
let callout: JSX.Element | undefined;
|
||||
if (oldName && oldName !== newName) {
|
||||
callout = (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--person" />
|
||||
<div>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofingReviewDialog__group__name-change-info"
|
||||
components={{
|
||||
oldName: <UserText text={oldName} />,
|
||||
newName: <UserText text={newName} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const name = (
|
||||
<ContactName
|
||||
module="module-ContactSpoofingReviewDialogPerson__info__contact-name"
|
||||
title={conversation.title}
|
||||
/>
|
||||
);
|
||||
|
||||
const contents = (
|
||||
<>
|
||||
<Avatar
|
||||
|
@ -45,40 +81,59 @@ export function ContactSpoofingReviewDialogPerson({
|
|||
className="module-ContactSpoofingReviewDialogPerson__avatar"
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info">
|
||||
<ContactName
|
||||
module="module-ContactSpoofingReviewDialogPerson__info__contact-name"
|
||||
title={conversation.title}
|
||||
/>
|
||||
{onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="module-ContactSpoofingReviewDialogPerson"
|
||||
onClick={onClick}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
) : (
|
||||
name
|
||||
)}
|
||||
{callout}
|
||||
{conversation.phoneNumber ? (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
{conversation.phoneNumber}
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--phone" />
|
||||
<div>{conversation.phoneNumber}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{isSignalConnection ? (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--connections" />
|
||||
<button
|
||||
type="button"
|
||||
className="module-ContactSpoofingReviewDialogPerson__info__property__signal-connection"
|
||||
onClick={toggleSignalConnectionsModal}
|
||||
>
|
||||
{i18n('icu:ContactSpoofingReviewDialog__signal-connection')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--group" />
|
||||
<div>
|
||||
{conversation.sharedGroupNames?.length ? (
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
) : (
|
||||
i18n(
|
||||
'icu:ContactSpoofingReviewDialog__group__members__no-shared-groups'
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-ContactSpoofingReviewDialogPerson"
|
||||
onClick={onClick}
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson">{contents}</div>
|
||||
);
|
||||
|
|
|
@ -26,6 +26,7 @@ export default {
|
|||
unblurAvatar: action('unblurAvatar'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
viewUserStories: action('viewUserStories'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
|
@ -78,7 +79,7 @@ export const DirectNoGroupsJustPhoneNumber = Template.bind({});
|
|||
DirectNoGroupsJustPhoneNumber.args = {
|
||||
phoneNumber: casual.phone,
|
||||
profileName: '',
|
||||
title: '',
|
||||
title: casual.phone,
|
||||
};
|
||||
|
||||
export const DirectNoGroupsNoData = Template.bind({});
|
||||
|
@ -86,7 +87,7 @@ DirectNoGroupsNoData.args = {
|
|||
avatarPath: undefined,
|
||||
phoneNumber: '',
|
||||
profileName: '',
|
||||
title: '',
|
||||
title: casual.phone,
|
||||
};
|
||||
|
||||
export const DirectNoGroupsNoDataNotAccepted = Template.bind({});
|
||||
|
|
|
@ -13,7 +13,6 @@ import type { HasStories } from '../../types/Stories';
|
|||
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
|
||||
import { StoryViewModeType } from '../../types/Stories';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
|
||||
|
@ -27,7 +26,6 @@ export type Props = {
|
|||
isMe: boolean;
|
||||
isSignalConversation?: boolean;
|
||||
membersCount?: number;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
sharedGroupNames?: ReadonlyArray<string>;
|
||||
unblurAvatar: (conversationId: string) => void;
|
||||
|
@ -35,6 +33,7 @@ export type Props = {
|
|||
updateSharedGroups: (conversationId: string) => unknown;
|
||||
theme: ThemeType;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
const renderMembershipRow = ({
|
||||
|
@ -56,22 +55,25 @@ const renderMembershipRow = ({
|
|||
Required<Pick<Props, 'sharedGroupNames'>> & {
|
||||
onClickMessageRequestWarning: () => void;
|
||||
}) => {
|
||||
const className = 'module-conversation-hero__membership';
|
||||
|
||||
if (conversationType !== 'direct') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMe) {
|
||||
return <div className={className}>{i18n('icu:noteToSelfHero')}</div>;
|
||||
return (
|
||||
<div className="module-conversation-hero__note-to-self">
|
||||
{i18n('icu:noteToSelfHero')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sharedGroupNames.length > 0) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="module-conversation-hero__membership">
|
||||
<i className="module-conversation-hero__membership__chevron" />
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
nameClassName={`${className}__name`}
|
||||
nameClassName="module-conversation-hero__membership__name"
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
/>
|
||||
</div>
|
||||
|
@ -81,21 +83,30 @@ const renderMembershipRow = ({
|
|||
if (phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return <div className={className}>{i18n('icu:no-groups-in-common')}</div>;
|
||||
return (
|
||||
<div className="module-conversation-hero__membership">
|
||||
{i18n('icu:no-groups-in-common')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-conversation-hero__message-request-warning">
|
||||
<div className="module-conversation-hero__message-request-warning__message">
|
||||
{i18n('icu:no-groups-in-common-warning')}
|
||||
<div className="module-conversation-hero__membership">
|
||||
<div className="module-conversation-hero__membership__warning">
|
||||
<i className="module-conversation-hero__membership__warning__icon" />
|
||||
<span>{i18n('icu:no-groups-in-common-warning')}</span>
|
||||
|
||||
<button
|
||||
className="module-conversation-hero__membership__warning__learn-more"
|
||||
type="button"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
onClickMessageRequestWarning();
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequestWarning__learn-more')}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onClickMessageRequestWarning}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
>
|
||||
{i18n('icu:MessageRequestWarning__learn-more')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -115,7 +126,6 @@ export function ConversationHero({
|
|||
isSignalConversation,
|
||||
membersCount,
|
||||
sharedGroupNames = [],
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
theme,
|
||||
|
@ -124,6 +134,7 @@ export function ConversationHero({
|
|||
unblurredAvatarPath,
|
||||
updateSharedGroups,
|
||||
viewUserStories,
|
||||
toggleAboutContactModal,
|
||||
}: Props): JSX.Element {
|
||||
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
||||
useState(false);
|
||||
|
@ -158,9 +169,29 @@ export function ConversationHero({
|
|||
};
|
||||
}
|
||||
|
||||
const phoneNumberOnly = Boolean(
|
||||
!name && !profileName && conversationType === 'direct'
|
||||
);
|
||||
let titleElem: JSX.Element | undefined;
|
||||
|
||||
if (isMe) {
|
||||
titleElem = <>{i18n('icu:noteToSelf')}</>;
|
||||
} else if (isSignalConversation || conversationType !== 'direct') {
|
||||
titleElem = (
|
||||
<ContactName isSignalConversation={isSignalConversation} title={title} />
|
||||
);
|
||||
} else if (title) {
|
||||
titleElem = (
|
||||
<button
|
||||
type="button"
|
||||
className="module-conversation-hero__title"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(id);
|
||||
}}
|
||||
>
|
||||
<ContactName title={title} />
|
||||
<i className="module-conversation-hero__title__chevron" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
return (
|
||||
|
@ -187,14 +218,7 @@ export function ConversationHero({
|
|||
title={title}
|
||||
/>
|
||||
<h1 className="module-conversation-hero__profile-name">
|
||||
{isMe ? (
|
||||
i18n('icu:noteToSelf')
|
||||
) : (
|
||||
<ContactName
|
||||
isSignalConversation={isSignalConversation}
|
||||
title={title}
|
||||
/>
|
||||
)}
|
||||
{titleElem}
|
||||
{isMe && <span className="ContactModal__official-badge__large" />}
|
||||
</h1>
|
||||
{about && !isMe && (
|
||||
|
@ -212,9 +236,7 @@ export function ConversationHero({
|
|||
/>
|
||||
) : membersCount != null ? (
|
||||
i18n('icu:ConversationHero--members', { count: membersCount })
|
||||
) : phoneNumberOnly ? null : (
|
||||
phoneNumber
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{!isSignalConversation &&
|
||||
|
|
|
@ -13,10 +13,8 @@ import type { PropsType } from './Timeline';
|
|||
import { Timeline } from './Timeline';
|
||||
import type { TimelineItemType } from './TimelineItem';
|
||||
import { TimelineItem } from './TimelineItem';
|
||||
import { ContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
||||
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
|
||||
import { ConversationHero } from './ConversationHero';
|
||||
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from '../../state/smart/ContactSpoofingReviewDialog';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { TypingBubble } from './TypingBubble';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
|
@ -26,9 +24,13 @@ import { ThemeType } from '../../types/Util';
|
|||
import { TextDirection } from './Message';
|
||||
import { PaymentEventKind } from '../../types/Payment';
|
||||
import type { PropsData as TimelineMessageProps } from './TimelineMessage';
|
||||
import { CollidingAvatars } from '../CollidingAvatars';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const alice = getDefaultConversation();
|
||||
const bob = getDefaultConversation();
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/Timeline',
|
||||
argTypes: {},
|
||||
|
@ -323,10 +325,7 @@ const actions = () => ({
|
|||
returnToActiveCall: action('returnToActiveCall'),
|
||||
|
||||
closeContactSpoofingReview: action('closeContactSpoofingReview'),
|
||||
reviewGroupMemberNameCollision: action('reviewGroupMemberNameCollision'),
|
||||
reviewMessageRequestNameCollision: action(
|
||||
'reviewMessageRequestNameCollision'
|
||||
),
|
||||
reviewConversationNameCollision: action('reviewConversationNameCollision'),
|
||||
|
||||
unblurAvatar: action('unblurAvatar'),
|
||||
|
||||
|
@ -375,35 +374,9 @@ const renderItem = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const renderContactSpoofingReviewDialog = (
|
||||
props: SmartContactSpoofingReviewDialogPropsType
|
||||
) => {
|
||||
const sharedProps = {
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
removeMember: action('removeMember'),
|
||||
showContactModal: action('showContactModal'),
|
||||
theme: ThemeType.dark,
|
||||
};
|
||||
|
||||
if (props.type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...props}
|
||||
{...sharedProps}
|
||||
group={{
|
||||
...getDefaultConversation(),
|
||||
areWeAdmin: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ContactSpoofingReviewDialog {...props} {...sharedProps} />;
|
||||
const renderContactSpoofingReviewDialog = () => {
|
||||
// hasContactSpoofingReview is always false in stories
|
||||
return <div />;
|
||||
};
|
||||
|
||||
const getAbout = () => '👍 Free to chat';
|
||||
|
@ -433,6 +406,7 @@ const renderHeroRow = () => {
|
|||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={noop}
|
||||
viewUserStories={action('viewUserStories')}
|
||||
toggleAboutContactModal={action('toggleAboutContactModal')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -452,6 +426,9 @@ const renderTypingBubble = () => (
|
|||
theme={ThemeType.light}
|
||||
/>
|
||||
);
|
||||
const renderCollidingAvatars = () => (
|
||||
<CollidingAvatars i18n={i18n} conversations={[alice, bob]} />
|
||||
);
|
||||
const renderMiniPlayer = () => (
|
||||
<div>If active, this is where smart mini player would be</div>
|
||||
);
|
||||
|
@ -477,12 +454,14 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
invitedContactsForNewlyCreatedGroup:
|
||||
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
||||
warning: overrideProps.warning,
|
||||
hasContactSpoofingReview: false,
|
||||
|
||||
id: uuid(),
|
||||
renderItem,
|
||||
renderHeroRow,
|
||||
renderMiniPlayer,
|
||||
renderTypingBubble,
|
||||
renderCollidingAvatars,
|
||||
renderContactSpoofingReviewDialog,
|
||||
isSomeoneTyping: overrideProps.isSomeoneTyping || false,
|
||||
|
||||
|
@ -581,7 +560,9 @@ export function WithSameNameInDirectConversationWarning(): JSX.Element {
|
|||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
safeConversation: getDefaultConversation(),
|
||||
|
||||
// Just to pacify type-script
|
||||
safeConversationId: '123',
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
@ -590,6 +571,21 @@ export function WithSameNameInDirectConversationWarning(): JSX.Element {
|
|||
}
|
||||
|
||||
export function WithSameNameInGroupConversationWarning(): JSX.Element {
|
||||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions: {},
|
||||
groupNameCollisions: {
|
||||
Alice: times(2, () => uuid()),
|
||||
},
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
||||
return <Timeline {...props} />;
|
||||
}
|
||||
|
||||
export function WithSameNamesInGroupConversationWarning(): JSX.Element {
|
||||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
|
|
|
@ -58,7 +58,7 @@ const LOAD_NEWER_THRESHOLD = 5;
|
|||
export type WarningType = ReadonlyDeep<
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
safeConversation: ConversationType;
|
||||
safeConversationId: string;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
|
@ -67,23 +67,6 @@ export type WarningType = ReadonlyDeep<
|
|||
}
|
||||
>;
|
||||
|
||||
export type ContactSpoofingReviewPropType =
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
collisionInfoByTitle: Record<
|
||||
string,
|
||||
Array<{
|
||||
oldName?: string;
|
||||
conversation: ConversationType;
|
||||
}>
|
||||
>;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
haveNewest: boolean;
|
||||
haveOldest: boolean;
|
||||
|
@ -112,7 +95,7 @@ type PropsHousekeepingType = {
|
|||
shouldShowMiniPlayer: boolean;
|
||||
|
||||
warning?: WarningType;
|
||||
contactSpoofingReview?: ContactSpoofingReviewPropType;
|
||||
hasContactSpoofingReview: boolean | undefined;
|
||||
|
||||
discardMessages: (
|
||||
_: Readonly<
|
||||
|
@ -128,6 +111,9 @@ type PropsHousekeepingType = {
|
|||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
|
||||
renderCollidingAvatars: (_: {
|
||||
conversationIds: ReadonlyArray<string>;
|
||||
}) => JSX.Element;
|
||||
renderContactSpoofingReviewDialog: (
|
||||
props: SmartContactSpoofingReviewDialogPropsType
|
||||
) => JSX.Element;
|
||||
|
@ -167,12 +153,7 @@ export type PropsActionsType = {
|
|||
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
|
||||
peekGroupCallForTheFirstTime: (conversationId: string) => unknown;
|
||||
peekGroupCallIfItHasMembers: (conversationId: string) => unknown;
|
||||
reviewGroupMemberNameCollision: (groupConversationId: string) => void;
|
||||
reviewMessageRequestNameCollision: (
|
||||
_: Readonly<{
|
||||
safeConversationId: string;
|
||||
}>
|
||||
) => void;
|
||||
reviewConversationNameCollision: () => void;
|
||||
scrollToOldestUnreadMention: (conversationId: string) => unknown;
|
||||
};
|
||||
|
||||
|
@ -798,7 +779,7 @@ export class Timeline extends React.Component<
|
|||
acknowledgeGroupMemberNameCollisions,
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup,
|
||||
closeContactSpoofingReview,
|
||||
contactSpoofingReview,
|
||||
hasContactSpoofingReview,
|
||||
getPreferredBadge,
|
||||
getTimestampForMessage,
|
||||
haveNewest,
|
||||
|
@ -811,13 +792,13 @@ export class Timeline extends React.Component<
|
|||
items,
|
||||
messageLoadingState,
|
||||
oldestUnseenIndex,
|
||||
renderCollidingAvatars,
|
||||
renderContactSpoofingReviewDialog,
|
||||
renderHeroRow,
|
||||
renderItem,
|
||||
renderMiniPlayer,
|
||||
renderTypingBubble,
|
||||
reviewGroupMemberNameCollision,
|
||||
reviewMessageRequestNameCollision,
|
||||
reviewConversationNameCollision,
|
||||
scrollToOldestUnreadMention,
|
||||
shouldShowMiniPlayer,
|
||||
theme,
|
||||
|
@ -963,8 +944,14 @@ export class Timeline extends React.Component<
|
|||
let headerElements: ReactNode;
|
||||
if (warning || shouldShowMiniPlayer) {
|
||||
let text: ReactChild | undefined;
|
||||
let icon: ReactChild | undefined;
|
||||
let onClose: () => void;
|
||||
if (warning) {
|
||||
icon = (
|
||||
<TimelineWarning.IconContainer>
|
||||
<TimelineWarning.GenericIcon />
|
||||
</TimelineWarning.IconContainer>
|
||||
);
|
||||
switch (warning.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle:
|
||||
text = (
|
||||
|
@ -976,11 +963,7 @@ export class Timeline extends React.Component<
|
|||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
reviewRequestLink: parts => (
|
||||
<TimelineWarning.Link
|
||||
onClick={() => {
|
||||
reviewMessageRequestNameCollision({
|
||||
safeConversationId: warning.safeConversation.id,
|
||||
});
|
||||
}}
|
||||
onClick={reviewConversationNameCollision}
|
||||
>
|
||||
{parts}
|
||||
</TimelineWarning.Link>
|
||||
|
@ -998,24 +981,25 @@ export class Timeline extends React.Component<
|
|||
const { groupNameCollisions } = warning;
|
||||
const numberOfSharedNames = Object.keys(groupNameCollisions).length;
|
||||
const reviewRequestLink: FullJSXType = parts => (
|
||||
<TimelineWarning.Link
|
||||
onClick={() => {
|
||||
reviewGroupMemberNameCollision(id);
|
||||
}}
|
||||
>
|
||||
<TimelineWarning.Link onClick={reviewConversationNameCollision}>
|
||||
{parts}
|
||||
</TimelineWarning.Link>
|
||||
);
|
||||
if (numberOfSharedNames === 1) {
|
||||
const [conversationIds] = [...Object.values(groupNameCollisions)];
|
||||
if (conversationIds.length >= 2) {
|
||||
icon = (
|
||||
<TimelineWarning.CustomInfo>
|
||||
{renderCollidingAvatars({ conversationIds })}
|
||||
</TimelineWarning.CustomInfo>
|
||||
);
|
||||
}
|
||||
text = (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-name-in-group--link"
|
||||
components={{
|
||||
count: Object.values(groupNameCollisions).reduce(
|
||||
(result, conversations) => result + conversations.length,
|
||||
0
|
||||
),
|
||||
count: conversationIds.length,
|
||||
reviewRequestLink,
|
||||
}}
|
||||
/>
|
||||
|
@ -1053,9 +1037,7 @@ export class Timeline extends React.Component<
|
|||
{renderMiniPlayer({ shouldFlow: true })}
|
||||
{text && (
|
||||
<TimelineWarning i18n={i18n} onClose={onClose}>
|
||||
<TimelineWarning.IconContainer>
|
||||
<TimelineWarning.GenericIcon />
|
||||
</TimelineWarning.IconContainer>
|
||||
{icon}
|
||||
<TimelineWarning.Text>{text}</TimelineWarning.Text>
|
||||
</TimelineWarning>
|
||||
)}
|
||||
|
@ -1066,33 +1048,11 @@ export class Timeline extends React.Component<
|
|||
}
|
||||
|
||||
let contactSpoofingReviewDialog: ReactNode;
|
||||
if (contactSpoofingReview) {
|
||||
const commonProps = {
|
||||
if (hasContactSpoofingReview) {
|
||||
contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
|
||||
conversationId: id,
|
||||
onClose: closeContactSpoofingReview,
|
||||
};
|
||||
|
||||
switch (contactSpoofingReview.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle:
|
||||
contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
|
||||
...commonProps,
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
possiblyUnsafeConversation:
|
||||
contactSpoofingReview.possiblyUnsafeConversation,
|
||||
safeConversation: contactSpoofingReview.safeConversation,
|
||||
});
|
||||
break;
|
||||
case ContactSpoofingType.MultipleGroupMembersWithSameTitle:
|
||||
contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
|
||||
...commonProps,
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
groupConversationId: id,
|
||||
collisionInfoByTitle: contactSpoofingReview.collisionInfoByTitle,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(contactSpoofingReview);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -71,3 +71,11 @@ function Link({ children, onClick }: Readonly<LinkProps>): JSX.Element {
|
|||
}
|
||||
|
||||
TimelineWarning.Link = Link;
|
||||
|
||||
function CustomInfo({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): JSX.Element {
|
||||
return <div className="module-TimelineWarning__custom_info">{children}</div>;
|
||||
}
|
||||
|
||||
TimelineWarning.CustomInfo = CustomInfo;
|
||||
|
|
|
@ -102,6 +102,7 @@ const createProps = (
|
|||
setMuteExpiration: action('setMuteExpiration'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
|
|
|
@ -150,6 +150,7 @@ type ActionProps = {
|
|||
setMuteExpiration: (id: string, muteExpiresAt: undefined | number) => unknown;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
updateGroupAttributes: (
|
||||
|
@ -223,6 +224,7 @@ export function ConversationDetails({
|
|||
showConversation,
|
||||
showLightboxWithMedia,
|
||||
theme,
|
||||
toggleAboutContactModal,
|
||||
toggleSafetyNumberModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
updateGroupAttributes,
|
||||
|
@ -398,6 +400,7 @@ export function ConversationDetails({
|
|||
);
|
||||
}}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={toggleAboutContactModal}
|
||||
/>
|
||||
|
||||
<div className="ConversationDetails__header-buttons">
|
||||
|
|
|
@ -45,6 +45,7 @@ function Wrapper(overrideProps: Partial<Props>) {
|
|||
isGroup
|
||||
isMe={false}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={action('toggleAboutContactModal')}
|
||||
{...overrideProps}
|
||||
/>
|
||||
);
|
||||
|
@ -80,7 +81,16 @@ export function EditableNoDescription(): JSX.Element {
|
|||
}
|
||||
|
||||
export function OneOnOne(): JSX.Element {
|
||||
return <Wrapper isGroup={false} badges={getFakeBadges(3)} />;
|
||||
return (
|
||||
<Wrapper
|
||||
isGroup={false}
|
||||
badges={getFakeBadges(3)}
|
||||
conversation={getDefaultConversation({
|
||||
title: 'Maya Johnson',
|
||||
type: 'direct',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteToSelf(): JSX.Element {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { GroupDescription } from '../GroupDescription';
|
|||
import { About } from '../About';
|
||||
import type { GroupV2Membership } from './ConversationDetailsMembershipList';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import { bemGenerator } from './util';
|
||||
import { assertDev } from '../../../util/assert';
|
||||
import { BadgeDialog } from '../../BadgeDialog';
|
||||
import type { BadgeType } from '../../../badges/types';
|
||||
import { UserText } from '../../UserText';
|
||||
|
@ -26,6 +26,7 @@ export type Props = {
|
|||
isMe: boolean;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
startEditing: (isGroupTitle: boolean) => void;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
|
@ -34,8 +35,6 @@ enum ConversationDetailsHeaderActiveModal {
|
|||
ShowingBadges,
|
||||
}
|
||||
|
||||
const bem = bemGenerator('ConversationDetails-header');
|
||||
|
||||
export function ConversationDetailsHeader({
|
||||
areWeASubscriber,
|
||||
badges,
|
||||
|
@ -46,6 +45,7 @@ export function ConversationDetailsHeader({
|
|||
isMe,
|
||||
memberships,
|
||||
startEditing,
|
||||
toggleAboutContactModal,
|
||||
theme,
|
||||
}: Props): JSX.Element {
|
||||
const [activeModal, setActiveModal] = useState<
|
||||
|
@ -75,10 +75,10 @@ export function ConversationDetailsHeader({
|
|||
} else if (!isMe) {
|
||||
subtitle = (
|
||||
<>
|
||||
<div className={bem('subtitle__about')}>
|
||||
<div className="ConversationDetailsHeader__subtitle__about">
|
||||
<About text={conversation.about} />
|
||||
</div>
|
||||
<div className={bem('subtitle__phone-number')}>
|
||||
<div className="ConversationDetailsHeader__subtitle__phone-number">
|
||||
{conversation.phoneNumber}
|
||||
</div>
|
||||
</>
|
||||
|
@ -105,15 +105,6 @@ export function ConversationDetailsHeader({
|
|||
/>
|
||||
);
|
||||
|
||||
const contents = (
|
||||
<div>
|
||||
<div className={bem('title')}>
|
||||
{isMe ? i18n('icu:noteToSelf') : <UserText text={conversation.title} />}
|
||||
{isMe && <span className="ContactModal__official-badge__large" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let modal: ReactNode;
|
||||
switch (activeModal) {
|
||||
case ConversationDetailsHeaderActiveModal.ShowingAvatar:
|
||||
|
@ -150,8 +141,13 @@ export function ConversationDetailsHeader({
|
|||
}
|
||||
|
||||
if (canEdit) {
|
||||
assertDev(isGroup, 'Only groups support editable title');
|
||||
|
||||
return (
|
||||
<div className={bem('root')} data-testid="ConversationDetailsHeader">
|
||||
<div
|
||||
className="ConversationDetailsHeader"
|
||||
data-testid="ConversationDetailsHeader"
|
||||
>
|
||||
{modal}
|
||||
{avatar}
|
||||
<button
|
||||
|
@ -161,12 +157,14 @@ export function ConversationDetailsHeader({
|
|||
ev.stopPropagation();
|
||||
startEditing(true);
|
||||
}}
|
||||
className={bem('root', 'editable')}
|
||||
className="ConversationDetailsHeader__edit-button"
|
||||
>
|
||||
{contents}
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
<UserText text={conversation.title} />
|
||||
</div>
|
||||
</button>
|
||||
{hasNestedButton ? (
|
||||
<div className={bem('subtitle')}>{subtitle}</div>
|
||||
<div className="ConversationDetailsHeader__subtitle">{subtitle}</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -179,21 +177,61 @@ export function ConversationDetailsHeader({
|
|||
ev.stopPropagation();
|
||||
startEditing(false);
|
||||
}}
|
||||
className={bem('root', 'editable')}
|
||||
className="ConversationDetailsHeader__edit-button"
|
||||
>
|
||||
<div className={bem('subtitle')}>{subtitle}</div>
|
||||
<div className="ConversationDetailsHeader__subtitle">
|
||||
{subtitle}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let title: JSX.Element;
|
||||
|
||||
if (isMe) {
|
||||
title = (
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
{i18n('icu:noteToSelf')}
|
||||
<span className="ContactModal__official-badge__large" />
|
||||
</div>
|
||||
);
|
||||
} else if (isGroup) {
|
||||
title = (
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
<UserText text={conversation.title} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
title = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleAboutContactModal(conversation.id);
|
||||
}}
|
||||
className="ConversationDetailsHeader__about-button"
|
||||
>
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
<UserText text={conversation.title} />
|
||||
|
||||
<span className="ConversationDetailsHeader__about-icon" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={bem('root')} data-testid="ConversationDetailsHeader">
|
||||
<div
|
||||
className="ConversationDetailsHeader"
|
||||
data-testid="ConversationDetailsHeader"
|
||||
>
|
||||
{modal}
|
||||
{avatar}
|
||||
{contents}
|
||||
<div className={bem('subtitle')}>{subtitle}</div>
|
||||
{title}
|
||||
<div className="ConversationDetailsHeader__subtitle">{subtitle}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -381,6 +381,7 @@ export type ConversationAttributesType = {
|
|||
profileKey?: string;
|
||||
profileName?: string;
|
||||
verified?: number;
|
||||
profileLastUpdatedAt?: number;
|
||||
profileLastFetchedAt?: number;
|
||||
pendingUniversalTimer?: string;
|
||||
pendingRemovedContactNotification?: string;
|
||||
|
|
|
@ -3185,6 +3185,8 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
const serviceId = this.getServiceId();
|
||||
if (isDirectConversation(this.attributes) && serviceId) {
|
||||
this.set({ profileLastUpdatedAt: Date.now() });
|
||||
|
||||
void window.ConversationController.getAllGroupsInvolvingServiceId(
|
||||
serviceId
|
||||
).then(groups => {
|
||||
|
|
|
@ -83,7 +83,6 @@ import {
|
|||
import { isMessageUnread } from '../../util/isMessageUnread';
|
||||
import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelectedContactForGroupAddition';
|
||||
import type { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
import { writeProfile } from '../../services/writeProfile';
|
||||
import {
|
||||
getConversationServiceIdsStoppingSend,
|
||||
|
@ -237,6 +236,7 @@ export type ConversationType = ReadonlyDeep<
|
|||
familyName?: string;
|
||||
firstName?: string;
|
||||
profileName?: string;
|
||||
profileLastUpdatedAt?: number;
|
||||
username?: string;
|
||||
about?: string;
|
||||
aboutText?: string;
|
||||
|
@ -464,17 +464,6 @@ type ComposerStateType = ReadonlyDeep<
|
|||
))
|
||||
>;
|
||||
|
||||
type ContactSpoofingReviewStateType = ReadonlyDeep<
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
safeConversationId: string;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
groupConversationId: string;
|
||||
}
|
||||
>;
|
||||
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME
|
||||
export type ConversationsStateType = Readonly<{
|
||||
preJoinConversation?: PreJoinConversationType;
|
||||
|
@ -502,7 +491,7 @@ export type ConversationsStateType = Readonly<{
|
|||
|
||||
showArchived: boolean;
|
||||
composer?: ComposerStateType;
|
||||
contactSpoofingReview?: ContactSpoofingReviewStateType;
|
||||
hasContactSpoofingReview: boolean;
|
||||
|
||||
/**
|
||||
* Each key is a conversation ID. Each value is a value representing the state of
|
||||
|
@ -850,17 +839,8 @@ export type TargetedConversationChangedActionType = ReadonlyDeep<{
|
|||
switchToAssociatedView?: boolean;
|
||||
};
|
||||
}>;
|
||||
type ReviewGroupMemberNameCollisionActionType = ReadonlyDeep<{
|
||||
type: 'REVIEW_GROUP_MEMBER_NAME_COLLISION';
|
||||
payload: {
|
||||
groupConversationId: string;
|
||||
};
|
||||
}>;
|
||||
type ReviewMessageRequestNameCollisionActionType = ReadonlyDeep<{
|
||||
type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION';
|
||||
payload: {
|
||||
safeConversationId: string;
|
||||
};
|
||||
type ReviewConversationNameCollisionActionType = ReadonlyDeep<{
|
||||
type: 'REVIEW_CONVERSATION_NAME_COLLISION';
|
||||
}>;
|
||||
type ShowInboxActionType = ReadonlyDeep<{
|
||||
type: 'SHOW_INBOX';
|
||||
|
@ -989,8 +969,7 @@ export type ConversationActionType =
|
|||
| RepairNewestMessageActionType
|
||||
| RepairOldestMessageActionType
|
||||
| ReplaceAvatarsActionType
|
||||
| ReviewGroupMemberNameCollisionActionType
|
||||
| ReviewMessageRequestNameCollisionActionType
|
||||
| ReviewConversationNameCollisionActionType
|
||||
| ScrollToMessageActionType
|
||||
| TargetedConversationChangedActionType
|
||||
| SetComposeGroupAvatarActionType
|
||||
|
@ -1092,8 +1071,7 @@ export const actions = {
|
|||
copyMessageText,
|
||||
retryDeleteForEveryone,
|
||||
retryMessageSend,
|
||||
reviewGroupMemberNameCollision,
|
||||
reviewMessageRequestNameCollision,
|
||||
reviewConversationNameCollision,
|
||||
revokePendingMembershipsFromGroupV2,
|
||||
saveAttachment,
|
||||
saveAttachmentFromMessage,
|
||||
|
@ -2885,23 +2863,12 @@ function repairOldestMessage(
|
|||
};
|
||||
}
|
||||
|
||||
function reviewGroupMemberNameCollision(
|
||||
groupConversationId: string
|
||||
): ReviewGroupMemberNameCollisionActionType {
|
||||
function reviewConversationNameCollision(): ReviewConversationNameCollisionActionType {
|
||||
return {
|
||||
type: 'REVIEW_GROUP_MEMBER_NAME_COLLISION',
|
||||
payload: { groupConversationId },
|
||||
type: 'REVIEW_CONVERSATION_NAME_COLLISION',
|
||||
};
|
||||
}
|
||||
|
||||
function reviewMessageRequestNameCollision(
|
||||
payload: Readonly<{
|
||||
safeConversationId: string;
|
||||
}>
|
||||
): ReviewMessageRequestNameCollisionActionType {
|
||||
return { type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION', payload };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
export type MessageResetOptionsType = {
|
||||
conversationId: string;
|
||||
|
@ -4208,6 +4175,7 @@ export function getEmptyState(): ConversationsStateType {
|
|||
lastSelectedMessage: undefined,
|
||||
selectedMessageIds: undefined,
|
||||
showArchived: false,
|
||||
hasContactSpoofingReview: false,
|
||||
targetedConversationPanels: {
|
||||
isAnimating: false,
|
||||
wasAnimated: false,
|
||||
|
@ -4591,7 +4559,10 @@ export function reducer(
|
|||
}
|
||||
|
||||
if (action.type === 'CLOSE_CONTACT_SPOOFING_REVIEW') {
|
||||
return omit(state, 'contactSpoofingReview');
|
||||
return {
|
||||
...state,
|
||||
hasContactSpoofingReview: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL') {
|
||||
|
@ -4713,6 +4684,7 @@ export function reducer(
|
|||
}
|
||||
|
||||
const keysToOmit: Array<keyof ConversationsStateType> = [];
|
||||
const keyValuesToAdd: { hasContactSpoofingReview?: false } = {};
|
||||
|
||||
if (selectedConversationId === id) {
|
||||
// Archived -> Inbox: we go back to the normal inbox view
|
||||
|
@ -4728,12 +4700,13 @@ export function reducer(
|
|||
}
|
||||
|
||||
if (!existing.isBlocked && data.isBlocked) {
|
||||
keysToOmit.push('contactSpoofingReview');
|
||||
keyValuesToAdd.hasContactSpoofingReview = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...omit(state, keysToOmit),
|
||||
...keyValuesToAdd,
|
||||
selectedConversationId,
|
||||
showArchived,
|
||||
conversationLookup: {
|
||||
|
@ -4775,7 +4748,8 @@ export function reducer(
|
|||
: undefined;
|
||||
|
||||
return {
|
||||
...omit(state, 'contactSpoofingReview'),
|
||||
...state,
|
||||
hasContactSpoofingReview: false,
|
||||
selectedConversationId,
|
||||
targetedConversationPanels: {
|
||||
isAnimating: false,
|
||||
|
@ -5494,23 +5468,10 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === 'REVIEW_GROUP_MEMBER_NAME_COLLISION') {
|
||||
if (action.type === 'REVIEW_CONVERSATION_NAME_COLLISION') {
|
||||
return {
|
||||
...state,
|
||||
contactSpoofingReview: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
...action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION') {
|
||||
return {
|
||||
...state,
|
||||
contactSpoofingReview: {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
...action.payload,
|
||||
},
|
||||
hasContactSpoofingReview: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5683,7 +5644,8 @@ export function reducer(
|
|||
}
|
||||
|
||||
const nextState = {
|
||||
...omit(state, 'contactSpoofingReview'),
|
||||
...state,
|
||||
hasContactSpoofingReview: false,
|
||||
selectedConversationId: conversationId,
|
||||
targetedMessage: messageId,
|
||||
targetedMessageSource: TargetedMessageSource.NavigateToMessage,
|
||||
|
|
|
@ -77,9 +77,13 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
|
|||
hasMigrated: boolean;
|
||||
invitedMemberIds: Array<string>;
|
||||
}>;
|
||||
export type AboutContactModalPropsType = ReadonlyDeep<{
|
||||
contactId: string;
|
||||
}>;
|
||||
|
||||
export type GlobalModalsStateType = ReadonlyDeep<{
|
||||
addUserToAnotherGroupModalContactId?: string;
|
||||
aboutContactModalProps?: AboutContactModalPropsType;
|
||||
authArtCreatorData?: AuthorizeArtCreatorDataType;
|
||||
contactModalState?: ContactModalStateType;
|
||||
deleteMessagesProps?: DeleteMessagesPropsType;
|
||||
|
@ -130,6 +134,7 @@ export const TOGGLE_PROFILE_EDITOR_ERROR =
|
|||
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
|
||||
const TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL =
|
||||
'globalModals/TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL';
|
||||
const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL';
|
||||
const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
|
||||
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
|
||||
export const SHOW_SEND_ANYWAY_DIALOG = 'globalModals/SHOW_SEND_ANYWAY_DIALOG';
|
||||
|
@ -230,6 +235,11 @@ type ToggleAddUserToAnotherGroupModalActionType = ReadonlyDeep<{
|
|||
payload: string | undefined;
|
||||
}>;
|
||||
|
||||
type ToggleAboutContactModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_ABOUT_MODAL;
|
||||
payload: AboutContactModalPropsType | undefined;
|
||||
}>;
|
||||
|
||||
type ToggleSignalConnectionsModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_SIGNAL_CONNECTIONS_MODAL;
|
||||
}>;
|
||||
|
@ -372,6 +382,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| ShowUserNotFoundModalActionType
|
||||
| ShowWhatsNewModalActionType
|
||||
| StartMigrationToGV2ActionType
|
||||
| ToggleAboutContactModalActionType
|
||||
| ToggleAddUserToAnotherGroupModalActionType
|
||||
| ToggleConfirmationModalActionType
|
||||
| ToggleDeleteMessagesModalActionType
|
||||
|
@ -411,6 +422,7 @@ export const actions = {
|
|||
showStoriesSettings,
|
||||
showUserNotFoundModal,
|
||||
showWhatsNewModal,
|
||||
toggleAboutContactModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
toggleConfirmationModal,
|
||||
toggleDeleteMessagesModal,
|
||||
|
@ -627,6 +639,15 @@ function toggleAddUserToAnotherGroupModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleAboutContactModal(
|
||||
contactId?: string
|
||||
): ToggleAboutContactModalActionType {
|
||||
return {
|
||||
type: TOGGLE_ABOUT_MODAL,
|
||||
payload: contactId ? { contactId } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleSignalConnectionsModal(): ToggleSignalConnectionsModalActionType {
|
||||
return {
|
||||
type: TOGGLE_SIGNAL_CONNECTIONS_MODAL,
|
||||
|
@ -891,6 +912,13 @@ export function reducer(
|
|||
state: Readonly<GlobalModalsStateType> = getEmptyState(),
|
||||
action: Readonly<GlobalModalsActionType>
|
||||
): GlobalModalsStateType {
|
||||
if (action.type === TOGGLE_ABOUT_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
aboutContactModalProps: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_PROFILE_EDITOR) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -154,11 +154,34 @@ export const getAllSignalConnections = createSelector(
|
|||
conversations.filter(isSignalConnection)
|
||||
);
|
||||
|
||||
export const getConversationsByTitleSelector = createSelector(
|
||||
export const getSafeConversationWithSameTitle = createSelector(
|
||||
getAllConversations,
|
||||
(conversations): ((title: string) => Array<ConversationType>) =>
|
||||
(title: string) =>
|
||||
conversations.filter(conversation => conversation.title === title)
|
||||
(
|
||||
_state: StateType,
|
||||
{
|
||||
possiblyUnsafeConversation,
|
||||
}: {
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
}
|
||||
) => possiblyUnsafeConversation,
|
||||
(conversations, possiblyUnsafeConversation): ConversationType | undefined => {
|
||||
const conversationsWithSameTitle = conversations.filter(conversation => {
|
||||
return conversation.title === possiblyUnsafeConversation.title;
|
||||
});
|
||||
assertDev(
|
||||
conversationsWithSameTitle.length,
|
||||
'Expected at least 1 conversation with the same title (this one)'
|
||||
);
|
||||
|
||||
const safeConversation = conversationsWithSameTitle.find(
|
||||
otherConversation =>
|
||||
otherConversation.acceptedMessageRequest &&
|
||||
otherConversation.type === 'direct' &&
|
||||
otherConversation.id !== possiblyUnsafeConversation.id
|
||||
);
|
||||
|
||||
return safeConversation;
|
||||
}
|
||||
);
|
||||
|
||||
export const getSelectedConversationId = createSelector(
|
||||
|
|
28
ts/state/smart/CollidingAvatars.tsx
Normal file
28
ts/state/smart/CollidingAvatars.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { CollidingAvatars } from '../../components/CollidingAvatars';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
conversationIds: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
export function SmartCollidingAvatars({
|
||||
conversationIds,
|
||||
}: PropsType): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
|
||||
const conversations = useMemo(() => {
|
||||
return conversationIds.map(getConversation).sort((a, b) => {
|
||||
return (b.profileLastUpdatedAt ?? 0) - (a.profileLastUpdatedAt ?? 0);
|
||||
});
|
||||
}, [conversationIds, getConversation]);
|
||||
|
||||
return <CollidingAvatars i18n={i18n} conversations={conversations} />;
|
||||
}
|
|
@ -1,48 +1,42 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { mapValues } from 'lodash';
|
||||
import type { StateType } from '../reducer';
|
||||
|
||||
import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog';
|
||||
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import type { GetConversationByIdType } from '../selectors/conversations';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getConversationByServiceIdSelector,
|
||||
getSafeConversationWithSameTitle,
|
||||
} from '../selectors/conversations';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import { assertDev } from '../../util/assert';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||
import { isSignalConnection } from '../../util/getSignalConnections';
|
||||
import {
|
||||
getCollisionsFromMemberships,
|
||||
invertIdsByTitle,
|
||||
} from '../../util/groupMemberNameCollisions';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
|
||||
export type PropsType =
|
||||
| {
|
||||
conversationId: string;
|
||||
onClose: () => void;
|
||||
} & (
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
groupConversationId: string;
|
||||
collisionInfoByTitle: Record<
|
||||
string,
|
||||
Array<{
|
||||
oldName?: string;
|
||||
conversation: ConversationType;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
);
|
||||
export type PropsType = Readonly<{
|
||||
conversationId: string;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
export function SmartContactSpoofingReviewDialog(
|
||||
props: PropsType
|
||||
): JSX.Element {
|
||||
const { type } = props;
|
||||
): JSX.Element | null {
|
||||
const { conversationId } = props;
|
||||
|
||||
const getConversation = useSelector<StateType, GetConversationByIdType>(
|
||||
getConversationSelector
|
||||
|
@ -55,12 +49,29 @@ export function SmartContactSpoofingReviewDialog(
|
|||
deleteConversation,
|
||||
removeMember,
|
||||
} = useConversationsActions();
|
||||
const { showContactModal } = useGlobalModalActions();
|
||||
const { showContactModal, toggleSignalConnectionsModal } =
|
||||
useGlobalModalActions();
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
const getConversationByServiceId = useSelector(
|
||||
getConversationByServiceIdSelector
|
||||
);
|
||||
const conversation = getConversation(conversationId);
|
||||
|
||||
// Just binding the options argument
|
||||
const safeConversationSelector = useCallback(
|
||||
(state: StateType) => {
|
||||
return getSafeConversationWithSameTitle(state, {
|
||||
possiblyUnsafeConversation: conversation,
|
||||
});
|
||||
},
|
||||
[conversation]
|
||||
);
|
||||
const safeConvo = useSelector(safeConversationSelector);
|
||||
|
||||
const sharedProps = {
|
||||
...props,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
|
@ -69,18 +80,65 @@ export function SmartContactSpoofingReviewDialog(
|
|||
i18n,
|
||||
removeMember,
|
||||
showContactModal,
|
||||
toggleSignalConnectionsModal,
|
||||
theme,
|
||||
};
|
||||
|
||||
if (type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
|
||||
if (conversation.type === 'group') {
|
||||
const { memberships } = getGroupMemberships(
|
||||
conversation,
|
||||
getConversationByServiceId
|
||||
);
|
||||
const groupNameCollisions = getCollisionsFromMemberships(memberships);
|
||||
|
||||
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
|
||||
conversation.acknowledgedGroupNameCollisions
|
||||
);
|
||||
|
||||
const collisionInfoByTitle = mapValues(groupNameCollisions, collisions =>
|
||||
collisions.map(collision => ({
|
||||
conversation: collision,
|
||||
isSignalConnection: isSignalConnection(collision),
|
||||
oldName: getOwn(previouslyAcknowledgedTitlesById, collision.id),
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...props}
|
||||
{...sharedProps}
|
||||
group={getConversation(props.groupConversationId)}
|
||||
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
||||
group={conversation}
|
||||
collisionInfoByTitle={collisionInfoByTitle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ContactSpoofingReviewDialog {...props} {...sharedProps} />;
|
||||
const possiblyUnsafeConvo = conversation;
|
||||
assertDev(
|
||||
possiblyUnsafeConvo.type === 'direct',
|
||||
'DirectConversationWithSameTitle: expects possibly unsafe direct ' +
|
||||
'conversation'
|
||||
);
|
||||
|
||||
if (!safeConvo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const possiblyUnsafe = {
|
||||
conversation: possiblyUnsafeConvo,
|
||||
isSignalConnection: isSignalConnection(possiblyUnsafeConvo),
|
||||
};
|
||||
const safe = {
|
||||
conversation: safeConvo,
|
||||
isSignalConnection: isSignalConnection(safeConvo),
|
||||
};
|
||||
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...sharedProps}
|
||||
type={ContactSpoofingType.DirectConversationWithSameTitle}
|
||||
possiblyUnsafe={possiblyUnsafe}
|
||||
safe={safe}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import { useSelector } from 'react-redux';
|
|||
|
||||
import type { GlobalModalsStateType } from '../ducks/globalModals';
|
||||
import type { StateType } from '../reducer';
|
||||
import { isSignalConnection } from '../../util/getSignalConnections';
|
||||
import type { ExternalPropsType as AboutContactModalPropsType } from '../../components/conversation/AboutContactModal';
|
||||
import { ErrorModal } from '../../components/ErrorModal';
|
||||
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
|
||||
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
|
||||
|
@ -19,9 +21,13 @@ import { SmartSendAnywayDialog } from './SendAnywayDialog';
|
|||
import { SmartShortcutGuideModal } from './ShortcutGuideModal';
|
||||
import { SmartStickerPreviewModal } from './StickerPreviewModal';
|
||||
import { SmartStoriesSettingsModal } from './StoriesSettingsModal';
|
||||
import { getConversationsStoppingSend } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getConversationsStoppingSend,
|
||||
} from '../selectors/conversations';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
|
||||
|
||||
function renderEditHistoryMessagesModal(): JSX.Element {
|
||||
|
@ -62,12 +68,14 @@ function renderShortcutGuideModal(): JSX.Element {
|
|||
|
||||
export function SmartGlobalModalContainer(): JSX.Element {
|
||||
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
|
||||
|
||||
const {
|
||||
aboutContactModalProps: aboutContactModalRawProps,
|
||||
addUserToAnotherGroupModalContactId,
|
||||
authArtCreatorData,
|
||||
contactModalState,
|
||||
|
@ -100,9 +108,24 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
hideWhatsNewModal,
|
||||
showFormattingWarningModal,
|
||||
showSendEditWarningModal,
|
||||
toggleAboutContactModal,
|
||||
toggleSignalConnectionsModal,
|
||||
} = useGlobalModalActions();
|
||||
|
||||
const { updateSharedGroups } = useConversationsActions();
|
||||
|
||||
let aboutContactModalProps: AboutContactModalPropsType | undefined;
|
||||
if (aboutContactModalRawProps) {
|
||||
const conversation = getConversation(aboutContactModalRawProps.contactId);
|
||||
|
||||
aboutContactModalProps = {
|
||||
conversation,
|
||||
isSignalConnection: isSignalConnection(conversation),
|
||||
toggleSignalConnectionsModal,
|
||||
updateSharedGroups,
|
||||
};
|
||||
}
|
||||
|
||||
const renderAddUserToAnotherGroup = useCallback(() => {
|
||||
return (
|
||||
<SmartAddUserToAnotherGroupModal
|
||||
|
@ -140,6 +163,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
|
||||
return (
|
||||
<GlobalModalContainer
|
||||
aboutContactModalProps={aboutContactModalProps}
|
||||
addUserToAnotherGroupModalContactId={addUserToAnotherGroupModalContactId}
|
||||
contactModalState={contactModalState}
|
||||
editHistoryMessages={editHistoryMessages}
|
||||
|
@ -176,6 +200,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
showSendEditWarningModal={showSendEditWarningModal}
|
||||
stickerPackPreviewId={stickerPackPreviewId}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={toggleAboutContactModal}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
userNotFoundModalState={userNotFoundModalState}
|
||||
usernameOnboardingState={usernameOnboardingState}
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isEmpty, mapValues, pick } from 'lodash';
|
||||
import { isEmpty, pick } from 'lodash';
|
||||
import type { RefObject } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import type {
|
||||
ContactSpoofingReviewPropType,
|
||||
WarningType as TimelineWarningType,
|
||||
} from '../../components/conversation/Timeline';
|
||||
import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline';
|
||||
import { Timeline } from '../../components/conversation/Timeline';
|
||||
import type { StateType } from '../reducer';
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
|
@ -22,26 +19,25 @@ import {
|
|||
getConversationByServiceIdSelector,
|
||||
getConversationMessagesSelector,
|
||||
getConversationSelector,
|
||||
getConversationsByTitleSelector,
|
||||
getInvitedContactsForNewlyCreatedGroup,
|
||||
getSafeConversationWithSameTitle,
|
||||
getTargetedMessage,
|
||||
} from '../selectors/conversations';
|
||||
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
|
||||
|
||||
import { SmartTimelineItem } from './TimelineItem';
|
||||
import { SmartCollidingAvatars } from './CollidingAvatars';
|
||||
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
|
||||
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
||||
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog';
|
||||
import { SmartTypingBubble } from './TypingBubble';
|
||||
import { SmartHeroRow } from './HeroRow';
|
||||
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import { assertDev } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||
import {
|
||||
dehydrateCollisionsWithConversations,
|
||||
getCollisionsFromMemberships,
|
||||
invertIdsByTitle,
|
||||
} from '../../util/groupMemberNameCollisions';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
||||
|
@ -86,6 +82,12 @@ function renderItem({
|
|||
);
|
||||
}
|
||||
|
||||
function renderCollidingAvatars(
|
||||
props: SmartCollidingAvatarsPropsType
|
||||
): JSX.Element {
|
||||
return <SmartCollidingAvatars {...props} />;
|
||||
}
|
||||
|
||||
function renderContactSpoofingReviewDialog(
|
||||
props: SmartContactSpoofingReviewDialogPropsType
|
||||
): JSX.Element {
|
||||
|
@ -109,27 +111,14 @@ const getWarning = (
|
|||
switch (conversation.type) {
|
||||
case 'direct':
|
||||
if (!conversation.acceptedMessageRequest && !conversation.isBlocked) {
|
||||
const getConversationsWithTitle =
|
||||
getConversationsByTitleSelector(state);
|
||||
const conversationsWithSameTitle = getConversationsWithTitle(
|
||||
conversation.title
|
||||
);
|
||||
assertDev(
|
||||
conversationsWithSameTitle.length,
|
||||
'Expected at least 1 conversation with the same title (this one)'
|
||||
);
|
||||
|
||||
const safeConversation = conversationsWithSameTitle.find(
|
||||
otherConversation =>
|
||||
otherConversation.acceptedMessageRequest &&
|
||||
otherConversation.type === 'direct' &&
|
||||
otherConversation.id !== conversation.id
|
||||
);
|
||||
const safeConversation = getSafeConversationWithSameTitle(state, {
|
||||
possiblyUnsafeConversation: conversation,
|
||||
});
|
||||
|
||||
if (safeConversation) {
|
||||
return {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
safeConversation,
|
||||
safeConversationId: safeConversation.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -165,63 +154,6 @@ const getWarning = (
|
|||
}
|
||||
};
|
||||
|
||||
const getContactSpoofingReview = (
|
||||
selectedConversationId: string,
|
||||
state: Readonly<StateType>
|
||||
): undefined | ContactSpoofingReviewPropType => {
|
||||
const { contactSpoofingReview } = state.conversations;
|
||||
if (!contactSpoofingReview) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
const getConversationByServiceId = getConversationByServiceIdSelector(state);
|
||||
|
||||
const currentConversation = conversationSelector(selectedConversationId);
|
||||
|
||||
switch (contactSpoofingReview.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle:
|
||||
return {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
possiblyUnsafeConversation: currentConversation,
|
||||
safeConversation: conversationSelector(
|
||||
contactSpoofingReview.safeConversationId
|
||||
),
|
||||
};
|
||||
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
|
||||
assertDev(
|
||||
currentConversation.type === 'group',
|
||||
'MultipleGroupMembersWithSameTitle: expects group conversation'
|
||||
);
|
||||
const { memberships } = getGroupMemberships(
|
||||
currentConversation,
|
||||
getConversationByServiceId
|
||||
);
|
||||
const groupNameCollisions = getCollisionsFromMemberships(memberships);
|
||||
|
||||
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
|
||||
currentConversation.acknowledgedGroupNameCollisions
|
||||
);
|
||||
|
||||
const collisionInfoByTitle = mapValues(
|
||||
groupNameCollisions,
|
||||
conversations =>
|
||||
conversations.map(conversation => ({
|
||||
conversation,
|
||||
oldName: getOwn(previouslyAcknowledgedTitlesById, conversation.id),
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
collisionInfoByTitle,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(contactSpoofingReview);
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
|
||||
|
@ -259,13 +191,14 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
shouldShowMiniPlayer,
|
||||
|
||||
warning: getWarning(conversation, state),
|
||||
contactSpoofingReview: getContactSpoofingReview(id, state),
|
||||
hasContactSpoofingReview: state.conversations.hasContactSpoofingReview,
|
||||
|
||||
getTimestampForMessage,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
theme: getTheme(state),
|
||||
|
||||
renderCollidingAvatars,
|
||||
renderContactSpoofingReviewDialog,
|
||||
renderHeroRow,
|
||||
renderItem,
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
getContactNameColorSelector,
|
||||
getConversationByIdSelector,
|
||||
getConversationServiceIdsStoppingSend,
|
||||
getConversationsByTitleSelector,
|
||||
getSafeConversationWithSameTitle,
|
||||
getConversationSelector,
|
||||
getConversationsStoppingSend,
|
||||
getFilteredCandidateContactsForNewGroup,
|
||||
|
@ -1577,32 +1577,32 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getConversationsByTitleSelector', () => {
|
||||
describe('#getSafeConversationWithSameTitle', () => {
|
||||
it('returns a selector that finds conversations by title', () => {
|
||||
const unsafe = { ...makeConversation('abc'), title: 'Janet' };
|
||||
const safe = { ...makeConversation('def'), title: 'Janet' };
|
||||
const unique = { ...makeConversation('geh'), title: 'Rick' };
|
||||
const state = {
|
||||
...getEmptyRootState(),
|
||||
conversations: {
|
||||
...getEmptyState(),
|
||||
conversationLookup: {
|
||||
abc: { ...makeConversation('abc'), title: 'Janet' },
|
||||
def: { ...makeConversation('def'), title: 'Janet' },
|
||||
geh: { ...makeConversation('geh'), title: 'Rick' },
|
||||
abc: unsafe,
|
||||
def: safe,
|
||||
geh: unique,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const selector = getConversationsByTitleSelector(state);
|
||||
const janet = getSafeConversationWithSameTitle(state, {
|
||||
possiblyUnsafeConversation: unsafe,
|
||||
});
|
||||
assert.strictEqual(janet, safe);
|
||||
|
||||
assert.sameMembers(
|
||||
selector('Janet').map(c => c.id),
|
||||
['abc', 'def']
|
||||
);
|
||||
assert.sameMembers(
|
||||
selector('Rick').map(c => c.id),
|
||||
['geh']
|
||||
);
|
||||
assert.isEmpty(selector('abc'));
|
||||
assert.isEmpty(selector('xyz'));
|
||||
const rick = getSafeConversationWithSameTitle(state, {
|
||||
possiblyUnsafeConversation: unique,
|
||||
});
|
||||
assert.strictEqual(rick, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ import {
|
|||
updateConversationLookups,
|
||||
} from '../../../state/ducks/conversations';
|
||||
import { ReadStatus } from '../../../messages/MessageReadStatus';
|
||||
import { ContactSpoofingType } from '../../../util/contactSpoofing';
|
||||
import type { SingleServePromiseIdString } from '../../../services/singleServePromise';
|
||||
import { CallMode } from '../../../types/Calling';
|
||||
import { generateAci, getAciFromPrefix } from '../../../types/ServiceId';
|
||||
|
@ -75,8 +74,7 @@ const {
|
|||
repairNewestMessage,
|
||||
repairOldestMessage,
|
||||
resetAllChatColors,
|
||||
reviewGroupMemberNameCollision,
|
||||
reviewMessageRequestNameCollision,
|
||||
reviewConversationNameCollision,
|
||||
setComposeGroupAvatar,
|
||||
setComposeGroupName,
|
||||
setComposeSearchTerm,
|
||||
|
@ -523,15 +521,12 @@ describe('both/state/ducks/conversations', () => {
|
|||
it('closes the contact spoofing review modal if it was open', () => {
|
||||
const state = {
|
||||
...getEmptyState(),
|
||||
contactSpoofingReview: {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle as const,
|
||||
safeConversationId: 'abc123',
|
||||
},
|
||||
hasContactSpoofingReview: true,
|
||||
};
|
||||
const action = closeContactSpoofingReview();
|
||||
const actual = reducer(state, action);
|
||||
|
||||
assert.isUndefined(actual.contactSpoofingReview);
|
||||
assert.isFalse(actual.hasContactSpoofingReview);
|
||||
});
|
||||
|
||||
it("does nothing if the modal wasn't already open", () => {
|
||||
|
@ -1347,31 +1342,13 @@ describe('both/state/ducks/conversations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('REVIEW_GROUP_MEMBER_NAME_COLLISION', () => {
|
||||
it('starts reviewing a group member name collision', () => {
|
||||
describe('REVIEW_CONVERSATION_NAME_COLLISION', () => {
|
||||
it('starts reviewing a name collision', () => {
|
||||
const state = getEmptyState();
|
||||
const action = reviewGroupMemberNameCollision('abc123');
|
||||
const action = reviewConversationNameCollision();
|
||||
const actual = reducer(state, action);
|
||||
|
||||
assert.deepEqual(actual.contactSpoofingReview, {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle as const,
|
||||
groupConversationId: 'abc123',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('REVIEW_MESSAGE_REQUEST_NAME_COLLISION', () => {
|
||||
it('starts reviewing a message request name collision', () => {
|
||||
const state = getEmptyState();
|
||||
const action = reviewMessageRequestNameCollision({
|
||||
safeConversationId: 'def',
|
||||
});
|
||||
const actual = reducer(state, action);
|
||||
|
||||
assert.deepEqual(actual.contactSpoofingReview, {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle as const,
|
||||
safeConversationId: 'def',
|
||||
});
|
||||
assert.isTrue(actual.hasContactSpoofingReview);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -210,6 +210,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
phoneNumber: getNumber(attributes),
|
||||
profileName: getProfileName(attributes),
|
||||
profileSharing: attributes.profileSharing,
|
||||
profileLastUpdatedAt: attributes.profileLastUpdatedAt,
|
||||
notSharingPhoneNumber: attributes.notSharingPhoneNumber,
|
||||
publicParams: attributes.publicParams,
|
||||
secretParams: attributes.secretParams,
|
||||
|
|
Loading…
Add table
Reference in a new issue