Context menu for left pane list items

This commit is contained in:
Fedor Indutny 2023-04-05 13:48:00 -07:00 committed by GitHub
parent 02dedc7157
commit f61d8f38b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1046 additions and 110 deletions

View file

@ -611,6 +611,50 @@
"messageformat": "Select messages",
"description": "Shown in menu for conversation, allows the user to start selecting multiple messages in the conversation"
},
"icu:ContactListItem__menu": {
"messageformat": "Manage Contact",
"description": "Shown as aria label for context menu for a contact"
},
"icu:ContactListItem__menu__message": {
"messageformat": "Message",
"description": "Shown in a context menu for a contact, allows the user to start the conversation with the contact"
},
"icu:ContactListItem__menu__audio-call": {
"messageformat": "Audio call",
"description": "Shown in a context menu for a contact, allows the user to start the audio call with the contact"
},
"icu:ContactListItem__menu__video-call": {
"messageformat": "Video call",
"description": "Shown in a context menu for a contact, allows the user to start the video call with the contact"
},
"icu:ContactListItem__menu__remove": {
"messageformat": "Remove",
"description": "Shown in a context menu for a contact, allows the user to remove the contact from the contact list"
},
"icu:ContactListItem__menu__block": {
"messageformat": "Block",
"description": "Shown in a context menu for a contact, allows the user to block the contact"
},
"icu:ContactListItem__remove--title": {
"messageformat": "Remove {title}?",
"description": "Shown as the title in the confirmation modal for removing a contact from the contact list"
},
"icu:ContactListItem__remove--body": {
"messageformat": "You wont see this person when searching. Youll get a message request if they message you in the future.",
"description": "Shown as the body in the confirmation modal for removing a contact from the contact list"
},
"icu:ContactListItem__remove--confirm": {
"messageformat": "Remove",
"description": "Shown as the confirmation button text in the confirmation modal for removing a contact from the contact list"
},
"icu:ContactListItem__remove-system--title": {
"messageformat": "Unable to remove {title}",
"description": "Shown as the title in the confirmation modal for removing a system contact from the contact list"
},
"icu:ContactListItem__remove-system--body": {
"messageformat": "This person is saved to your devices Contacts. Delete them from your Contacts on your mobile device and try again.",
"description": "Shown as the body in the confirmation modal for removing a system contact from the contact list"
},
"moveConversationToInbox": {
"message": "Unarchive",
"description": "(deleted 03/29/2023) Undoes Archive Conversation action, and moves archived conversation back to the main conversation list"
@ -4006,6 +4050,10 @@
"messageformat": "No conversations found",
"description": "Label shown when there are no conversations to compose to"
},
"icu:Toast--ConversationRemoved": {
"messageformat": "{title} has been removed.",
"description": "Shown after the contact was removed from the contact list"
},
"Toast--error": {
"message": "An error has occurred",
"description": "(deleted 03/29/2023) Toast for general errors"
@ -5811,6 +5859,10 @@
"messageformat": "Let {name} message you and share your name and photo with them? They wont know youve seen their messages until you accept.",
"description": "Shown as the message for a message request in a direct message"
},
"icu:MessageRequests--message-direct-hidden": {
"messageformat": "Let {name} message you and share your name and photo with them? You have removed this person in the past.",
"description": "Shown as the message for a message request in a hidden conversation"
},
"MessageRequests--message-direct-blocked": {
"message": "Let $name$ message you and share your name and photo with them? You won't receive any messages until you unblock them.",
"description": "(deleted 03/29/2023) Shown as the message for a message request in a direct message with a blocked account"
@ -9739,6 +9791,10 @@
"messageformat": "The disappearing message time will be set to {timeValue} when you message them.",
"description": "A message displayed when default disappearing message timeout is about to be applied"
},
"icu:ContactRemovedNotification__text": {
"messageformat": "You have removed this person, messaging them again will add them back to your list.",
"description": "A message displayed when contact was removed and will be added back on an outgoing message"
},
"ErrorBoundaryNotification__text": {
"message": "Couldn't display this message. Click to submit a debug log.",
"description": "(deleted 03/29/2023) An error notification displayed when message fails to render due to an internal error"

View file

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1106_15216)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99998 0.850006C7.06102 0.850006 6.13127 1.03495 5.26379 1.39427C4.39631 1.75359 3.6081 2.28025 2.94416 2.94419C2.28022 3.60813 1.75356 4.39634 1.39424 5.26382C1.03492 6.1313 0.849976 7.06105 0.849976 8.00001C0.849976 8.93896 1.03492 9.86871 1.39424 10.7362C1.75356 11.6037 2.28022 12.3919 2.94416 13.0558C3.6081 13.7198 4.39631 14.2464 5.26379 14.6057C6.13127 14.9651 7.06102 15.15 7.99998 15.15C9.89627 15.15 11.7149 14.3967 13.0558 13.0558C14.3967 11.7149 15.15 9.8963 15.15 8.00001C15.15 6.10371 14.3967 4.28508 13.0558 2.94419C11.7149 1.60331 9.89627 0.850006 7.99998 0.850006ZM2.14998 8.00001C2.14999 6.89834 2.46108 5.81906 3.04744 4.8864C3.63381 3.95374 4.47161 3.2056 5.46442 2.72811C6.45724 2.25061 7.5647 2.06317 8.65935 2.18734C9.754 2.31151 10.7913 2.74226 11.652 3.43001L3.42898 11.652C2.59981 10.6155 2.14867 9.32735 2.14998 8.00001ZM4.34898 12.57C5.47323 13.4683 6.88913 13.9207 8.32596 13.8407C9.76279 13.7607 11.1197 13.1539 12.1373 12.1363C13.1548 11.1187 13.7616 9.76182 13.8417 8.32499C13.9217 6.88816 13.4693 5.47226 12.571 4.34801L4.34898 12.571V12.57Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1106_15216">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.35004 8.00001C1.35004 4.32731 4.32735 1.35001 8.00004 1.35001C11.6727 1.35001 14.65 4.32731 14.65 8.00001C14.65 11.6727 11.6727 14.65 8.00004 14.65C6.98452 14.65 6.02049 14.4219 5.15793 14.0136L2.72756 14.8886C1.81202 15.2182 0.926416 14.3326 1.25601 13.417L2.10043 11.0714C1.62089 10.152 1.35004 9.10669 1.35004 8.00001ZM8.00004 2.65001C5.04532 2.65001 2.65004 5.04528 2.65004 8.00001C2.65004 8.90696 2.87519 9.7594 3.27211 10.5064C3.42107 10.7867 3.46078 11.1306 3.3441 11.4548L2.58713 13.5574L4.77169 12.771C5.08007 12.66 5.40687 12.6906 5.67914 12.8219C6.3804 13.1601 7.16713 13.35 8.00004 13.35C10.9548 13.35 13.35 10.9547 13.35 8.00001C13.35 5.04528 10.9548 2.65001 8.00004 2.65001Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 860 B

