From f61d8f38b0f0101f8de9a2e00b6e59831ecb5132 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:48:00 -0700 Subject: [PATCH] Context menu for left pane list items --- _locales/en/messages.json | 56 +++++ images/icons/v3/block--compact.svg | 10 + images/icons/v3/chat--compact.svg | 3 + images/icons/v3/minus--circle--compact.svg | 11 + images/icons/v3/phone--compact.svg | 3 + images/icons/v3/video--compact.svg | 3 + protos/SignalStorage.proto | 1 + stylesheets/_modules.scss | 2 +- stylesheets/components/ContactListItem.scss | 129 ++++++++++ stylesheets/components/ContextMenu.scss | 6 +- stylesheets/manifest.scss | 1 + ts/RemoteConfig.ts | 2 + ts/components/CompositionArea.tsx | 9 +- ts/components/ContextMenu.tsx | 20 +- ts/components/ConversationList.stories.tsx | 26 ++ ts/components/ConversationList.tsx | 31 ++- ts/components/ForwardMessagesModal.tsx | 4 + ts/components/LeftPane.stories.tsx | 103 ++++---- ts/components/LeftPane.tsx | 20 ++ ts/components/ListTile.tsx | 6 +- ts/components/StoriesSettingsModal.tsx | 4 + ts/components/ToastManager.stories.tsx | 10 + ts/components/ToastManager.tsx | 10 + .../conversation/MessageRequestActions.tsx | 62 +++-- .../conversation/TimelineItem.stories.tsx | 4 + ts/components/conversation/TimelineItem.tsx | 13 + .../conversationList/ContactListItem.tsx | 232 ++++++++++++++++-- .../conversationList/ConversationListItem.tsx | 4 +- .../leftPane/LeftPaneComposeHelper.tsx | 1 + ts/model-types.d.ts | 8 + ts/models/conversations.ts | 147 ++++++++++- ts/models/messages.ts | 2 + ts/services/storageRecordOps.ts | 13 + .../81-contact-removed-notification.ts | 118 +++++++++ ts/sql/migrations/index.ts | 2 + ts/state/ducks/conversations.ts | 23 ++ ts/state/selectors/conversations.ts | 2 + ts/state/selectors/items.ts | 21 ++ ts/state/selectors/message.ts | 17 ++ ts/state/smart/LeftPane.tsx | 2 + .../leftPane/LeftPaneComposeHelper_test.ts | 8 + ts/types/Toast.tsx | 1 + ts/util/isConversationAccepted.ts | 6 +- 43 files changed, 1046 insertions(+), 110 deletions(-) create mode 100644 images/icons/v3/block--compact.svg create mode 100644 images/icons/v3/chat--compact.svg create mode 100644 images/icons/v3/minus--circle--compact.svg create mode 100644 images/icons/v3/phone--compact.svg create mode 100644 images/icons/v3/video--compact.svg create mode 100644 stylesheets/components/ContactListItem.scss create mode 100644 ts/sql/migrations/81-contact-removed-notification.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 373ce7cd5b2..d559de72cd8 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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 won’t see this person when searching. You’ll 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 device’s 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 won’t know you’ve 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" diff --git a/images/icons/v3/block--compact.svg b/images/icons/v3/block--compact.svg new file mode 100644 index 00000000000..91382b94eb5 --- /dev/null +++ b/images/icons/v3/block--compact.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/images/icons/v3/chat--compact.svg b/images/icons/v3/chat--compact.svg new file mode 100644 index 00000000000..101342adea1 --- /dev/null +++ b/images/icons/v3/chat--compact.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/v3/minus--circle--compact.svg b/images/icons/v3/minus--circle--compact.svg new file mode 100644 index 00000000000..fc6fc8efc76 --- /dev/null +++ b/images/icons/v3/minus--circle--compact.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/images/icons/v3/phone--compact.svg b/images/icons/v3/phone--compact.svg new file mode 100644 index 00000000000..79b6a0dbaeb --- /dev/null +++ b/images/icons/v3/phone--compact.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/icons/v3/video--compact.svg b/images/icons/v3/video--compact.svg new file mode 100644 index 00000000000..4477eb00c99 --- /dev/null +++ b/images/icons/v3/video--compact.svg @@ -0,0 +1,3 @@ + + + diff --git a/protos/SignalStorage.proto b/protos/SignalStorage.proto index 8d2ce79da76..9b870c80b03 100644 --- a/protos/SignalStorage.proto +++ b/protos/SignalStorage.proto @@ -96,6 +96,7 @@ message ContactRecord { optional string systemGivenName = 17; optional string systemFamilyName = 18; optional string systemNickname = 19; + optional bool hidden = 20; } message GroupV1Record { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 9f469b5ed5a..b07a0d4cfcb 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; diff --git a/stylesheets/components/ContactListItem.scss b/stylesheets/components/ContactListItem.scss new file mode 100644 index 00000000000..f566f5e55ff --- /dev/null +++ b/stylesheets/components/ContactListItem.scss @@ -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 + ); + } + } + } + } +} diff --git a/stylesheets/components/ContextMenu.scss b/stylesheets/components/ContextMenu.scss index 4967c3a85c4..de2d777c6e6 100644 --- a/stylesheets/components/ContextMenu.scss +++ b/stylesheets/components/ContextMenu.scss @@ -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; } diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 64be9d28577..1279dc7b4f4 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -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'; diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 179ae6ffff1..a3e3387aeeb 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -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' diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index b8d1a4d77e8..1c061a079e0 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -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 ( ); @@ -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 ) { diff --git a/ts/components/ContextMenu.tsx b/ts/components/ContextMenu.tsx index d399a0ab67c..b9af3906a88 100644 --- a/ts/components/ContextMenu.tsx +++ b/ts/components/ContextMenu.tsx @@ -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 = Readonly<{ onClick?: (ev: React.MouseEvent) => unknown; onMenuShowingChanged?: (value: boolean) => unknown; popperOptions?: Pick; + portalToRoot?: boolean; theme?: Theme; title?: string; value?: T; @@ -67,6 +69,7 @@ export function ContextMenu({ onClick, onMenuShowingChanged, popperOptions, + portalToRoot, theme, title, value, @@ -118,6 +121,21 @@ export function ContextMenu({ ); }, [isMenuShowing, referenceElement, popperElement]); + const [portalNode, setPortalNode] = React.useState(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({ > {children} - {menuNode} + {portalNode ? createPortal(menuNode, portalNode) : menuNode} ); } diff --git a/ts/components/ConversationList.stories.tsx b/ts/components/ConversationList.stories.tsx index 17d8086dbff..18d86982adb 100644 --- a/ts/components/ConversationList.stories.tsx +++ b/ts/components/ConversationList.stories.tsx @@ -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) => ( + ); +} + +ContactDirectWithContextMenu.story = { + name: 'Contact: context menu', +}; + export function ContactDirectWithShortAbout(): JSX.Element { return ( 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 = ( ); 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, ] ); diff --git a/ts/components/ForwardMessagesModal.tsx b/ts/components/ForwardMessagesModal.tsx index af2ee0a403c..e94ca58fa33 100644 --- a/ts/components/ForwardMessagesModal.tsx +++ b/ts/components/ForwardMessagesModal.tsx @@ -356,6 +356,10 @@ export function ForwardMessagesModal({ showUserNotFoundModal={shouldNeverBeCalled} setIsFetchingUUID={shouldNeverBeCalled} onSelectConversation={shouldNeverBeCalled} + blockConversation={shouldNeverBeCalled} + removeConversation={shouldNeverBeCalled} + onOutgoingAudioCallInConversation={shouldNeverBeCalled} + onOutgoingVideoCallInConversation={shouldNeverBeCalled} renderMessageSearchResult={() => { shouldNeverBeCalled(); return
; diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index b0371aa24fc..9d5ca9eaa32 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -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: () =>
, renderMessageSearchResult: (id: string) => ( { }; }; +function LeftPaneInContainer(props: PropsType): JSX.Element { + return ( +
+ +
+ ); +} + export function InboxNoConversations(): JSX.Element { return ( - ; + return ; } InboxPinnedNonPinnedAndArchivedConversations.story = { @@ -425,7 +446,7 @@ InboxPinnedNonPinnedAndArchivedConversations.story = { export function SearchNoResultsWhenSearchingEverywhere(): JSX.Element { return ( - ( - 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} diff --git a/ts/components/ListTile.tsx b/ts/components/ListTile.tsx index 2b07e33ff96..51b0e5a9253 100644 --- a/ts/components/ListTile.tsx +++ b/ts/components/ListTile.tsx @@ -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) => 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( subtitle, leading, trailing, + moduleClassName, onClick, onContextMenu, clickable, @@ -85,6 +85,8 @@ const ListTileImpl = React.forwardRef( ) { const isClickable = clickable ?? Boolean(onClick); + const getClassName = getClassNamesFor('ListTile', moduleClassName); + const rootProps = { className: classNames( getClassName(''), diff --git a/ts/components/StoriesSettingsModal.tsx b/ts/components/StoriesSettingsModal.tsx index f8551cf1caf..a76648658b6 100644 --- a/ts/components/StoriesSettingsModal.tsx +++ b/ts/components/StoriesSettingsModal.tsx @@ -1209,6 +1209,10 @@ export function EditDistributionListModal({ toggleSelectedConversation(conversationId); }} onSelectConversation={shouldNeverBeCalled} + blockConversation={shouldNeverBeCalled} + removeConversation={shouldNeverBeCalled} + onOutgoingAudioCallInConversation={shouldNeverBeCalled} + onOutgoingVideoCallInConversation={shouldNeverBeCalled} renderMessageSearchResult={() => { shouldNeverBeCalled(); return
; diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 0f7ab99daba..8f7a8666e71 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -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: { diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index 3aea4c4ab63..c64bc7ecc37 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -134,6 +134,16 @@ export function ToastManager({ ); } + if (toastType === ToastType.ConversationRemoved) { + return ( + + {i18n('icu:Toast--ConversationRemoved', { + title: toast?.parameters?.title ?? '', + })} + + ); + } + if (toastType === ToastType.ConversationUnarchived) { return ( diff --git a/ts/components/conversation/MessageRequestActions.tsx b/ts/components/conversation/MessageRequestActions.tsx index a7dd60c8877..0eac3850e25 100644 --- a/ts/components/conversation/MessageRequestActions.tsx +++ b/ts/components/conversation/MessageRequestActions.tsx @@ -15,6 +15,7 @@ import type { LocalizerType } from '../../types/Util'; export type Props = { i18n: LocalizerType; + isHidden?: boolean; } & Omit & 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({ ); + let message: JSX.Element | undefined; + if (conversationType === 'direct') { + if (isBlocked) { + message = ( + + ); + } else if (isHidden) { + message = ( + + ); + } else { + message = ( + + ); + } + } else if (conversationType === 'group') { + if (isBlocked) { + message = ( + + ); + } else { + message = ; + } + } + return ( <> {mrState !== MessageRequestState.default ? ( @@ -61,28 +100,7 @@ export function MessageRequestActions({ /> ) : null}
-

- {conversationType === 'direct' && isBlocked && ( - - )} - {conversationType === 'direct' && !isBlocked && ( - - )} - {conversationType === 'group' && isBlocked && ( - - )} - {conversationType === 'group' && !isBlocked && ( - - )} -

+

{message}