From 302604f67e4ef9ed085859cdc1557b48d2a87eb2 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 11 Apr 2022 17:26:09 -0700 Subject: [PATCH] Restore ability to message someone from embedded contact --- stylesheets/_modules.scss | 13 +- .../conversation/EmbeddedContact.stories.tsx | 179 ------------------ .../conversation/Message.stories.tsx | 141 ++++++++++++++ ts/components/conversation/Message.tsx | 58 ++++-- .../conversation/MessageDetail.stories.tsx | 1 + ts/components/conversation/MessageDetail.tsx | 3 + ts/components/conversation/Quote.stories.tsx | 1 + .../conversation/Timeline.stories.tsx | 1 + ts/components/conversation/Timeline.tsx | 1 + .../conversation/TimelineItem.stories.tsx | 1 + ts/state/ducks/accounts.ts | 65 +++++-- ts/state/selectors/accounts.ts | 9 +- ts/state/selectors/message.ts | 4 +- ts/state/smart/MessageDetail.tsx | 2 + ts/state/smart/Timeline.tsx | 1 + ts/test-node/types/EmbeddedContact_test.ts | 17 +- ts/types/EmbeddedContact.ts | 14 +- ts/views/conversation_view.ts | 36 +++- 18 files changed, 311 insertions(+), 236 deletions(-) delete mode 100644 ts/components/conversation/EmbeddedContact.stories.tsx diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e4a24f33d59..a9003c1c672 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1207,15 +1207,15 @@ @include font-body-2-bold; margin-top: 8px; - margin-bottom: -10px; + margin-bottom: -8px; margin-left: -12px; margin-right: -12px; text-align: center; padding: 10px; - border-bottom-left-radius: 16px; - border-bottom-right-radius: 16px; + border-bottom-left-radius: 18px; + border-bottom-right-radius: 18px; @include light-theme { color: $color-ultramarine; @@ -1235,6 +1235,13 @@ } } +.module-message__send-message-button--no-bottom-left-curve { + border-bottom-left-radius: 4px; +} +.module-message__send-message-button--no-bottom-right-curve { + border-bottom-right-radius: 4px; +} + .module-message__author-avatar-container { align-items: flex-end; display: flex; diff --git a/ts/components/conversation/EmbeddedContact.stories.tsx b/ts/components/conversation/EmbeddedContact.stories.tsx deleted file mode 100644 index 0dff799fe54..00000000000 --- a/ts/components/conversation/EmbeddedContact.stories.tsx +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; - -import { action } from '@storybook/addon-actions'; -import { boolean, number } from '@storybook/addon-knobs'; -import { storiesOf } from '@storybook/react'; - -import type { Props } from './EmbeddedContact'; -import { EmbeddedContact } from './EmbeddedContact'; -import { setupI18n } from '../../util/setupI18n'; -import enMessages from '../../../_locales/en/messages.json'; -import { ContactFormType } from '../../types/EmbeddedContact'; -import { IMAGE_GIF } from '../../types/MIME'; - -import { fakeAttachment } from '../../test-both/helpers/fakeAttachment'; - -const i18n = setupI18n('en', enMessages); - -const story = storiesOf('Components/Conversation/EmbeddedContact', module); - -const createProps = (overrideProps: Partial = {}): Props => ({ - contact: overrideProps.contact || {}, - i18n, - isIncoming: boolean('isIncoming', overrideProps.isIncoming || false), - onClick: action('onClick'), - tabIndex: number('tabIndex', overrideProps.tabIndex || 0), - withContentAbove: boolean( - 'withContentAbove', - overrideProps.withContentAbove || false - ), - withContentBelow: boolean( - 'withContentBelow', - overrideProps.withContentBelow || false - ), -}); - -const fullContact = { - avatar: { - avatar: fakeAttachment({ - path: '/fixtures/giphy-GVNvOUpeYmI7e.gif', - contentType: IMAGE_GIF, - }), - isProfile: true, - }, - email: [ - { - value: 'jerjor@fakemail.com', - type: ContactFormType.HOME, - }, - ], - name: { - givenName: 'Jerry', - familyName: 'Jordan', - prefix: 'Dr.', - suffix: 'Jr.', - middleName: 'James', - displayName: 'Jerry Jordan', - }, - number: [ - { - value: '555-444-2323', - type: ContactFormType.HOME, - }, - ], -}; - -story.add('Full Contact', () => { - const props = createProps({ - contact: fullContact, - }); - return ; -}); - -story.add('Only Email', () => { - const props = createProps({ - contact: { - email: fullContact.email, - }, - }); - - return ; -}); - -story.add('Given Name', () => { - const props = createProps({ - contact: { - name: { - givenName: 'Jerry', - }, - }, - }); - - return ; -}); - -story.add('Organization', () => { - const props = createProps({ - contact: { - organization: 'Company 5', - }, - }); - - return ; -}); - -story.add('Given + Family Name', () => { - const props = createProps({ - contact: { - name: { - givenName: 'Jerry', - familyName: 'FamilyName', - }, - }, - }); - - return ; -}); - -story.add('Family Name', () => { - const props = createProps({ - contact: { - name: { - familyName: 'FamilyName', - }, - }, - }); - - return ; -}); - -story.add('Loading Avatar', () => { - const props = createProps({ - contact: { - name: { - displayName: 'Jerry Jordan', - }, - avatar: { - avatar: fakeAttachment({ - pending: true, - contentType: IMAGE_GIF, - }), - isProfile: true, - }, - }, - }); - return ; -}); - -story.add('Incoming', () => { - const props = createProps({ - contact: { - name: fullContact.name, - }, - isIncoming: true, - }); - - // Wrapped in a
to provide a background for light color of text - return ( -
- -
- ); -}); - -story.add('Content Above and Below', () => { - const props = createProps({ - withContentAbove: true, - withContentBelow: true, - }); - return ( - <> -
Content Above
- -
Content Below
- - ); -}); diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index 7a268100d01..ad9965c2321 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -20,6 +20,7 @@ import { IMAGE_WEBP, VIDEO_MP4, stringToMIMEType, + IMAGE_GIF, } from '../../types/MIME'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { MessageAudio } from './MessageAudio'; @@ -30,6 +31,7 @@ import { pngUrl } from '../../storybook/Fixtures'; import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation'; import { WidthBreakpoint } from '../_util'; import { MINUTE } from '../../util/durations'; +import { ContactFormType } from '../../types/EmbeddedContact'; import { fakeAttachment, @@ -37,6 +39,7 @@ import { } from '../../test-both/helpers/fakeAttachment'; import { getFakeBadge } from '../../test-both/helpers/getFakeBadge'; import { ThemeType } from '../../types/Util'; +import { UUID } from '../../types/UUID'; const i18n = setupI18n('en', enMessages); @@ -118,6 +121,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ select('conversationColor', ConversationColors, ConversationColors[0]), conversationId: text('conversationId', overrideProps.conversationId || ''), conversationType: overrideProps.conversationType || 'direct', + contact: overrideProps.contact, deletedForEveryone: overrideProps.deletedForEveryone, deleteMessage: action('deleteMessage'), deleteMessageForEveryone: action('deleteMessageForEveryone'), @@ -191,6 +195,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ showForwardMessageModal: action('showForwardMessageModal'), showMessageDetail: action('showMessageDetail'), showVisualAttachment: action('showVisualAttachment'), + startConversation: action('startConversation'), status: overrideProps.status || 'sent', text: overrideProps.text || text('text', ''), textDirection: overrideProps.textDirection || TextDirection.Default, @@ -1516,3 +1521,139 @@ story.add('Story reply', () => { /> ); }); + +const fullContact = { + avatar: { + avatar: fakeAttachment({ + path: '/fixtures/giphy-GVNvOUpeYmI7e.gif', + contentType: IMAGE_GIF, + }), + isProfile: true, + }, + email: [ + { + value: 'jerjor@fakemail.com', + type: ContactFormType.HOME, + }, + ], + name: { + givenName: 'Jerry', + familyName: 'Jordan', + prefix: 'Dr.', + suffix: 'Jr.', + middleName: 'James', + displayName: 'Jerry Jordan', + }, + number: [ + { + value: '555-444-2323', + type: ContactFormType.HOME, + }, + ], +}; + +story.add('EmbeddedContact: Full Contact', () => { + const props = createProps({ + contact: fullContact, + }); + return renderBothDirections(props); +}); + +story.add('EmbeddedContact: 2x Incoming, with Send Message', () => { + const props = createProps({ + contact: { + ...fullContact, + firstNumber: fullContact.number[0].value, + uuid: UUID.generate().toString(), + }, + direction: 'incoming', + }); + return renderMany([props, props]); +}); + +story.add('EmbeddedContact: 2x Outgoing, with Send Message', () => { + const props = createProps({ + contact: { + ...fullContact, + firstNumber: fullContact.number[0].value, + uuid: UUID.generate().toString(), + }, + direction: 'outgoing', + }); + return renderMany([props, props]); +}); + +story.add('EmbeddedContact: Only Email', () => { + const props = createProps({ + contact: { + email: fullContact.email, + }, + }); + + return renderBothDirections(props); +}); + +story.add('EmbeddedContact: Given Name', () => { + const props = createProps({ + contact: { + name: { + givenName: 'Jerry', + }, + }, + }); + + return renderBothDirections(props); +}); + +story.add('EmbeddedContact: Organization', () => { + const props = createProps({ + contact: { + organization: 'Company 5', + }, + }); + + return renderBothDirections(props); +}); + +story.add('EmbeddedContact: Given + Family Name', () => { + const props = createProps({ + contact: { + name: { + givenName: 'Jerry', + familyName: 'FamilyName', + }, + }, + }); + + return renderBothDirections(props); +}); + +story.add('EmbeddedContact: Family Name', () => { + const props = createProps({ + contact: { + name: { + familyName: 'FamilyName', + }, + }, + }); + + return renderBothDirections(props); +}); + +story.add('EmbeddedContact: Loading Avatar', () => { + const props = createProps({ + contact: { + name: { + displayName: 'Jerry Jordan', + }, + avatar: { + avatar: fakeAttachment({ + pending: true, + contentType: IMAGE_GIF, + }), + isProfile: true, + }, + }, + }); + return renderBothDirections(props); +}); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 2a9acb74303..774181fde66 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -83,6 +83,7 @@ import { getCustomColorStyle } from '../../util/getCustomColorStyle'; import { offsetDistanceModifier } from '../../util/popperUtil'; import * as KeyboardLayout from '../../services/keyboardLayout'; import { StopPropagation } from '../StopPropagation'; +import type { UUIDStringType } from '../../types/UUID'; type Trigger = { handleContextClick: (event: React.MouseEvent) => void; @@ -279,7 +280,7 @@ export type PropsActions = { clearSelectedMessage: () => unknown; doubleCheckMissingQuoteReference: (messageId: string) => unknown; messageExpanded: (id: string, displayLimit: number) => unknown; - checkForAccount: (identifier: string) => unknown; + checkForAccount: (phoneNumber: string) => unknown; reactToMessage: ( id: string, @@ -293,10 +294,14 @@ export type PropsActions = { deleteMessageForEveryone: (id: string) => void; showMessageDetail: (id: string) => void; + startConversation: (e164: string, uuid: UUIDStringType) => void; openConversation: (conversationId: string, messageId?: string) => void; showContactDetail: (options: { contact: EmbeddedContactType; - signalAccount?: string; + signalAccount?: { + phoneNumber: string; + uuid: UUIDStringType; + }; }) => void; showContactModal: (contactId: string, conversationId?: string) => void; @@ -501,7 +506,7 @@ export class Message extends React.PureComponent { } const { contact, checkForAccount } = this.props; - if (contact && contact.firstNumber && !contact.isNumberOnSignal) { + if (contact && contact.firstNumber && !contact.uuid) { checkForAccount(contact.firstNumber); } } @@ -1336,8 +1341,7 @@ export class Message extends React.PureComponent { this.getMetadataPlacement() !== MetadataPlacement.NotRendered; const otherContent = - (contact && contact.firstNumber && contact.isNumberOnSignal) || - withCaption; + (contact && contact.firstNumber && contact.uuid) || withCaption; const tabIndex = otherContent ? 0 : -1; return ( @@ -1346,7 +1350,18 @@ export class Message extends React.PureComponent { isIncoming={direction === 'incoming'} i18n={i18n} onClick={() => { - showContactDetail({ contact, signalAccount: contact.firstNumber }); + const signalAccount = + contact.firstNumber && contact.uuid + ? { + phoneNumber: contact.firstNumber, + uuid: contact.uuid, + } + : undefined; + + showContactDetail({ + contact, + signalAccount, + }); }} withContentAbove={withContentAbove} withContentBelow={withContentBelow} @@ -1356,20 +1371,30 @@ export class Message extends React.PureComponent { } public renderSendMessageButton(): JSX.Element | null { - const { contact, openConversation, i18n } = this.props; + const { contact, direction, shouldCollapseBelow, startConversation, i18n } = + this.props; + const noBottomLeftCurve = direction === 'incoming' && shouldCollapseBelow; + const noBottomRightCurve = direction === 'outgoing' && shouldCollapseBelow; + if (!contact) { return null; } - const { firstNumber, isNumberOnSignal } = contact; - if (!firstNumber || !isNumberOnSignal) { + const { firstNumber, uuid } = contact; + if (!firstNumber || !uuid) { return null; } return ( @@ -2484,7 +2509,7 @@ export class Message extends React.PureComponent { this.audioButtonRef.current.click(); } - if (contact && contact.firstNumber && contact.isNumberOnSignal) { + if (contact && contact.firstNumber && contact.uuid) { openConversation(contact.firstNumber); event.preventDefault(); @@ -2492,7 +2517,14 @@ export class Message extends React.PureComponent { } if (contact) { - showContactDetail({ contact, signalAccount: contact.firstNumber }); + const signalAccount = + contact.firstNumber && contact.uuid + ? { + phoneNumber: contact.firstNumber, + uuid: contact.uuid, + } + : undefined; + showContactDetail({ contact, signalAccount }); event.preventDefault(); event.stopPropagation(); diff --git a/ts/components/conversation/MessageDetail.stories.tsx b/ts/components/conversation/MessageDetail.stories.tsx index d1f6e625893..7d36f3e9c6a 100644 --- a/ts/components/conversation/MessageDetail.stories.tsx +++ b/ts/components/conversation/MessageDetail.stories.tsx @@ -99,6 +99,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ ), showForwardMessageModal: action('showForwardMessageModal'), showVisualAttachment: action('showVisualAttachment'), + startConversation: action('startConversation'), }); story.add('Delivered Incoming', () => { diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index fb1dec3c7e8..dfd881c29d0 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -87,6 +87,7 @@ export type PropsBackboneActions = Pick< | 'showExpiredOutgoingTapToViewToast' | 'showForwardMessageModal' | 'showVisualAttachment' + | 'startConversation' >; export type PropsReduxActions = Pick< @@ -297,6 +298,7 @@ export class MessageDetail extends React.Component { showExpiredOutgoingTapToViewToast, showForwardMessageModal, showVisualAttachment, + startConversation, theme, } = this.props; @@ -364,6 +366,7 @@ export class MessageDetail extends React.Component { log.warn('MessageDetail: deleteMessageForEveryone called!'); }} showVisualAttachment={showVisualAttachment} + startConversation={startConversation} theme={theme} />
diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 30b941e89cf..5cd244545d6 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -96,6 +96,7 @@ const defaultMessageProps: MessagesProps = { showForwardMessageModal: action('default--showForwardMessageModal'), showMessageDetail: action('default--showMessageDetail'), showVisualAttachment: action('default--showVisualAttachment'), + startConversation: action('default--startConversation'), status: 'sent', text: 'This is really interesting.', textDirection: TextDirection.Default, diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index aad38e222ff..4d000b9dcf0 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -398,6 +398,7 @@ const actions = () => ({ downloadNewVersion: action('downloadNewVersion'), startCallingLobby: action('startCallingLobby'), + startConversation: action('startConversation'), returnToActiveCall: action('returnToActiveCall'), contactSupport: action('contactSupport'), diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index dd03adf1723..fc6dc61da2a 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -253,6 +253,7 @@ const getActions = createSelector( 'scrollToQuotedMessage', 'showExpiredIncomingTapToViewToast', 'showExpiredOutgoingTapToViewToast', + 'startConversation', 'showIdentity', diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index ade8bb02e82..2fe8961bd3d 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -93,6 +93,7 @@ const getDefaultProps = () => ({ downloadNewVersion: action('downloadNewVersion'), showIdentity: action('showIdentity'), startCallingLobby: action('startCallingLobby'), + startConversation: action('startConversation'), returnToActiveCall: action('returnToActiveCall'), shouldCollapseAbove: false, shouldCollapseBelow: false, diff --git a/ts/state/ducks/accounts.ts b/ts/state/ducks/accounts.ts index d4a4171e9a0..21a10efa9b0 100644 --- a/ts/state/ducks/accounts.ts +++ b/ts/state/ducks/accounts.ts @@ -2,15 +2,19 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ThunkAction } from 'redux-thunk'; + +import * as Errors from '../../types/errors'; +import * as log from '../../logging/log'; + import type { StateType as RootStateType } from '../reducer'; -import { UUID } from '../../types/UUID'; +import type { UUIDStringType } from '../../types/UUID'; import type { NoopActionType } from './noop'; // State export type AccountsStateType = { - accounts: Record; + accounts: Record; }; // Actions @@ -18,8 +22,8 @@ export type AccountsStateType = { type AccountUpdateActionType = { type: 'accounts/UPDATE'; payload: { - identifier: string; - hasAccount: boolean; + phoneNumber: string; + uuid?: UUIDStringType; }; }; @@ -32,14 +36,14 @@ export const actions = { }; function checkForAccount( - identifier: string + phoneNumber: string ): ThunkAction< void, RootStateType, unknown, AccountUpdateActionType | NoopActionType > { - return async dispatch => { + return async (dispatch, getState) => { if (!window.textsecure.messaging) { dispatch({ type: 'NOOP', @@ -48,21 +52,50 @@ function checkForAccount( return; } - let hasAccount = false; + const conversation = window.ConversationController.get(phoneNumber); + if (conversation && conversation.get('uuid')) { + log.error(`checkForAccount: found ${phoneNumber} in existing contacts`); + const uuid = conversation.get('uuid'); + dispatch({ + type: 'accounts/UPDATE', + payload: { + phoneNumber, + uuid, + }, + }); + return; + } + + const state = getState(); + const existing = Object.prototype.hasOwnProperty.call( + state.accounts.accounts, + phoneNumber + ); + if (existing) { + dispatch({ + type: 'NOOP', + payload: null, + }); + } + + let uuid: UUIDStringType | undefined; + + log.error(`checkForAccount: looking ${phoneNumber} up on server`); try { - hasAccount = await window.textsecure.messaging.checkAccountExistence( - new UUID(identifier) - ); - } catch (_error) { - // Doing nothing with this failed fetch + const uuidLookup = await window.textsecure.messaging.getUuidsForE164s([ + phoneNumber, + ]); + uuid = uuidLookup[phoneNumber] || undefined; + } catch (error) { + log.error('checkForAccount:', Errors.toLogFormat(error)); } dispatch({ type: 'accounts/UPDATE', payload: { - identifier, - hasAccount, + phoneNumber, + uuid, }, }); }; @@ -86,13 +119,13 @@ export function reducer( if (action.type === 'accounts/UPDATE') { const { payload } = action; - const { identifier, hasAccount } = payload; + const { phoneNumber, uuid } = payload; return { ...state, accounts: { ...state.accounts, - [identifier]: hasAccount, + [phoneNumber]: uuid, }, }; } diff --git a/ts/state/selectors/accounts.ts b/ts/state/selectors/accounts.ts index 45f5d71b0bb..caca10dfb3c 100644 --- a/ts/state/selectors/accounts.ts +++ b/ts/state/selectors/accounts.ts @@ -5,20 +5,23 @@ import { createSelector } from 'reselect'; import type { StateType } from '../reducer'; import type { AccountsStateType } from '../ducks/accounts'; +import type { UUIDStringType } from '../../types/UUID'; export const getAccounts = (state: StateType): AccountsStateType => state.accounts; -export type AccountSelectorType = (identifier?: string) => boolean; +export type AccountSelectorType = ( + identifier?: string +) => UUIDStringType | undefined; export const getAccountSelector = createSelector( getAccounts, (accounts: AccountsStateType): AccountSelectorType => { return (identifier?: string) => { if (!identifier) { - return false; + return undefined; } - return accounts.accounts[identifier] || false; + return accounts.accounts[identifier] || undefined; }; } ); diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 28a37412970..85a542275ec 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -1443,7 +1443,7 @@ export function getMessagePropStatus( export function getPropsForEmbeddedContact( message: MessageWithUIFieldsType, regionCode: string | undefined, - accountSelector: (identifier?: string) => boolean + accountSelector: (identifier?: string) => UUIDStringType | undefined ): EmbeddedContactType | undefined { const contacts = message.contact; if (!contacts || !contacts.length) { @@ -1459,7 +1459,7 @@ export function getPropsForEmbeddedContact( getAbsoluteAttachmentPath: window.Signal.Migrations.getAbsoluteAttachmentPath, firstNumber, - isNumberOnSignal: accountSelector(firstNumber), + uuid: accountSelector(firstNumber), }); } diff --git a/ts/state/smart/MessageDetail.tsx b/ts/state/smart/MessageDetail.tsx index 7a20bb91f9a..1842dfc6a2c 100644 --- a/ts/state/smart/MessageDetail.tsx +++ b/ts/state/smart/MessageDetail.tsx @@ -56,6 +56,7 @@ const mapStateToProps = ( showExpiredOutgoingTapToViewToast, showForwardMessageModal, showVisualAttachment, + startConversation, } = props; const contactNameColor = @@ -102,6 +103,7 @@ const mapStateToProps = ( showExpiredOutgoingTapToViewToast, showForwardMessageModal, showVisualAttachment, + startConversation, }; }; diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index 526a06f18ff..85b193f3346 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -98,6 +98,7 @@ export type TimelinePropsType = ExternalProps & | 'showIdentity' | 'showMessageDetail' | 'showVisualAttachment' + | 'startConversation' | 'unblurAvatar' | 'updateSharedGroups' >; diff --git a/ts/test-node/types/EmbeddedContact_test.ts b/ts/test-node/types/EmbeddedContact_test.ts index 927d92be281..da8e078ae83 100644 --- a/ts/test-node/types/EmbeddedContact_test.ts +++ b/ts/test-node/types/EmbeddedContact_test.ts @@ -14,6 +14,7 @@ import { parseAndWriteAvatar, } from '../../types/EmbeddedContact'; import { fakeAttachment } from '../../test-both/helpers/fakeAttachment'; +import { UUID } from '../../types/UUID'; describe('Contact', () => { const NUMBER = '+12025550099'; @@ -113,7 +114,7 @@ describe('Contact', () => { describe('embeddedContactSelector', () => { const regionCode = '1'; const firstNumber = '+1202555000'; - const isNumberOnSignal = false; + const uuid = undefined; const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`; it('eliminates avatar if it has had an attachment download error', () => { @@ -141,13 +142,13 @@ describe('Contact', () => { organization: 'Somewhere, Inc.', avatar: undefined, firstNumber, - isNumberOnSignal, + uuid, number: undefined, }; const actual = embeddedContactSelector(contact, { regionCode, firstNumber, - isNumberOnSignal, + uuid, getAbsoluteAttachmentPath, }); assert.deepEqual(actual, expected); @@ -185,19 +186,21 @@ describe('Contact', () => { }), }, firstNumber, - isNumberOnSignal, + uuid, number: undefined, }; const actual = embeddedContactSelector(contact, { regionCode, firstNumber, - isNumberOnSignal, + uuid, getAbsoluteAttachmentPath, }); assert.deepEqual(actual, expected); }); it('calculates absolute path', () => { + const fullUuid = UUID.generate().toString(); + const contact = { name: { displayName: 'displayName', @@ -228,13 +231,13 @@ describe('Contact', () => { }), }, firstNumber, - isNumberOnSignal: true, + uuid: fullUuid, number: undefined, }; const actual = embeddedContactSelector(contact, { regionCode, firstNumber, - isNumberOnSignal: true, + uuid: fullUuid, getAbsoluteAttachmentPath, }); assert.deepEqual(actual, expected); diff --git a/ts/types/EmbeddedContact.ts b/ts/types/EmbeddedContact.ts index 09398fd6790..7e6da1657b7 100644 --- a/ts/types/EmbeddedContact.ts +++ b/ts/types/EmbeddedContact.ts @@ -14,6 +14,7 @@ import { import type { AttachmentType, migrateDataToFileSystem } from './Attachment'; import { toLogFormat } from './errors'; import type { LoggerType } from './Logging'; +import type { UUIDStringType } from './UUID'; export type EmbeddedContactType = { name?: Name; @@ -25,7 +26,7 @@ export type EmbeddedContactType = { // Populated by selector firstNumber?: string; - isNumberOnSignal?: boolean; + uuid?: UUIDStringType; }; type Name = { @@ -133,16 +134,11 @@ export function embeddedContactSelector( options: { regionCode?: string; firstNumber?: string; - isNumberOnSignal?: boolean; + uuid?: UUIDStringType; getAbsoluteAttachmentPath: (path: string) => string; } ): EmbeddedContactType { - const { - getAbsoluteAttachmentPath, - firstNumber, - isNumberOnSignal, - regionCode, - } = options; + const { getAbsoluteAttachmentPath, firstNumber, uuid, regionCode } = options; let { avatar } = contact; if (avatar && avatar.avatar) { @@ -164,7 +160,7 @@ export function embeddedContactSelector( return { ...contact, firstNumber, - isNumberOnSignal, + uuid, avatar, number: contact.number && diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index c4c6e1a5222..8614e017be3 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -173,7 +173,10 @@ type MessageActionsType = { retryDeleteForEveryone: (messageId: string) => unknown; showContactDetail: (options: { contact: EmbeddedContactType; - signalAccount?: string; + signalAccount?: { + phoneNumber: string; + uuid: UUIDStringType; + }; }) => unknown; showContactModal: (contactId: string) => unknown; showSafetyNumber: (contactId: string) => unknown; @@ -187,6 +190,7 @@ type MessageActionsType = { messageId: string; showSingle?: boolean; }) => unknown; + startConversation: (e164: string, uuid: UUIDStringType) => unknown; }; type MediaType = { @@ -768,7 +772,10 @@ export class ConversationView extends window.Backbone.View { }; const showContactDetail = (options: { contact: EmbeddedContactType; - signalAccount?: string; + signalAccount?: { + phoneNumber: string; + uuid: UUIDStringType; + }; }) => { this.showContactDetail(options); }; @@ -866,6 +873,7 @@ export class ConversationView extends window.Backbone.View { }; const showForwardMessageModal = this.showForwardMessageModal.bind(this); + const startConversation = this.startConversation.bind(this); return { deleteMessage, @@ -891,6 +899,7 @@ export class ConversationView extends window.Backbone.View { showIdentity, showMessageDetail, showVisualAttachment, + startConversation, }; } @@ -2368,7 +2377,10 @@ export class ConversationView extends window.Backbone.View { signalAccount, }: { contact: EmbeddedContactType; - signalAccount?: string; + signalAccount?: { + phoneNumber: string; + uuid: UUIDStringType; + }; }): void { const view = new Whisper.ReactWrapperView({ Component: window.Signal.Components.ContactDetail, @@ -2378,7 +2390,10 @@ export class ConversationView extends window.Backbone.View { hasSignalAccount: Boolean(signalAccount), onSendMessage: () => { if (signalAccount) { - this.openConversation(signalAccount); + this.startConversation( + signalAccount.phoneNumber, + signalAccount.uuid + ); } }, }, @@ -2390,6 +2405,19 @@ export class ConversationView extends window.Backbone.View { this.listenBack(view); } + startConversation(e164: string, uuid: UUIDStringType): void { + const conversationId = window.ConversationController.ensureContactIds({ + e164, + uuid, + }); + strictAssert( + conversationId, + `startConversation failed given ${e164}/${uuid} combination` + ); + + this.openConversation(conversationId); + } + async openConversation( conversationId: string, messageId?: string