View file

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_872_13049)">
<path d="M10.5 8.64998C10.859 8.64998 11.15 8.35896 11.15 7.99998C11.15 7.64099 10.859 7.34998 10.5 7.34998H5.50002C5.14104 7.34998 4.85002 7.64099 4.85002 7.99998C4.85002 8.35896 5.14104 8.64998 5.50002 8.64998H10.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99998 0.849976C4.05114 0.849976 0.849976 4.05114 0.849976 7.99998C0.849976 11.9488 4.05114 15.15 7.99998 15.15C11.9488 15.15 15.15 11.9488 15.15 7.99998C15.15 4.05114 11.9488 0.849976 7.99998 0.849976ZM2.14998 7.99998C2.14998 4.76911 4.76911 2.14998 7.99998 2.14998C11.2308 2.14998 13.85 4.76911 13.85 7.99998C13.85 11.2308 11.2308 13.85 7.99998 13.85C4.76911 13.85 2.14998 11.2308 2.14998 7.99998Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_872_13049">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 950 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.91773 1.92588C3.76021 1.0834 5.15243 1.17789 5.87336 2.12647L7.2799 3.97719C7.87482 4.75998 7.80003 5.86189 7.1048 6.55712L6.46123 7.2007C6.48182 7.25097 6.51214 7.31523 6.55567 7.39358C6.72937 7.70625 7.04291 8.11804 7.46241 8.53754C7.88191 8.95704 8.2937 9.27058 8.60636 9.44428C8.68472 9.48781 8.74898 9.51812 8.79924 9.53872L9.44282 8.89514C10.1381 8.19991 11.24 8.12513 12.0228 8.72005L13.8735 10.1266C14.8221 10.8475 14.9165 12.2397 14.0741 13.0822L13.7915 13.3648C12.7572 14.3991 11.2069 14.9732 9.70868 14.4598C7.87144 13.8302 6.1462 12.7824 4.68188 11.3181C3.21756 9.85375 2.16978 8.12851 1.54014 6.29127C1.0267 4.79307 1.60087 3.24274 2.63519 2.20842L2.91773 1.92588ZM4.83835 2.91308C4.59409 2.5917 4.12241 2.55968 3.83697 2.84512L3.55443 3.12766C2.78146 3.90063 2.45476 4.95016 2.76993 5.86981C3.33675 7.52375 4.27994 9.07765 5.60112 10.3988C6.92229 11.72 8.4762 12.6632 10.1301 13.23C11.0498 13.5452 12.0993 13.2185 12.8723 12.4455L13.1548 12.163C13.4403 11.8775 13.4082 11.4059 13.0869 11.1616L11.2362 9.75506C10.9709 9.5535 10.5976 9.57883 10.3621 9.81438L9.55636 10.6201C9.25886 10.9176 8.87013 10.8917 8.66404 10.8517C8.43108 10.8066 8.1936 10.7021 7.97503 10.5807C7.52983 10.3334 7.02314 9.93674 6.54317 9.45678C6.0632 8.97681 5.6666 8.47012 5.41926 8.02492C5.29783 7.80634 5.19332 7.56887 5.1482 7.33591C5.10829 7.12982 5.08236 6.74109 5.37986 6.44359L6.18557 5.63789C6.42111 5.40234 6.44645 5.02901 6.24489 4.7638L4.83835 2.91308ZM8.92706 9.57776C8.92706 9.57776 8.92448 9.57776 8.91847 9.57677C8.92392 9.57711 8.92706 9.57776 8.92706 9.57776ZM6.42218 7.07289C6.42218 7.07289 6.42284 7.07603 6.42318 7.08148C6.42219 7.07547 6.42218 7.07289 6.42218 7.07289Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.42316 2.34998C3.886 2.34997 3.44368 2.34996 3.08357 2.37938C2.70987 2.40992 2.36784 2.47534 2.04702 2.63881C1.54839 2.89287 1.143 3.29827 0.888931 3.7969C0.725467 4.11772 0.660038 4.45975 0.629505 4.83345C0.600083 5.19356 0.60009 5.63588 0.600098 6.17304V9.82692C0.60009 10.3641 0.600083 10.8064 0.629505 11.1665C0.660038 11.5402 0.725467 11.8822 0.888931 12.2031C1.143 12.7017 1.54839 13.1071 2.04702 13.3611C2.36784 13.5246 2.70987 13.59 3.08357 13.6206C3.44368 13.65 3.886 13.65 4.42316 13.65H7.82703C8.36419 13.65 8.80651 13.65 9.16663 13.6206C9.54033 13.59 9.88236 13.5246 10.2032 13.3611C10.7018 13.1071 11.1072 12.7017 11.3613 12.2031C11.5247 11.8822 11.5902 11.5402 11.6207 11.1665C11.638 10.9544 11.6451 10.7139 11.6481 10.4422L13.2601 12.0543C14.1421 12.9362 15.6501 12.3116 15.6501 11.0643V4.93564C15.6501 3.68837 14.1421 3.06373 13.2601 3.94569L11.6481 5.55777C11.6451 5.2861 11.638 5.04552 11.6207 4.83345C11.5902 4.45975 11.5247 4.11772 11.3613 3.7969C11.1072 3.29827 10.7018 2.89287 10.2032 2.63881C9.88236 2.47534 9.54033 2.40992 9.16663 2.37938C8.80651 2.34996 8.36419 2.34997 7.82704 2.34998H4.42316ZM10.3501 6.19998C10.3501 5.6292 10.3496 5.24021 10.325 4.93931C10.301 4.646 10.2575 4.4941 10.203 4.38709C10.0735 4.13307 9.867 3.92655 9.61299 3.79712C9.50598 3.74259 9.35407 3.69903 9.06076 3.67507C8.75987 3.65048 8.37088 3.64998 7.8001 3.64998H4.4501C3.87932 3.64998 3.49033 3.65048 3.18943 3.67507C2.89613 3.69903 2.74422 3.74259 2.63721 3.79712C2.38319 3.92655 2.17667 4.13307 2.04724 4.38709C1.99272 4.4941 1.94915 4.646 1.92519 4.93931C1.9006 5.24021 1.9001 5.6292 1.9001 6.19998V9.79998C1.9001 10.3708 1.9006 10.7597 1.92519 11.0606C1.94915 11.3539 1.99272 11.5059 2.04724 11.6129C2.17667 11.8669 2.38319 12.0734 2.63721 12.2028C2.74422 12.2574 2.89613 12.3009 3.18943 12.3249C3.49033 12.3495 3.87932 12.35 4.4501 12.35H7.8001C8.37088 12.35 8.75987 12.3495 9.06076 12.3249C9.35407 12.3009 9.50598 12.2574 9.61299 12.2028C9.867 12.0734 10.0735 11.8669 10.203 11.6129C10.2575 11.5059 10.301 11.3539 10.325 11.0606C10.3496 10.7597 10.3501 10.3708 10.3501 9.79998V6.19998ZM11.6501 7.99998C11.6501 8.38784 11.8042 8.75981 12.0784 9.03407L14.1794 11.135C14.2016 11.1572 14.2185 11.1626 14.2301 11.1646C14.2453 11.1672 14.2661 11.1659 14.2884 11.1567C14.3107 11.1475 14.3263 11.1337 14.3352 11.1211C14.342 11.1114 14.3501 11.0957 14.3501 11.0643V4.93564C14.3501 4.90421 14.342 4.88852 14.3352 4.87886C14.3263 4.86626 14.3107 4.85249 14.2884 4.84325C14.2661 4.83401 14.2453 4.83271 14.2301 4.83534C14.2185 4.83736 14.2016 4.84271 14.1794 4.86492L12.0784 6.96588C11.8042 7.24014 11.6501 7.61211 11.6501 7.99998Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -96,6 +96,7 @@ message ContactRecord {
optional string systemGivenName = 17;
optional string systemFamilyName = 18;
optional string systemNickname = 19;
optional bool hidden = 20;
}
message GroupV1Record {

View file

@ -5458,7 +5458,7 @@ button.module-image__border-overlay:focus {
%module-composition-popper {
width: 332px;
border-radius: 8px;
border-radius: 4px;
margin-bottom: 6px;
z-index: $z-index-context-menu;
user-select: none;

View file

@ -0,0 +1,129 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.ContactListItem {
&__context-menu {
&__chat-icon {
@include dark-theme {
@include color-svg(
'../images/icons/v3/chat--compact.svg',
$color-white
);
}
@include light-theme {
@include color-svg(
'../images/icons/v3/chat--compact.svg',
$color-black
);
}
}
&__phone-icon {
@include dark-theme {
@include color-svg(
'../images/icons/v3/phone--compact.svg',
$color-white
);
}
@include light-theme {
@include color-svg(
'../images/icons/v3/phone--compact.svg',
$color-black
);
}
}
&__video-icon {
@include dark-theme {
@include color-svg(
'../images/icons/v3/video--compact.svg',
$color-white
);
}
@include light-theme {
@include color-svg(
'../images/icons/v3/video--compact.svg',
$color-black
);
}
}
&__delete-icon {
@include dark-theme {
@include color-svg(
'../images/icons/v3/minus--circle--compact.svg',
$color-white
);
}
@include light-theme {
@include color-svg(
'../images/icons/v3/minus--circle--compact.svg',
$color-black
);
}
}
&__block-icon {
@include dark-theme {
@include color-svg(
'../images/icons/v3/block--compact.svg',
$color-white
);
}
@include light-theme {
@include color-svg(
'../images/icons/v3/block--compact.svg',
$color-black
);
}
}
// Overrides
&__popper.ContextMenu__popper {
min-width: 240px;
}
&__button.ContextMenu__button {
opacity: 0;
.ContactListItem:hover & {
opacity: 1;
}
&:hover {
@include light-theme {
background-color: $color-gray-20;
}
@include dark-theme {
background-color: $color-gray-80;
}
}
width: 28px;
height: 28px;
padding: 4px;
border-radius: 4px;
&::after {
display: block;
width: 20px;
height: 20px;
content: '';
@include dark-theme {
@include color-svg(
'../images/icons/v2/more-horiz-24.svg',
$color-white
);
}
@include light-theme {
@include color-svg(
'../images/icons/v2/more-horiz-24.svg',
$color-black
);
}
}
}
}
}

View file

@ -11,7 +11,7 @@
&__popper {
@extend %module-composition-popper;
margin: 0;
padding: 6px 2px;
padding: 6px 0px;
width: auto;
&--single-item {
@ -36,10 +36,9 @@
}
align-items: center;
border-radius: 6px;
display: flex;
justify-content: space-between;
padding: 6px;
padding: 7px 12px;
min-width: 150px;
width: 100%;
@ -88,7 +87,6 @@
&:focus,
&:active {
@include keyboard-mode {
border-radius: 6px;
box-shadow: 0 0 1px 1px $color-ultramarine;
outline: none;
}

View file

@ -55,6 +55,7 @@
@import './components/CompositionRecordingDraft.scss';
@import './components/CompositionInput.scss';
@import './components/CompositionTextArea.scss';
@import './components/ContactListItem.scss';
@import './components/ContactModal.scss';
@import './components/ContactName.scss';
@import './components/ContactPill.scss';

View file

@ -16,6 +16,8 @@ export type ConfigKeyType =
| 'desktop.announcementGroup'
| 'desktop.calling.audioLevelForSpeaking'
| 'desktop.cdsi.returnAcisWithoutUaks'
| 'desktop.contactManagement'
| 'desktop.contactManagement.beta'
| 'desktop.clientExpiration'
| 'desktop.groupCallOutboundRing2'
| 'desktop.groupCallOutboundRing2.beta'

View file

@ -71,6 +71,7 @@ import type { ShowToastAction } from '../state/ducks/toast';
export type OwnProps = Readonly<{
acceptedMessageRequest?: boolean;
removalStage?: 'justNotification' | 'messageRequest';
addAttachment: (
conversationId: string,
attachment: InMemoryAttachmentDraftType
@ -265,6 +266,7 @@ export function CompositionArea({
isMissingMandatoryProfileSharing,
left,
messageRequestsEnabled,
removalStage,
acceptConversation,
blockConversation,
blockAndReportSpam,
@ -577,7 +579,9 @@ export function CompositionArea({
if (
isBlocked ||
areWePending ||
(messageRequestsEnabled && !acceptedMessageRequest)
(messageRequestsEnabled &&
!acceptedMessageRequest &&
removalStage !== 'justNotification')
) {
return (
<MessageRequestActions
@ -589,6 +593,7 @@ export function CompositionArea({
deleteConversation={deleteConversation}
i18n={i18n}
isBlocked={isBlocked}
isHidden={removalStage !== undefined}
title={title}
/>
);
@ -627,7 +632,7 @@ export function CompositionArea({
// If no message request, but we haven't shared profile yet, we show profile-sharing UI
if (
!left &&
(conversationType === 'direct' ||
((conversationType === 'direct' && removalStage !== 'justNotification') ||
(conversationType === 'group' && groupVersion === 1)) &&
isMissingMandatoryProfileSharing
) {

View file

@ -4,6 +4,7 @@
import type { KeyboardEvent, ReactNode } from 'react';
import type { Options, VirtualElement } from '@popperjs/core';
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import { usePopper } from 'react-popper';
import { noop } from 'lodash';
@ -42,6 +43,7 @@ export type PropsType<T> = Readonly<{
onClick?: (ev: React.MouseEvent) => unknown;
onMenuShowingChanged?: (value: boolean) => unknown;
popperOptions?: Pick<Options, 'placement' | 'strategy'>;
portalToRoot?: boolean;
theme?: Theme;
title?: string;
value?: T;
@ -67,6 +69,7 @@ export function ContextMenu<T>({
onClick,
onMenuShowingChanged,
popperOptions,
portalToRoot,
theme,
title,
value,
@ -118,6 +121,21 @@ export function ContextMenu<T>({
);
}, [isMenuShowing, referenceElement, popperElement]);
const [portalNode, setPortalNode] = React.useState<HTMLElement | null>(null);
useEffect(() => {
if (!portalToRoot || !isMenuShowing) {
return noop;
}
const div = document.createElement('div');
document.body.appendChild(div);
setPortalNode(div);
return () => {
document.body.removeChild(div);
};
}, [portalToRoot, isMenuShowing]);
const handleKeyDown = (ev: KeyboardEvent) => {
if ((ev.key === 'Enter' || ev.key === 'Space') && !isMenuShowing) {
closeCurrentOpenContextMenu?.();
@ -302,7 +320,7 @@ export function ContextMenu<T>({
>
{children}
</button>
{menuNode}
{portalNode ? createPortal(menuNode, portalNode) : menuNode}
</div>
);
}

View file

@ -67,9 +67,17 @@ function Wrapper({
getRow={(index: number) => rows[index]}
shouldRecomputeRowHeights={false}
i18n={i18n}
blockConversation={action('blockConversation')}
onSelectConversation={action('onSelectConversation')}
onOutgoingAudioCallInConversation={action(
'onOutgoingAudioCallInConversation'
)}
onOutgoingVideoCallInConversation={action(
'onOutgoingVideoCallInConversation'
)}
onClickArchiveButton={action('onClickArchiveButton')}
onClickContactCheckbox={action('onClickContactCheckbox')}
removeConversation={action('removeConversation')}
renderMessageSearchResult={(id: string) => (
<MessageSearchResult
body="Lorem ipsum wow"
@ -140,6 +148,24 @@ ContactDirect.story = {
name: 'Contact: direct',
};
export function ContactDirectWithContextMenu(): JSX.Element {
return (
<Wrapper
rows={[
{
type: RowType.Contact,
contact: defaultConversations[0],
hasContextMenu: true,
},
]}
/>
);
}
ContactDirectWithContextMenu.story = {
name: 'Contact: context menu',
};
export function ContactDirectWithShortAbout(): JSX.Element {
return (
<Wrapper

View file

@ -64,6 +64,7 @@ type ContactRowType = {
type: RowType.Contact;
contact: ContactListItemPropsType;
isClickable?: boolean;
hasContextMenu?: boolean;
};
type ContactCheckboxRowType = {
@ -175,12 +176,16 @@ export type PropsType = {
i18n: LocalizerType;
theme: ThemeType;
blockConversation: (conversationId: string) => void;
onClickArchiveButton: () => void;
onClickContactCheckbox: (
conversationId: string,
disabledReason: undefined | ContactCheckboxDisabledReason
) => void;
onSelectConversation: (conversationId: string, messageId?: string) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
removeConversation?: (conversationId: string) => void;
renderMessageSearchResult?: (id: string) => JSX.Element;
showChooseGroupMembers: () => void;
showConversation: ShowConversationType;
@ -195,9 +200,13 @@ export function ConversationList({
getPreferredBadge,
getRow,
i18n,
blockConversation,
onClickArchiveButton,
onClickContactCheckbox,
onSelectConversation,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
removeConversation,
renderMessageSearchResult,
rowCount,
scrollBehavior = ScrollBehavior.Default,
@ -266,7 +275,7 @@ export function ConversationList({
result = undefined;
break;
case RowType.Contact: {
const { isClickable = true } = row;
const { isClickable = true, hasContextMenu = false } = row;
result = (
<ContactListItem
{...row.contact}
@ -274,6 +283,15 @@ export function ConversationList({
onClick={isClickable ? onSelectConversation : undefined}
i18n={i18n}
theme={theme}
hasContextMenu={hasContextMenu}
onAudioCall={
isClickable ? onOutgoingAudioCallInConversation : undefined
}
onVideoCall={
isClickable ? onOutgoingVideoCallInConversation : undefined
}
onBlock={isClickable ? blockConversation : undefined}
onRemove={isClickable ? removeConversation : undefined}
/>
);
break;
@ -343,6 +361,7 @@ export function ConversationList({
'muteExpiresAt',
'phoneNumber',
'profileName',
'removalStage',
'sharedGroupNames',
'shouldShowDraft',
'title',
@ -452,18 +471,22 @@ export function ConversationList({
);
},
[
blockConversation,
getPreferredBadge,
getRow,
i18n,
lookupConversationWithoutUuid,
onClickArchiveButton,
onClickContactCheckbox,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
onSelectConversation,
lookupConversationWithoutUuid,
showUserNotFoundModal,
setIsFetchingUUID,
removeConversation,
renderMessageSearchResult,
setIsFetchingUUID,
showChooseGroupMembers,
showConversation,
showUserNotFoundModal,
theme,
]
);

View file

@ -356,6 +356,10 @@ export function ForwardMessagesModal({
showUserNotFoundModal={shouldNeverBeCalled}
setIsFetchingUUID={shouldNeverBeCalled}
onSelectConversation={shouldNeverBeCalled}
blockConversation={shouldNeverBeCalled}
removeConversation={shouldNeverBeCalled}
onOutgoingAudioCallInConversation={shouldNeverBeCalled}
onOutgoingVideoCallInConversation={shouldNeverBeCalled}
renderMessageSearchResult={() => {
shouldNeverBeCalled();
return <div />;

View file

@ -127,6 +127,10 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
}
const isUpdateDownloaded = boolean('isUpdateDownloaded', false);
const isContactManagementEnabled = boolean(
'isContactManagementEnabled',
true
);
return {
clearConversationSearch: action('clearConversationSearch'),
@ -160,12 +164,21 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
undefined
),
isUpdateDownloaded,
isContactManagementEnabled,
setChallengeStatus: action('setChallengeStatus'),
lookupConversationWithoutUuid: makeFakeLookupConversationWithoutUuid(),
showUserNotFoundModal: action('showUserNotFoundModal'),
setIsFetchingUUID,
showConversation: action('showConversation'),
blockConversation: action('blockConversation'),
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
),
onOutgoingVideoCallInConversation: action(
'onOutgoingVideoCallInConversation'
),
removeConversation: action('removeConversation'),
renderMainHeader: () => <div />,
renderMessageSearchResult: (id: string) => (
<MessageSearchResult
@ -268,9 +281,17 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
};
};
function LeftPaneInContainer(props: PropsType): JSX.Element {
return (
<div style={{ height: '600px' }}>
<LeftPane {...props} />
</div>
);
}
export function InboxNoConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -291,7 +312,7 @@ InboxNoConversations.story = {
export function InboxOnlyPinnedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -312,7 +333,7 @@ InboxOnlyPinnedConversations.story = {
export function InboxOnlyNonPinnedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -333,7 +354,7 @@ InboxOnlyNonPinnedConversations.story = {
export function InboxOnlyArchivedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -354,7 +375,7 @@ InboxOnlyArchivedConversations.story = {
export function InboxPinnedAndArchivedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -375,7 +396,7 @@ InboxPinnedAndArchivedConversations.story = {
export function InboxNonPinnedAndArchivedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -396,7 +417,7 @@ InboxNonPinnedAndArchivedConversations.story = {
export function InboxPinnedAndNonPinnedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -416,7 +437,7 @@ InboxPinnedAndNonPinnedConversations.story = {
};
export function InboxPinnedNonPinnedAndArchivedConversations(): JSX.Element {
return <LeftPane {...useProps()} />;
return <LeftPaneInContainer {...useProps()} />;
}
InboxPinnedNonPinnedAndArchivedConversations.story = {
@ -425,7 +446,7 @@ InboxPinnedNonPinnedAndArchivedConversations.story = {
export function SearchNoResultsWhenSearchingEverywhere(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -446,7 +467,7 @@ SearchNoResultsWhenSearchingEverywhere.story = {
export function SearchNoResultsWhenSearchingEverywhereSms(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -467,7 +488,7 @@ SearchNoResultsWhenSearchingEverywhereSms.story = {
export function SearchNoResultsWhenSearchingInAConversation(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -489,7 +510,7 @@ SearchNoResultsWhenSearchingInAConversation.story = {
export function SearchAllResultsLoading(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -510,7 +531,7 @@ SearchAllResultsLoading.story = {
export function SearchSomeResultsLoading(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -534,7 +555,7 @@ SearchSomeResultsLoading.story = {
export function SearchHasConversationsAndContactsButNotMessages(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -558,7 +579,7 @@ SearchHasConversationsAndContactsButNotMessages.story = {
export function SearchAllResults(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -588,7 +609,7 @@ SearchAllResults.story = {
export function ArchiveNoArchivedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Archive,
@ -608,7 +629,7 @@ ArchiveNoArchivedConversations.story = {
export function ArchiveArchivedConversations(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Archive,
@ -628,7 +649,7 @@ ArchiveArchivedConversations.story = {
export function ArchiveSearchingAConversation(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Archive,
@ -648,7 +669,7 @@ ArchiveSearchingAConversation.story = {
export function ComposeNoResults(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -670,7 +691,7 @@ ComposeNoResults.story = {
export function ComposeSomeContactsNoSearchTerm(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -692,7 +713,7 @@ ComposeSomeContactsNoSearchTerm.story = {
export function ComposeSomeContactsWithASearchTerm(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -714,7 +735,7 @@ ComposeSomeContactsWithASearchTerm.story = {
export function ComposeSomeGroupsNoSearchTerm(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -736,7 +757,7 @@ ComposeSomeGroupsNoSearchTerm.story = {
export function ComposeSomeGroupsWithSearchTerm(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -758,7 +779,7 @@ ComposeSomeGroupsWithSearchTerm.story = {
export function ComposeSearchIsValidUsername(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -780,7 +801,7 @@ ComposeSearchIsValidUsername.story = {
export function ComposeSearchIsValidUsernameFetchingUsername(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -804,7 +825,7 @@ ComposeSearchIsValidUsernameFetchingUsername.story = {
export function ComposeSearchIsValidUsernameButFlagIsNotEnabled(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -826,7 +847,7 @@ ComposeSearchIsValidUsernameButFlagIsNotEnabled.story = {
export function ComposeSearchIsPartialPhoneNumber(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -848,7 +869,7 @@ ComposeSearchIsPartialPhoneNumber.story = {
export function ComposeSearchIsValidPhoneNumber(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -870,7 +891,7 @@ ComposeSearchIsValidPhoneNumber.story = {
export function ComposeSearchIsValidPhoneNumberFetchingPhoneNumber(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -894,7 +915,7 @@ ComposeSearchIsValidPhoneNumberFetchingPhoneNumber.story = {
export function ComposeAllKindsOfResultsNoSearchTerm(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -916,7 +937,7 @@ ComposeAllKindsOfResultsNoSearchTerm.story = {
export function ComposeAllKindsOfResultsWithASearchTerm(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
@ -938,7 +959,7 @@ ComposeAllKindsOfResultsWithASearchTerm.story = {
export function CaptchaDialogRequired(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -961,7 +982,7 @@ CaptchaDialogRequired.story = {
export function CaptchaDialogPending(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -983,7 +1004,7 @@ CaptchaDialogPending.story = {
};
export const _CrashReportDialog = (): JSX.Element => (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,
@ -1005,7 +1026,7 @@ _CrashReportDialog.story = {
export function ChooseGroupMembersPartialPhoneNumber(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.ChooseGroupMembers,
@ -1031,7 +1052,7 @@ ChooseGroupMembersPartialPhoneNumber.story = {
export function ChooseGroupMembersValidPhoneNumber(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.ChooseGroupMembers,
@ -1057,7 +1078,7 @@ ChooseGroupMembersValidPhoneNumber.story = {
export function ChooseGroupMembersUsername(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.ChooseGroupMembers,
@ -1083,7 +1104,7 @@ ChooseGroupMembersUsername.story = {
export function GroupMetadataNoTimer(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.SetGroupMetadata,
@ -1107,7 +1128,7 @@ GroupMetadataNoTimer.story = {
export function GroupMetadataRegularTimer(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.SetGroupMetadata,
@ -1131,7 +1152,7 @@ GroupMetadataRegularTimer.story = {
export function GroupMetadataCustomTimer(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
mode: LeftPaneMode.SetGroupMetadata,
@ -1155,7 +1176,7 @@ GroupMetadataCustomTimer.story = {
export function SearchingConversation(): JSX.Element {
return (
<LeftPane
<LeftPaneInContainer
{...useProps({
modeSpecificProps: {
...defaultSearchProps,

View file

@ -67,6 +67,7 @@ export type PropsType = {
hasRelinkDialog: boolean;
hasUpdateDialog: boolean;
isUpdateDownloaded: boolean;
isContactManagementEnabled: boolean;
unsupportedOSDialogType: 'error' | 'warning' | undefined;
// These help prevent invalid states. For example, we don't need the list of pinned
@ -104,6 +105,7 @@ export type PropsType = {
theme: ThemeType;
// Action Creators
blockConversation: (conversationId: string) => void;
clearConversationSearch: () => void;
clearGroupCreationError: () => void;
clearSearch: () => void;
@ -113,6 +115,9 @@ export type PropsType = {
composeReplaceAvatar: ReplaceAvatarActionType;
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
createGroup: () => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
removeConversation: (conversationId: string) => void;
savePreferredLeftPaneWidth: (_: number) => void;
searchInConversation: (conversationId: string) => unknown;
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
@ -151,6 +156,7 @@ export type PropsType = {
} & LookupConversationWithoutUuidActionsType;
export function LeftPane({
blockConversation,
challengeStatus,
clearConversationSearch,
clearGroupCreationError,
@ -171,8 +177,12 @@ export function LeftPane({
lookupConversationWithoutUuid,
isMacOS,
isUpdateDownloaded,
isContactManagementEnabled,
modeSpecificProps,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
preferredWidthFromStorage,
removeConversation,
renderCaptchaDialog,
renderCrashReportDialog,
renderExpiredBuildDialog,
@ -678,7 +688,17 @@ export function LeftPane({
setIsFetchingUUID={setIsFetchingUUID}
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
showConversation={showConversation}
blockConversation={blockConversation}
onSelectConversation={onSelectConversation}
onOutgoingAudioCallInConversation={
onOutgoingAudioCallInConversation
}
onOutgoingVideoCallInConversation={
onOutgoingVideoCallInConversation
}
removeConversation={
isContactManagementEnabled ? removeConversation : undefined
}
renderMessageSearchResult={renderMessageSearchResult}
rowCount={helper.getRowCount()}
scrollBehavior={scrollBehavior}

View file

@ -12,6 +12,7 @@ export type Props = {
subtitle?: string | JSX.Element;
leading?: string | JSX.Element;
trailing?: string | JSX.Element;
moduleClassName?: string;
onClick?: () => void;
onContextMenu?: (ev: React.MouseEvent<Element, MouseEvent>) => void;
// show hover highlight,
@ -28,8 +29,6 @@ export type Props = {
testId?: string;
};
const getClassName = getClassNamesFor('ListTile');
/**
* A single row that typically contains some text and leading/trailing icons/widgets
*
@ -72,6 +71,7 @@ const ListTileImpl = React.forwardRef<HTMLButtonElement, Props>(
subtitle,
leading,
trailing,
moduleClassName,
onClick,
onContextMenu,
clickable,
@ -85,6 +85,8 @@ const ListTileImpl = React.forwardRef<HTMLButtonElement, Props>(
) {
const isClickable = clickable ?? Boolean(onClick);
const getClassName = getClassNamesFor('ListTile', moduleClassName);
const rootProps = {
className: classNames(
getClassName(''),

View file

@ -1209,6 +1209,10 @@ export function EditDistributionListModal({
toggleSelectedConversation(conversationId);
}}
onSelectConversation={shouldNeverBeCalled}
blockConversation={shouldNeverBeCalled}
removeConversation={shouldNeverBeCalled}
onOutgoingAudioCallInConversation={shouldNeverBeCalled}
onOutgoingVideoCallInConversation={shouldNeverBeCalled}
renderMessageSearchResult={() => {
shouldNeverBeCalled();
return <div />;

View file

@ -127,6 +127,16 @@ ConversationMarkedUnread.args = {
},
};
export const ConversationRemoved = Template.bind({});
ConversationRemoved.args = {
toast: {
toastType: ToastType.ConversationRemoved,
parameters: {
title: 'Alice',
},
},
};
export const ConversationUnarchived = Template.bind({});
ConversationUnarchived.args = {
toast: {

View file

@ -134,6 +134,16 @@ export function ToastManager({
);
}
if (toastType === ToastType.ConversationRemoved) {
return (
<Toast onClose={hideToast}>
{i18n('icu:Toast--ConversationRemoved', {
title: toast?.parameters?.title ?? '',
})}
</Toast>
);
}
if (toastType === ToastType.ConversationUnarchived) {
return (
<Toast onClose={hideToast}>

View file

@ -15,6 +15,7 @@ import type { LocalizerType } from '../../types/Util';
export type Props = {
i18n: LocalizerType;
isHidden?: boolean;
} & Omit<ContactNameProps, 'module'> &
Omit<
MessageRequestActionsConfirmationProps,
@ -30,6 +31,7 @@ export function MessageRequestActions({
deleteConversation,
firstName,
i18n,
isHidden,
isBlocked,
title,
}: Props): JSX.Element {
@ -44,6 +46,43 @@ export function MessageRequestActions({
</strong>
);
let message: JSX.Element | undefined;
if (conversationType === 'direct') {
if (isBlocked) {
message = (
<Intl
i18n={i18n}
id="icu:MessageRequests--message-direct-blocked"
components={{ name }}
/>
);
} else if (isHidden) {
message = (
<Intl
i18n={i18n}
id="icu:MessageRequests--message-direct-hidden"
components={{ name }}
/>
);
} else {
message = (
<Intl
i18n={i18n}
id="icu:MessageRequests--message-direct"
components={{ name }}
/>
);
}
} else if (conversationType === 'group') {
if (isBlocked) {
message = (
<Intl i18n={i18n} id="icu:MessageRequests--message-group-blocked" />
);
} else {
message = <Intl i18n={i18n} id="icu:MessageRequests--message-group" />;
}
}
return (
<>
{mrState !== MessageRequestState.default ? (
@ -61,28 +100,7 @@ export function MessageRequestActions({
/>
) : null}
<div className="module-message-request-actions">
<p className="module-message-request-actions__message">
{conversationType === 'direct' && isBlocked && (
<Intl
i18n={i18n}
id="icu:MessageRequests--message-direct-blocked"
components={{ name }}
/>
)}
{conversationType === 'direct' && !isBlocked && (
<Intl
i18n={i18n}
id="icu:MessageRequests--message-direct"
components={{ name }}
/>
)}
{conversationType === 'group' && isBlocked && (
<Intl i18n={i18n} id="icu:MessageRequests--message-group-blocked" />
)}
{conversationType === 'group' && !isBlocked && (
<Intl i18n={i18n} id="icu:MessageRequests--message-group" />
)}
</p>
<p className="module-message-request-actions__message">{message}</p>
<div className="module-message-request-actions__buttons">
<Button
onClick={() => {

View file

@ -153,6 +153,10 @@ export function Notification(): JSX.Element {
{
type: 'chatSessionRefreshed',
},
{
type: 'contactRemovedNotification',
data: null,
},
{
type: 'safetyNumberNotification',
data: {

View file

@ -50,6 +50,7 @@ import type { PropsType as PaymentEventNotificationPropsType } from './PaymentEv
import { PaymentEventNotification } from './PaymentEventNotification';
import type { PropsDataType as ConversationMergeNotificationPropsType } from './ConversationMergeNotification';
import { ConversationMergeNotification } from './ConversationMergeNotification';
import { SystemMessage } from './SystemMessage';
import type { FullJSXType } from '../Intl';
import { TimelineMessage } from './TimelineMessage';
@ -81,6 +82,10 @@ type UniversalTimerNotificationType = {
type: 'universalTimerNotification';
data: null;
};
type ContactRemovedNotificationType = {
type: 'contactRemovedNotification';
data: null;
};
type ChangeNumberNotificationType = {
type: 'changeNumberNotification';
data: ChangeNumberNotificationProps;
@ -137,6 +142,7 @@ export type TimelineItemType = (
| SafetyNumberNotificationType
| TimerNotificationType
| UniversalTimerNotificationType
| ContactRemovedNotificationType
| UnsupportedMessageType
| VerificationNotificationType
| PaymentEventType
@ -266,6 +272,13 @@ export class TimelineItem extends React.PureComponent<PropsType> {
);
} else if (item.type === 'universalTimerNotification') {
notification = renderUniversalTimerNotification();
} else if (item.type === 'contactRemovedNotification') {
notification = (
<SystemMessage
icon="info"
contents={i18n('icu:ContactRemovedNotification__text')}
/>
);
} else if (item.type === 'changeNumberNotification') {
notification = (
<ChangeNumberNotification

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { FunctionComponent } from 'react';
import React from 'react';
import React, { useMemo, useState } from 'react';
import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
import type { ConversationType } from '../../state/ducks/conversations';
@ -12,7 +12,11 @@ import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About';
import { ListTile } from '../ListTile';
import { Avatar, AvatarSize } from '../Avatar';
import { ContextMenu } from '../ContextMenu';
import { Intl } from '../Intl';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { isSignalConversation } from '../../util/isSignalConversation';
import { isInSystemContacts } from '../../util/isInSystemContacts';
export type ContactListItemConversationType = Pick<
ConversationType,
@ -23,10 +27,13 @@ export type ContactListItemConversationType = Pick<
| 'color'
| 'groupId'
| 'id'
| 'name'
| 'isMe'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'systemGivenName'
| 'systemFamilyName'
| 'title'
| 'type'
| 'unblurredAvatarPath'
@ -42,6 +49,11 @@ type PropsDataType = ContactListItemConversationType & {
type PropsHousekeepingType = {
i18n: LocalizerType;
onClick?: (id: string) => void;
onAudioCall?: (id: string) => void;
onVideoCall?: (id: string) => void;
onRemove?: (id: string) => void;
onBlock?: (id: string) => void;
hasContextMenu: boolean;
theme: ThemeType;
};
@ -54,19 +66,81 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
avatarPath,
badge,
color,
hasContextMenu,
i18n,
id,
isMe,
name,
onClick,
onAudioCall,
onVideoCall,
onRemove,
onBlock,
phoneNumber,
profileName,
sharedGroupNames,
systemGivenName,
systemFamilyName,
theme,
title,
type,
unblurredAvatarPath,
uuid,
}) {
const [isConfirmingBlocking, setConfirmingBlocking] = useState(false);
const [isConfirmingRemoving, setConfirmingRemoving] = useState(false);
const menuOptions = useMemo(
() => [
...(onClick
? [
{
icon: 'ContactListItem__context-menu__chat-icon',
label: i18n('icu:ContactListItem__menu__message'),
onClick: () => onClick(id),
},
]
: []),
...(onAudioCall
? [
{
icon: 'ContactListItem__context-menu__phone-icon',
label: i18n('icu:ContactListItem__menu__audio-call'),
onClick: () => onAudioCall(id),
},
]
: []),
...(onVideoCall
? [
{
icon: 'ContactListItem__context-menu__video-icon',
label: i18n('icu:ContactListItem__menu__video-call'),
onClick: () => onVideoCall(id),
},
]
: []),
...(onRemove
? [
{
icon: 'ContactListItem__context-menu__delete-icon',
label: i18n('icu:ContactListItem__menu__remove'),
onClick: () => setConfirmingRemoving(true),
},
]
: []),
...(onBlock
? [
{
icon: 'ContactListItem__context-menu__block-icon',
label: i18n('icu:ContactListItem__menu__block'),
onClick: () => setConfirmingBlocking(true),
},
]
: []),
],
[id, i18n, onClick, onAudioCall, onVideoCall, onRemove, onBlock]
);
const headerName = isMe ? (
<ContactName
isMe={isMe}
@ -84,32 +158,138 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
const messageText =
about && !isMe ? <About className="" text={about} /> : undefined;
return (
<ListTile
leading={
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
color={color}
conversationType={type}
noteToSelf={Boolean(isMe)}
let trailing: JSX.Element | undefined;
if (hasContextMenu) {
trailing = (
<ContextMenu
i18n={i18n}
menuOptions={menuOptions}
popperOptions={{ placement: 'bottom-start', strategy: 'absolute' }}
moduleClassName="ContactListItem__context-menu"
ariaLabel={i18n('icu:ContactListItem__menu')}
portalToRoot
/>
);
}
let blockConfirmation: JSX.Element | undefined;
let removeConfirmation: JSX.Element | undefined;
if (isConfirmingBlocking) {
blockConfirmation = (
<ConfirmationDialog
dialogName="ContactListItem.blocking"
i18n={i18n}
onClose={() => setConfirmingBlocking(false)}
title={
<Intl
i18n={i18n}
id="icu:MessageRequests--block-direct-confirm-title"
components={{
title: <ContactName key="name" title={title} />,
}}
/>
}
actions={[
{
text: i18n('icu:MessageRequests--block'),
action: () => onBlock?.(id),
style: 'negative',
},
]}
>
{i18n('icu:MessageRequests--block-direct-confirm-body')}
</ConfirmationDialog>
);
}
if (isConfirmingRemoving) {
if (
isInSystemContacts({ type, name, systemGivenName, systemFamilyName })
) {
removeConfirmation = (
<ConfirmationDialog
key="ContactListItem.systemContact"
dialogName="ContactListItem.systemContact"
i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath}
// This is here to appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })}
/>
}
title={headerName}
subtitle={messageText}
subtitleMaxLines={1}
onClick={onClick ? () => onClick(id) : undefined}
/>
onClose={() => setConfirmingRemoving(false)}
title={
<Intl
i18n={i18n}
id="icu:ContactListItem__remove-system--title"
components={{
title: <ContactName key="name" title={title} />,
}}
/>
}
cancelText={i18n('icu:Confirmation--confirm')}
>
{i18n('icu:ContactListItem__remove-system--body')}
</ConfirmationDialog>
);
} else {
removeConfirmation = (
<ConfirmationDialog
key="ContactListItem.removing"
dialogName="ContactListItem.removing"
i18n={i18n}
onClose={() => setConfirmingRemoving(false)}
title={
<Intl
i18n={i18n}
id="icu:ContactListItem__remove--title"
components={{
title: <ContactName key="name" title={title} />,
}}
/>
}
actions={[
{
text: i18n('icu:ContactListItem__remove--confirm'),
action: () => onRemove?.(id),
style: 'negative',
},
]}
>
{i18n('icu:ContactListItem__remove--body')}
</ConfirmationDialog>
);
}
}
return (
<>
<ListTile
moduleClassName="ContactListItem"
leading={
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
color={color}
conversationType={type}
noteToSelf={Boolean(isMe)}
i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath}
// This is here to appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })}
/>
}
trailing={trailing}
title={headerName}
subtitle={messageText}
subtitleMaxLines={1}
onClick={onClick ? () => onClick(id) : undefined}
/>
{blockConfirmation}
{removeConfirmation}
</>
);
}
);

View file

@ -53,6 +53,7 @@ export type PropsData = Pick<
| 'muteExpiresAt'
| 'phoneNumber'
| 'profileName'
| 'removalStage'
| 'sharedGroupNames'
| 'shouldShowDraft'
| 'title'
@ -92,6 +93,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
onClick,
phoneNumber,
profileName,
removalStage,
sharedGroupNames,
shouldShowDraft,
theme,
@ -125,7 +127,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
let messageText: ReactNode = null;
let messageStatusIcon: ReactNode = null;
if (!acceptedMessageRequest) {
if (!acceptedMessageRequest && removalStage !== 'justNotification') {
messageText = (
<span className={`${MESSAGE_TEXT_CLASS_NAME}__message-request`}>
{i18n('icu:ConversationListItem--message-request')}

View file

@ -205,6 +205,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
return {
type: RowType.Contact,
contact,
hasContextMenu: true,
};
}

8
ts/model-types.d.ts vendored
View file

@ -190,6 +190,7 @@ export type MessageAttributesType = {
| 'story'
| 'timer-notification'
| 'universal-timer-notification'
| 'contact-removed-notification'
| 'verified-change';
body?: string;
attachments?: Array<AttachmentType>;
@ -301,6 +302,12 @@ export type ConversationAttributesType = {
draftTimestamp?: number | null;
hideStory?: boolean;
inbox_position?: number;
// When contact is removed - it is initially placed into `justNotification`
// removal stage. In this stage user can still send messages (which will
// set `removalStage` to `undefined`), but if a new incoming message arrives -
// the stage will progress to `messageRequest` and composition area will be
// replaced with a message request.
removalStage?: 'justNotification' | 'messageRequest';
isPinned?: boolean;
lastMessageDeletedForEveryone?: boolean;
lastMessageStatus?: LastMessageStatus | null;
@ -361,6 +368,7 @@ export type ConversationAttributesType = {
verified?: number;
profileLastFetchedAt?: number;
pendingUniversalTimer?: string;
pendingRemovedContactNotification?: string;
username?: string;
shareMyPhoneNumber?: boolean;
previousIdentityKey?: string;

View file

@ -974,6 +974,79 @@ export class ConversationModel extends window.Backbone
return unblocked;
}
async removeContact({
viaStorageServiceSync = false,
shouldSave = true,
} = {}): Promise<void> {
const logId = `removeContact(${this.idForLogging()}) storage? ${viaStorageServiceSync}`;
if (!isDirectConversation(this.attributes)) {
log.warn(`${logId}: not direct conversation`);
return;
}
if (this.get('removalStage')) {
log.warn(`${logId}: already removed`);
return;
}
// Don't show message request state until first incoming message.
log.info(`${logId}: updating`);
this.set({ removalStage: 'justNotification' });
if (!viaStorageServiceSync) {
this.captureChange('removeContact');
}
this.disableProfileSharing({ viaStorageServiceSync });
// Drop existing message request state to avoid sending receipts and
// display MR actions.
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
await this.applyMessageRequestResponse(messageRequestEnum.UNKNOWN, {
viaStorageServiceSync,
shouldSave: false,
});
// Add notification
drop(this.queueJob('removeContact', () => this.maybeSetContactRemoved()));
if (shouldSave) {
await window.Signal.Data.updateConversation(this.attributes);
}
}
async restoreContact({
viaStorageServiceSync = false,
shouldSave = true,
} = {}): Promise<void> {
const logId = `restoreContact(${this.idForLogging()}) storage? ${viaStorageServiceSync}`;
if (!isDirectConversation(this.attributes)) {
log.warn(`${logId}: not direct conversation`);
return;
}
if (this.get('removalStage') === undefined) {
log.warn(`${logId}: not removed`);
return;
}
log.info(`${logId}: updating`);
this.set({ removalStage: undefined });
if (!viaStorageServiceSync) {
this.captureChange('restoreContact');
}
// Remove notification since the conversation isn't hidden anymore
await this.maybeClearContactRemoved();
if (shouldSave) {
await window.Signal.Data.updateConversation(this.attributes);
}
}
enableProfileSharing({ viaStorageServiceSync = false } = {}): void {
log.info(
`enableProfileSharing: ${this.idForLogging()} storage? ${viaStorageServiceSync}`
@ -1377,6 +1450,17 @@ export class ConversationModel extends window.Backbone
return;
}
// Change to message request state if contact was removed and sent message.
if (
this.get('removalStage') === 'justNotification' &&
isIncoming(message.attributes)
) {
this.set({
removalStage: 'messageRequest',
});
window.Signal.Data.updateConversation(this.attributes);
}
void this.addSingleMessage(message);
}
@ -1506,7 +1590,12 @@ export class ConversationModel extends window.Backbone
// oldest messages, to ensure that the ConversationHero is shown. We don't want to
// scroll directly to the oldest message, because that could scroll the hero off
// the screen.
if (!newestMessageId && !this.getAccepted() && metrics.oldest) {
if (
!newestMessageId &&
!this.getAccepted() &&
this.get('removalStage') !== 'justNotification' &&
metrics.oldest
) {
void this.loadAndScroll(metrics.oldest.id, { disableScroll: true });
return;
}
@ -1899,6 +1988,7 @@ export class ConversationModel extends window.Backbone
inboxPosition,
isArchived: this.get('isArchived'),
isBlocked: this.isBlocked(),
removalStage: this.get('removalStage'),
isMe: isMe(this.attributes),
isGroupV1AndDisabled: this.isGroupV1AndDisabled(),
isPinned: this.get('isPinned'),
@ -2259,7 +2349,7 @@ export class ConversationModel extends window.Backbone
async applyMessageRequestResponse(
response: number,
{ fromSync = false, viaStorageServiceSync = false } = {}
{ fromSync = false, viaStorageServiceSync = false, shouldSave = true } = {}
): Promise<void> {
try {
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
@ -2276,6 +2366,9 @@ export class ConversationModel extends window.Backbone
if (response === messageRequestEnum.ACCEPT) {
this.unblock({ viaStorageServiceSync });
if (!viaStorageServiceSync) {
await this.restoreContact({ shouldSave: false });
}
this.enableProfileSharing({ viaStorageServiceSync });
// We really don't want to call this if we don't have to. It can take a lot of
@ -2382,7 +2475,9 @@ export class ConversationModel extends window.Backbone
}
}
} finally {
window.Signal.Data.updateConversation(this.attributes);
if (shouldSave) {
window.Signal.Data.updateConversation(this.attributes);
}
}
}
@ -2628,10 +2723,13 @@ export class ConversationModel extends window.Backbone
}
}
async syncMessageRequestResponse(response: number): Promise<void> {
async syncMessageRequestResponse(
response: number,
{ shouldSave = true } = {}
): Promise<void> {
// In GroupsV2, this may modify the server. We only want to continue if those
// server updates were successful.
await this.applyMessageRequestResponse(response);
await this.applyMessageRequestResponse(response, { shouldSave });
const groupId = this.getGroupIdBuffer();
@ -3555,6 +3653,44 @@ export class ConversationModel extends window.Backbone
return true;
}
async maybeSetContactRemoved(): Promise<void> {
if (!isDirectConversation(this.attributes)) {
return;
}
if (this.get('pendingRemovedContactNotification')) {
return;
}
log.info(
`maybeSetContactRemoved(${this.idForLogging()}): added notification`
);
const notificationId = await this.addNotification(
'contact-removed-notification'
);
this.set('pendingRemovedContactNotification', notificationId);
await window.Signal.Data.updateConversation(this.attributes);
}
async maybeClearContactRemoved(): Promise<boolean> {
const notificationId = this.get('pendingRemovedContactNotification');
if (!notificationId) {
return false;
}
this.set('pendingRemovedContactNotification', undefined);
log.info(
`maybeClearContactRemoved(${this.idForLogging()}): removed notification`
);
const message = window.MessageController.getById(notificationId);
if (message) {
await window.Signal.Data.removeMessage(message.id);
}
return true;
}
async addChangeNumberNotification(
oldValue: string,
newValue: string
@ -4010,6 +4146,7 @@ export class ConversationModel extends window.Backbone
if (!storyId || isDirectConversation(this.attributes)) {
await this.maybeApplyUniversalTimer();
expireTimer = this.get('expireTimer');
await this.restoreContact();
}
const recipientMaybeConversations = map(

View file

@ -98,6 +98,7 @@ import {
hasErrors,
isCallHistory,
isChatSessionRefreshed,
isContactRemovedNotification,
isDeliveryIssue,
isEndSession,
isExpirationTimerUpdate,
@ -341,6 +342,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return (
!isCallHistory(attributes) &&
!isChatSessionRefreshed(attributes) &&
!isContactRemovedNotification(attributes) &&
!isConversationMerge(attributes) &&
!isEndSession(attributes) &&
!isExpirationTimerUpdate(attributes) &&

View file

@ -203,6 +203,7 @@ export async function toContactRecord(
contactRecord.systemNickname = systemNickname;
}
contactRecord.blocked = conversation.isBlocked();
contactRecord.hidden = conversation.get('removalStage') !== undefined;
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
contactRecord.archived = Boolean(conversation.get('isArchived'));
contactRecord.markedUnread = Boolean(conversation.get('markedUnread'));
@ -1083,6 +1084,18 @@ export async function mergeContactRecord(
storageVersion,
});
if (contactRecord.hidden) {
await conversation.removeContact({
viaStorageServiceSync: true,
shouldSave: false,
});
} else {
await conversation.restoreContact({
viaStorageServiceSync: true,
shouldSave: false,
});
}
conversation.setMuteExpiration(
getTimestampFromLong(contactRecord.mutedUntilTimestamp),
{

View file

@ -0,0 +1,118 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion81(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 81) {
return;
}
db.transaction(() => {
db.exec(
`
--- These will be re-added below
DROP INDEX messages_preview;
DROP INDEX messages_preview_without_story;
DROP INDEX messages_activity;
DROP INDEX message_user_initiated;
--- These will also be re-added below
ALTER TABLE messages DROP COLUMN shouldAffectActivity;
ALTER TABLE messages DROP COLUMN shouldAffectPreview;
ALTER TABLE messages DROP COLUMN isUserInitiatedMessage;
--- Note: These generated columns were previously modified in
--- migration 73, and are mostly the same
--- (change: added contact-removed-notification)
ALTER TABLE messages
ADD COLUMN shouldAffectActivity INTEGER
GENERATED ALWAYS AS (
type IS NULL
OR
type NOT IN (
'change-number-notification',
'contact-removed-notification',
'conversation-merge',
'group-v1-migration',
'keychange',
'message-history-unsynced',
'profile-change',
'story',
'universal-timer-notification',
'verified-change'
)
);
--- (change: added contact-removed-notification)
ALTER TABLE messages
ADD COLUMN shouldAffectPreview INTEGER
GENERATED ALWAYS AS (
type IS NULL
OR
type NOT IN (
'change-number-notification',
'contact-removed-notification',
'conversation-merge',
'group-v1-migration',
'keychange',
'message-history-unsynced',
'profile-change',
'story',
'universal-timer-notification',
'verified-change'
)
);
--- (change: added contact-removed-notification)
ALTER TABLE messages
ADD COLUMN isUserInitiatedMessage INTEGER
GENERATED ALWAYS AS (
type IS NULL
OR
type NOT IN (
'change-number-notification',
'contact-removed-notification',
'conversation-merge',
'group-v1-migration',
'group-v2-change',
'keychange',
'message-history-unsynced',
'profile-change',
'story',
'universal-timer-notification',
'verified-change'
)
);
--- From migration 76
CREATE INDEX messages_preview ON messages
(conversationId, shouldAffectPreview, isGroupLeaveEventFromOther,
received_at, sent_at);
--- From migration 76
CREATE INDEX messages_preview_without_story ON messages
(conversationId, shouldAffectPreview, isGroupLeaveEventFromOther,
received_at, sent_at) WHERE storyId IS NULL;
--- From migration 73
CREATE INDEX messages_activity ON messages
(conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at);
--- From migration 74
CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage);
`
);
db.pragma('user_version = 81');
})();
logger.info('updateToSchemaVersion81: success!');
}

View file

@ -56,6 +56,7 @@ import updateToSchemaVersion77 from './77-signal-tokenizer';
import updateToSchemaVersion78 from './78-merge-receipt-jobs';
import updateToSchemaVersion79 from './79-paging-lightbox';
import updateToSchemaVersion80 from './80-edited-messages';
import updateToSchemaVersion81 from './81-contact-removed-notification';
function updateToSchemaVersion1(
currentVersion: number,
@ -1982,6 +1983,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion79,
updateToSchemaVersion80,
updateToSchemaVersion81,
];
export function updateSchema(db: Database, logger: LoggerType): void {

View file

@ -226,6 +226,7 @@ export type ConversationType = ReadonlyDeep<
hideStory?: boolean;
isArchived?: boolean;
isBlocked?: boolean;
removalStage?: 'justNotification' | 'messageRequest';
isGroupV1AndDisabled?: boolean;
isPinned?: boolean;
isUntrusted?: boolean;
@ -1009,6 +1010,7 @@ export const actions = {
popPanelForConversation,
pushPanelForConversation,
removeAllConversations,
removeConversation,
removeCustomColorOnConversations,
removeMember,
removeMemberFromGroup,
@ -3064,6 +3066,27 @@ function acceptConversation(conversationId: string): NoopActionType {
};
}
function removeConversation(conversationId: string): ShowToastActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
'acceptConversation: Expected a conversation to be found. Doing nothing'
);
}
drop(conversation.removeContact());
return {
type: SHOW_TOAST,
payload: {
toastType: ToastType.ConversationRemoved,
parameters: {
title: conversation.getTitle(),
},
},
};
}
function blockConversation(conversationId: string): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {

View file

@ -471,6 +471,7 @@ function canComposeConversation(conversation: ConversationType): boolean {
return Boolean(
!isSignalConversation(conversation) &&
!conversation.isBlocked &&
!conversation.removalStage &&
!isConversationUnregistered(conversation) &&
hasDisplayInfo(conversation) &&
isTrusted(conversation)
@ -484,6 +485,7 @@ export const getAllComposableConversations = createSelector(
conversation =>
!isSignalConversation(conversation) &&
!conversation.isBlocked &&
!conversation.removalStage &&
!conversation.isGroupV1AndDisabled &&
!isConversationUnregistered(conversation) &&
// All conversation should have a title except in weird cases where

View file

@ -124,6 +124,27 @@ export const getStoriesEnabled = createSelector(
}
);
export const getContactManagementEnabled = createSelector(
getRemoteConfig,
(remoteConfig: ConfigMapType): boolean => {
if (isRemoteConfigFlagEnabled(remoteConfig, 'desktop.contactManagement')) {
return true;
}
if (
isRemoteConfigFlagEnabled(
remoteConfig,
'desktop.contactManagement.beta'
) &&
isBeta(window.getVersion())
) {
return true;
}
return false;
}
);
export const getDefaultConversationColor = createSelector(
getItems,
(

View file

@ -871,6 +871,13 @@ export function getPropsForBubble(
timestamp,
};
}
if (isContactRemovedNotification(message)) {
return {
type: 'contactRemovedNotification',
data: null,
timestamp,
};
}
if (isChangeNumberNotification(message)) {
return {
type: 'changeNumberNotification',
@ -1374,6 +1381,16 @@ export function isUniversalTimerNotification(
return message.type === 'universal-timer-notification';
}
// Contact Removed Notification
// Note: smart, so props not generated here
export function isContactRemovedNotification(
message: MessageWithUIFieldsType
): boolean {
return message.type === 'contact-removed-notification';
}
// Change Number Notification
export function isChangeNumberNotification(

View file

@ -40,6 +40,7 @@ import { hasNetworkDialog } from '../selectors/network';
import {
getPreferredLeftPaneWidth,
getUsernamesEnabled,
getContactManagementEnabled,
} from '../selectors/items';
import {
getComposeAvatarData,
@ -233,6 +234,7 @@ const mapStateToProps = (state: StateType) => {
targetedMessageId: getTargetedMessage(state)?.id,
showArchived: getShowArchived(state),
getPreferredBadge: getPreferredBadgeSelector(state),
isContactManagementEnabled: getContactManagementEnabled(state),
i18n: getIntl(state),
isMacOS: getIsMacOS(state),
regionCode: getRegionCode(state),

View file

@ -227,10 +227,12 @@ describe('LeftPaneComposeHelper', () => {
assert.deepEqual(helper.getRow(2), {
type: RowType.Contact,
contact: composeContacts[0],
hasContextMenu: true,
});
assert.deepEqual(helper.getRow(3), {
type: RowType.Contact,
contact: composeContacts[1],
hasContextMenu: true,
});
});
@ -259,10 +261,12 @@ describe('LeftPaneComposeHelper', () => {
assert.deepEqual(helper.getRow(2), {
type: RowType.Contact,
contact: composeContacts[0],
hasContextMenu: true,
});
assert.deepEqual(helper.getRow(3), {
type: RowType.Contact,
contact: composeContacts[1],
hasContextMenu: true,
});
assert.deepEqual(_testHeaderText(helper.getRow(4)), 'icu:groupsHeader');
assert.deepEqual(helper.getRow(5), {
@ -306,10 +310,12 @@ describe('LeftPaneComposeHelper', () => {
assert.deepEqual(helper.getRow(1), {
type: RowType.Contact,
contact: composeContacts[0],
hasContextMenu: true,
});
assert.deepEqual(helper.getRow(2), {
type: RowType.Contact,
contact: composeContacts[1],
hasContextMenu: true,
});
});
@ -383,10 +389,12 @@ describe('LeftPaneComposeHelper', () => {
assert.deepEqual(helper.getRow(1), {
type: RowType.Contact,
contact: composeContacts[0],
hasContextMenu: true,
});
assert.deepEqual(helper.getRow(2), {
type: RowType.Contact,
contact: composeContacts[1],
hasContextMenu: true,
});
assert.deepEqual(
_testHeaderText(helper.getRow(3)),

View file

@ -14,6 +14,7 @@ export enum ToastType {
CannotStartGroupCall = 'CannotStartGroupCall',
ConversationArchived = 'ConversationArchived',
ConversationMarkedUnread = 'ConversationMarkedUnread',
ConversationRemoved = 'ConversationRemoved',
ConversationUnarchived = 'ConversationUnarchived',
CopiedUsername = 'CopiedUsername',
CopiedUsernameLink = 'CopiedUsernameLink',

View file

@ -28,7 +28,11 @@ export function isConversationAccepted(
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
const { messageRequestResponseType } = conversationAttrs;
const { messageRequestResponseType, removalStage } = conversationAttrs;
if (removalStage !== undefined) {
return false;
}
if (messageRequestResponseType === messageRequestEnum.ACCEPT) {
return true;
}