Restore ability to message someone from embedded contact

This commit is contained in:
Scott Nonnenberg 2022-04-11 17:26:09 -07:00 committed by GitHub
parent f77175f6b3
commit 302604f67e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 311 additions and 236 deletions

View file

@ -1207,15 +1207,15 @@
@include font-body-2-bold; @include font-body-2-bold;
margin-top: 8px; margin-top: 8px;
margin-bottom: -10px; margin-bottom: -8px;
margin-left: -12px; margin-left: -12px;
margin-right: -12px; margin-right: -12px;
text-align: center; text-align: center;
padding: 10px; padding: 10px;
border-bottom-left-radius: 16px; border-bottom-left-radius: 18px;
border-bottom-right-radius: 16px; border-bottom-right-radius: 18px;
@include light-theme { @include light-theme {
color: $color-ultramarine; 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 { .module-message__author-avatar-container {
align-items: flex-end; align-items: flex-end;
display: flex; display: flex;

View file

@ -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> = {}): 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 <EmbeddedContact {...props} />;
});
story.add('Only Email', () => {
const props = createProps({
contact: {
email: fullContact.email,
},
});
return <EmbeddedContact {...props} />;
});
story.add('Given Name', () => {
const props = createProps({
contact: {
name: {
givenName: 'Jerry',
},
},
});
return <EmbeddedContact {...props} />;
});
story.add('Organization', () => {
const props = createProps({
contact: {
organization: 'Company 5',
},
});
return <EmbeddedContact {...props} />;
});
story.add('Given + Family Name', () => {
const props = createProps({
contact: {
name: {
givenName: 'Jerry',
familyName: 'FamilyName',
},
},
});
return <EmbeddedContact {...props} />;
});
story.add('Family Name', () => {
const props = createProps({
contact: {
name: {
familyName: 'FamilyName',
},
},
});
return <EmbeddedContact {...props} />;
});
story.add('Loading Avatar', () => {
const props = createProps({
contact: {
name: {
displayName: 'Jerry Jordan',
},
avatar: {
avatar: fakeAttachment({
pending: true,
contentType: IMAGE_GIF,
}),
isProfile: true,
},
},
});
return <EmbeddedContact {...props} />;
});
story.add('Incoming', () => {
const props = createProps({
contact: {
name: fullContact.name,
},
isIncoming: true,
});
// Wrapped in a <div> to provide a background for light color of text
return (
<div style={{ backgroundColor: 'darkgreen' }}>
<EmbeddedContact {...props} />
</div>
);
});
story.add('Content Above and Below', () => {
const props = createProps({
withContentAbove: true,
withContentBelow: true,
});
return (
<>
<div>Content Above</div>
<EmbeddedContact {...props} />
<div>Content Below</div>
</>
);
});

View file

@ -20,6 +20,7 @@ import {
IMAGE_WEBP, IMAGE_WEBP,
VIDEO_MP4, VIDEO_MP4,
stringToMIMEType, stringToMIMEType,
IMAGE_GIF,
} from '../../types/MIME'; } from '../../types/MIME';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { MessageAudio } from './MessageAudio'; import { MessageAudio } from './MessageAudio';
@ -30,6 +31,7 @@ import { pngUrl } from '../../storybook/Fixtures';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation'; import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { WidthBreakpoint } from '../_util'; import { WidthBreakpoint } from '../_util';
import { MINUTE } from '../../util/durations'; import { MINUTE } from '../../util/durations';
import { ContactFormType } from '../../types/EmbeddedContact';
import { import {
fakeAttachment, fakeAttachment,
@ -37,6 +39,7 @@ import {
} from '../../test-both/helpers/fakeAttachment'; } from '../../test-both/helpers/fakeAttachment';
import { getFakeBadge } from '../../test-both/helpers/getFakeBadge'; import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
import { ThemeType } from '../../types/Util'; import { ThemeType } from '../../types/Util';
import { UUID } from '../../types/UUID';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -118,6 +121,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
select('conversationColor', ConversationColors, ConversationColors[0]), select('conversationColor', ConversationColors, ConversationColors[0]),
conversationId: text('conversationId', overrideProps.conversationId || ''), conversationId: text('conversationId', overrideProps.conversationId || ''),
conversationType: overrideProps.conversationType || 'direct', conversationType: overrideProps.conversationType || 'direct',
contact: overrideProps.contact,
deletedForEveryone: overrideProps.deletedForEveryone, deletedForEveryone: overrideProps.deletedForEveryone,
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'), deleteMessageForEveryone: action('deleteMessageForEveryone'),
@ -191,6 +195,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showForwardMessageModal: action('showForwardMessageModal'), showForwardMessageModal: action('showForwardMessageModal'),
showMessageDetail: action('showMessageDetail'), showMessageDetail: action('showMessageDetail'),
showVisualAttachment: action('showVisualAttachment'), showVisualAttachment: action('showVisualAttachment'),
startConversation: action('startConversation'),
status: overrideProps.status || 'sent', status: overrideProps.status || 'sent',
text: overrideProps.text || text('text', ''), text: overrideProps.text || text('text', ''),
textDirection: overrideProps.textDirection || TextDirection.Default, 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);
});

View file

@ -83,6 +83,7 @@ import { getCustomColorStyle } from '../../util/getCustomColorStyle';
import { offsetDistanceModifier } from '../../util/popperUtil'; import { offsetDistanceModifier } from '../../util/popperUtil';
import * as KeyboardLayout from '../../services/keyboardLayout'; import * as KeyboardLayout from '../../services/keyboardLayout';
import { StopPropagation } from '../StopPropagation'; import { StopPropagation } from '../StopPropagation';
import type { UUIDStringType } from '../../types/UUID';
type Trigger = { type Trigger = {
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void; handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
@ -279,7 +280,7 @@ export type PropsActions = {
clearSelectedMessage: () => unknown; clearSelectedMessage: () => unknown;
doubleCheckMissingQuoteReference: (messageId: string) => unknown; doubleCheckMissingQuoteReference: (messageId: string) => unknown;
messageExpanded: (id: string, displayLimit: number) => unknown; messageExpanded: (id: string, displayLimit: number) => unknown;
checkForAccount: (identifier: string) => unknown; checkForAccount: (phoneNumber: string) => unknown;
reactToMessage: ( reactToMessage: (
id: string, id: string,
@ -293,10 +294,14 @@ export type PropsActions = {
deleteMessageForEveryone: (id: string) => void; deleteMessageForEveryone: (id: string) => void;
showMessageDetail: (id: string) => void; showMessageDetail: (id: string) => void;
startConversation: (e164: string, uuid: UUIDStringType) => void;
openConversation: (conversationId: string, messageId?: string) => void; openConversation: (conversationId: string, messageId?: string) => void;
showContactDetail: (options: { showContactDetail: (options: {
contact: EmbeddedContactType; contact: EmbeddedContactType;
signalAccount?: string; signalAccount?: {
phoneNumber: string;
uuid: UUIDStringType;
};
}) => void; }) => void;
showContactModal: (contactId: string, conversationId?: string) => void; showContactModal: (contactId: string, conversationId?: string) => void;
@ -501,7 +506,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
const { contact, checkForAccount } = this.props; const { contact, checkForAccount } = this.props;
if (contact && contact.firstNumber && !contact.isNumberOnSignal) { if (contact && contact.firstNumber && !contact.uuid) {
checkForAccount(contact.firstNumber); checkForAccount(contact.firstNumber);
} }
} }
@ -1336,8 +1341,7 @@ export class Message extends React.PureComponent<Props, State> {
this.getMetadataPlacement() !== MetadataPlacement.NotRendered; this.getMetadataPlacement() !== MetadataPlacement.NotRendered;
const otherContent = const otherContent =
(contact && contact.firstNumber && contact.isNumberOnSignal) || (contact && contact.firstNumber && contact.uuid) || withCaption;
withCaption;
const tabIndex = otherContent ? 0 : -1; const tabIndex = otherContent ? 0 : -1;
return ( return (
@ -1346,7 +1350,18 @@ export class Message extends React.PureComponent<Props, State> {
isIncoming={direction === 'incoming'} isIncoming={direction === 'incoming'}
i18n={i18n} i18n={i18n}
onClick={() => { onClick={() => {
showContactDetail({ contact, signalAccount: contact.firstNumber }); const signalAccount =
contact.firstNumber && contact.uuid
? {
phoneNumber: contact.firstNumber,
uuid: contact.uuid,
}
: undefined;
showContactDetail({
contact,
signalAccount,
});
}} }}
withContentAbove={withContentAbove} withContentAbove={withContentAbove}
withContentBelow={withContentBelow} withContentBelow={withContentBelow}
@ -1356,20 +1371,30 @@ export class Message extends React.PureComponent<Props, State> {
} }
public renderSendMessageButton(): JSX.Element | null { 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) { if (!contact) {
return null; return null;
} }
const { firstNumber, isNumberOnSignal } = contact; const { firstNumber, uuid } = contact;
if (!firstNumber || !isNumberOnSignal) { if (!firstNumber || !uuid) {
return null; return null;
} }
return ( return (
<button <button
type="button" type="button"
onClick={() => openConversation(firstNumber)} onClick={() => startConversation(firstNumber, uuid)}
className="module-message__send-message-button" className={classNames(
'module-message__send-message-button',
noBottomLeftCurve &&
'module-message__send-message-button--no-bottom-left-curve',
noBottomRightCurve &&
'module-message__send-message-button--no-bottom-right-curve'
)}
> >
{i18n('sendMessageToContact')} {i18n('sendMessageToContact')}
</button> </button>
@ -2484,7 +2509,7 @@ export class Message extends React.PureComponent<Props, State> {
this.audioButtonRef.current.click(); this.audioButtonRef.current.click();
} }
if (contact && contact.firstNumber && contact.isNumberOnSignal) { if (contact && contact.firstNumber && contact.uuid) {
openConversation(contact.firstNumber); openConversation(contact.firstNumber);
event.preventDefault(); event.preventDefault();
@ -2492,7 +2517,14 @@ export class Message extends React.PureComponent<Props, State> {
} }
if (contact) { 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.preventDefault();
event.stopPropagation(); event.stopPropagation();

View file

@ -99,6 +99,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
), ),
showForwardMessageModal: action('showForwardMessageModal'), showForwardMessageModal: action('showForwardMessageModal'),
showVisualAttachment: action('showVisualAttachment'), showVisualAttachment: action('showVisualAttachment'),
startConversation: action('startConversation'),
}); });
story.add('Delivered Incoming', () => { story.add('Delivered Incoming', () => {

View file

@ -87,6 +87,7 @@ export type PropsBackboneActions = Pick<
| 'showExpiredOutgoingTapToViewToast' | 'showExpiredOutgoingTapToViewToast'
| 'showForwardMessageModal' | 'showForwardMessageModal'
| 'showVisualAttachment' | 'showVisualAttachment'
| 'startConversation'
>; >;
export type PropsReduxActions = Pick< export type PropsReduxActions = Pick<
@ -297,6 +298,7 @@ export class MessageDetail extends React.Component<Props> {
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showForwardMessageModal, showForwardMessageModal,
showVisualAttachment, showVisualAttachment,
startConversation,
theme, theme,
} = this.props; } = this.props;
@ -364,6 +366,7 @@ export class MessageDetail extends React.Component<Props> {
log.warn('MessageDetail: deleteMessageForEveryone called!'); log.warn('MessageDetail: deleteMessageForEveryone called!');
}} }}
showVisualAttachment={showVisualAttachment} showVisualAttachment={showVisualAttachment}
startConversation={startConversation}
theme={theme} theme={theme}
/> />
</div> </div>

View file

@ -96,6 +96,7 @@ const defaultMessageProps: MessagesProps = {
showForwardMessageModal: action('default--showForwardMessageModal'), showForwardMessageModal: action('default--showForwardMessageModal'),
showMessageDetail: action('default--showMessageDetail'), showMessageDetail: action('default--showMessageDetail'),
showVisualAttachment: action('default--showVisualAttachment'), showVisualAttachment: action('default--showVisualAttachment'),
startConversation: action('default--startConversation'),
status: 'sent', status: 'sent',
text: 'This is really interesting.', text: 'This is really interesting.',
textDirection: TextDirection.Default, textDirection: TextDirection.Default,

View file

@ -398,6 +398,7 @@ const actions = () => ({
downloadNewVersion: action('downloadNewVersion'), downloadNewVersion: action('downloadNewVersion'),
startCallingLobby: action('startCallingLobby'), startCallingLobby: action('startCallingLobby'),
startConversation: action('startConversation'),
returnToActiveCall: action('returnToActiveCall'), returnToActiveCall: action('returnToActiveCall'),
contactSupport: action('contactSupport'), contactSupport: action('contactSupport'),

View file

@ -253,6 +253,7 @@ const getActions = createSelector(
'scrollToQuotedMessage', 'scrollToQuotedMessage',
'showExpiredIncomingTapToViewToast', 'showExpiredIncomingTapToViewToast',
'showExpiredOutgoingTapToViewToast', 'showExpiredOutgoingTapToViewToast',
'startConversation',
'showIdentity', 'showIdentity',

View file

@ -93,6 +93,7 @@ const getDefaultProps = () => ({
downloadNewVersion: action('downloadNewVersion'), downloadNewVersion: action('downloadNewVersion'),
showIdentity: action('showIdentity'), showIdentity: action('showIdentity'),
startCallingLobby: action('startCallingLobby'), startCallingLobby: action('startCallingLobby'),
startConversation: action('startConversation'),
returnToActiveCall: action('returnToActiveCall'), returnToActiveCall: action('returnToActiveCall'),
shouldCollapseAbove: false, shouldCollapseAbove: false,
shouldCollapseBelow: false, shouldCollapseBelow: false,

View file

@ -2,15 +2,19 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk'; 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 type { StateType as RootStateType } from '../reducer';
import { UUID } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
import type { NoopActionType } from './noop'; import type { NoopActionType } from './noop';
// State // State
export type AccountsStateType = { export type AccountsStateType = {
accounts: Record<string, boolean>; accounts: Record<string, UUIDStringType | undefined>;
}; };
// Actions // Actions
@ -18,8 +22,8 @@ export type AccountsStateType = {
type AccountUpdateActionType = { type AccountUpdateActionType = {
type: 'accounts/UPDATE'; type: 'accounts/UPDATE';
payload: { payload: {
identifier: string; phoneNumber: string;
hasAccount: boolean; uuid?: UUIDStringType;
}; };
}; };
@ -32,14 +36,14 @@ export const actions = {
}; };
function checkForAccount( function checkForAccount(
identifier: string phoneNumber: string
): ThunkAction< ): ThunkAction<
void, void,
RootStateType, RootStateType,
unknown, unknown,
AccountUpdateActionType | NoopActionType AccountUpdateActionType | NoopActionType
> { > {
return async dispatch => { return async (dispatch, getState) => {
if (!window.textsecure.messaging) { if (!window.textsecure.messaging) {
dispatch({ dispatch({
type: 'NOOP', type: 'NOOP',
@ -48,21 +52,50 @@ function checkForAccount(
return; 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 { try {
hasAccount = await window.textsecure.messaging.checkAccountExistence( const uuidLookup = await window.textsecure.messaging.getUuidsForE164s([
new UUID(identifier) phoneNumber,
); ]);
} catch (_error) { uuid = uuidLookup[phoneNumber] || undefined;
// Doing nothing with this failed fetch } catch (error) {
log.error('checkForAccount:', Errors.toLogFormat(error));
} }
dispatch({ dispatch({
type: 'accounts/UPDATE', type: 'accounts/UPDATE',
payload: { payload: {
identifier, phoneNumber,
hasAccount, uuid,
}, },
}); });
}; };
@ -86,13 +119,13 @@ export function reducer(
if (action.type === 'accounts/UPDATE') { if (action.type === 'accounts/UPDATE') {
const { payload } = action; const { payload } = action;
const { identifier, hasAccount } = payload; const { phoneNumber, uuid } = payload;
return { return {
...state, ...state,
accounts: { accounts: {
...state.accounts, ...state.accounts,
[identifier]: hasAccount, [phoneNumber]: uuid,
}, },
}; };
} }

View file

@ -5,20 +5,23 @@ import { createSelector } from 'reselect';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { AccountsStateType } from '../ducks/accounts'; import type { AccountsStateType } from '../ducks/accounts';
import type { UUIDStringType } from '../../types/UUID';
export const getAccounts = (state: StateType): AccountsStateType => export const getAccounts = (state: StateType): AccountsStateType =>
state.accounts; state.accounts;
export type AccountSelectorType = (identifier?: string) => boolean; export type AccountSelectorType = (
identifier?: string
) => UUIDStringType | undefined;
export const getAccountSelector = createSelector( export const getAccountSelector = createSelector(
getAccounts, getAccounts,
(accounts: AccountsStateType): AccountSelectorType => { (accounts: AccountsStateType): AccountSelectorType => {
return (identifier?: string) => { return (identifier?: string) => {
if (!identifier) { if (!identifier) {
return false; return undefined;
} }
return accounts.accounts[identifier] || false; return accounts.accounts[identifier] || undefined;
}; };
} }
); );

View file

@ -1443,7 +1443,7 @@ export function getMessagePropStatus(
export function getPropsForEmbeddedContact( export function getPropsForEmbeddedContact(
message: MessageWithUIFieldsType, message: MessageWithUIFieldsType,
regionCode: string | undefined, regionCode: string | undefined,
accountSelector: (identifier?: string) => boolean accountSelector: (identifier?: string) => UUIDStringType | undefined
): EmbeddedContactType | undefined { ): EmbeddedContactType | undefined {
const contacts = message.contact; const contacts = message.contact;
if (!contacts || !contacts.length) { if (!contacts || !contacts.length) {
@ -1459,7 +1459,7 @@ export function getPropsForEmbeddedContact(
getAbsoluteAttachmentPath: getAbsoluteAttachmentPath:
window.Signal.Migrations.getAbsoluteAttachmentPath, window.Signal.Migrations.getAbsoluteAttachmentPath,
firstNumber, firstNumber,
isNumberOnSignal: accountSelector(firstNumber), uuid: accountSelector(firstNumber),
}); });
} }

View file

@ -56,6 +56,7 @@ const mapStateToProps = (
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showForwardMessageModal, showForwardMessageModal,
showVisualAttachment, showVisualAttachment,
startConversation,
} = props; } = props;
const contactNameColor = const contactNameColor =
@ -102,6 +103,7 @@ const mapStateToProps = (
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showForwardMessageModal, showForwardMessageModal,
showVisualAttachment, showVisualAttachment,
startConversation,
}; };
}; };

View file

@ -98,6 +98,7 @@ export type TimelinePropsType = ExternalProps &
| 'showIdentity' | 'showIdentity'
| 'showMessageDetail' | 'showMessageDetail'
| 'showVisualAttachment' | 'showVisualAttachment'
| 'startConversation'
| 'unblurAvatar' | 'unblurAvatar'
| 'updateSharedGroups' | 'updateSharedGroups'
>; >;

View file

@ -14,6 +14,7 @@ import {
parseAndWriteAvatar, parseAndWriteAvatar,
} from '../../types/EmbeddedContact'; } from '../../types/EmbeddedContact';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment'; import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
import { UUID } from '../../types/UUID';
describe('Contact', () => { describe('Contact', () => {
const NUMBER = '+12025550099'; const NUMBER = '+12025550099';
@ -113,7 +114,7 @@ describe('Contact', () => {
describe('embeddedContactSelector', () => { describe('embeddedContactSelector', () => {
const regionCode = '1'; const regionCode = '1';
const firstNumber = '+1202555000'; const firstNumber = '+1202555000';
const isNumberOnSignal = false; const uuid = undefined;
const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`; const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`;
it('eliminates avatar if it has had an attachment download error', () => { it('eliminates avatar if it has had an attachment download error', () => {
@ -141,13 +142,13 @@ describe('Contact', () => {
organization: 'Somewhere, Inc.', organization: 'Somewhere, Inc.',
avatar: undefined, avatar: undefined,
firstNumber, firstNumber,
isNumberOnSignal, uuid,
number: undefined, number: undefined,
}; };
const actual = embeddedContactSelector(contact, { const actual = embeddedContactSelector(contact, {
regionCode, regionCode,
firstNumber, firstNumber,
isNumberOnSignal, uuid,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
}); });
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
@ -185,19 +186,21 @@ describe('Contact', () => {
}), }),
}, },
firstNumber, firstNumber,
isNumberOnSignal, uuid,
number: undefined, number: undefined,
}; };
const actual = embeddedContactSelector(contact, { const actual = embeddedContactSelector(contact, {
regionCode, regionCode,
firstNumber, firstNumber,
isNumberOnSignal, uuid,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
}); });
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
it('calculates absolute path', () => { it('calculates absolute path', () => {
const fullUuid = UUID.generate().toString();
const contact = { const contact = {
name: { name: {
displayName: 'displayName', displayName: 'displayName',
@ -228,13 +231,13 @@ describe('Contact', () => {
}), }),
}, },
firstNumber, firstNumber,
isNumberOnSignal: true, uuid: fullUuid,
number: undefined, number: undefined,
}; };
const actual = embeddedContactSelector(contact, { const actual = embeddedContactSelector(contact, {
regionCode, regionCode,
firstNumber, firstNumber,
isNumberOnSignal: true, uuid: fullUuid,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
}); });
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);

View file

@ -14,6 +14,7 @@ import {
import type { AttachmentType, migrateDataToFileSystem } from './Attachment'; import type { AttachmentType, migrateDataToFileSystem } from './Attachment';
import { toLogFormat } from './errors'; import { toLogFormat } from './errors';
import type { LoggerType } from './Logging'; import type { LoggerType } from './Logging';
import type { UUIDStringType } from './UUID';
export type EmbeddedContactType = { export type EmbeddedContactType = {
name?: Name; name?: Name;
@ -25,7 +26,7 @@ export type EmbeddedContactType = {
// Populated by selector // Populated by selector
firstNumber?: string; firstNumber?: string;
isNumberOnSignal?: boolean; uuid?: UUIDStringType;
}; };
type Name = { type Name = {
@ -133,16 +134,11 @@ export function embeddedContactSelector(
options: { options: {
regionCode?: string; regionCode?: string;
firstNumber?: string; firstNumber?: string;
isNumberOnSignal?: boolean; uuid?: UUIDStringType;
getAbsoluteAttachmentPath: (path: string) => string; getAbsoluteAttachmentPath: (path: string) => string;
} }
): EmbeddedContactType { ): EmbeddedContactType {
const { const { getAbsoluteAttachmentPath, firstNumber, uuid, regionCode } = options;
getAbsoluteAttachmentPath,
firstNumber,
isNumberOnSignal,
regionCode,
} = options;
let { avatar } = contact; let { avatar } = contact;
if (avatar && avatar.avatar) { if (avatar && avatar.avatar) {
@ -164,7 +160,7 @@ export function embeddedContactSelector(
return { return {
...contact, ...contact,
firstNumber, firstNumber,
isNumberOnSignal, uuid,
avatar, avatar,
number: number:
contact.number && contact.number &&

View file

@ -173,7 +173,10 @@ type MessageActionsType = {
retryDeleteForEveryone: (messageId: string) => unknown; retryDeleteForEveryone: (messageId: string) => unknown;
showContactDetail: (options: { showContactDetail: (options: {
contact: EmbeddedContactType; contact: EmbeddedContactType;
signalAccount?: string; signalAccount?: {
phoneNumber: string;
uuid: UUIDStringType;
};
}) => unknown; }) => unknown;
showContactModal: (contactId: string) => unknown; showContactModal: (contactId: string) => unknown;
showSafetyNumber: (contactId: string) => unknown; showSafetyNumber: (contactId: string) => unknown;
@ -187,6 +190,7 @@ type MessageActionsType = {
messageId: string; messageId: string;
showSingle?: boolean; showSingle?: boolean;
}) => unknown; }) => unknown;
startConversation: (e164: string, uuid: UUIDStringType) => unknown;
}; };
type MediaType = { type MediaType = {
@ -768,7 +772,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}; };
const showContactDetail = (options: { const showContactDetail = (options: {
contact: EmbeddedContactType; contact: EmbeddedContactType;
signalAccount?: string; signalAccount?: {
phoneNumber: string;
uuid: UUIDStringType;
};
}) => { }) => {
this.showContactDetail(options); this.showContactDetail(options);
}; };
@ -866,6 +873,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}; };
const showForwardMessageModal = this.showForwardMessageModal.bind(this); const showForwardMessageModal = this.showForwardMessageModal.bind(this);
const startConversation = this.startConversation.bind(this);
return { return {
deleteMessage, deleteMessage,
@ -891,6 +899,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
showIdentity, showIdentity,
showMessageDetail, showMessageDetail,
showVisualAttachment, showVisualAttachment,
startConversation,
}; };
} }
@ -2368,7 +2377,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
signalAccount, signalAccount,
}: { }: {
contact: EmbeddedContactType; contact: EmbeddedContactType;
signalAccount?: string; signalAccount?: {
phoneNumber: string;
uuid: UUIDStringType;
};
}): void { }): void {
const view = new Whisper.ReactWrapperView({ const view = new Whisper.ReactWrapperView({
Component: window.Signal.Components.ContactDetail, Component: window.Signal.Components.ContactDetail,
@ -2378,7 +2390,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
hasSignalAccount: Boolean(signalAccount), hasSignalAccount: Boolean(signalAccount),
onSendMessage: () => { onSendMessage: () => {
if (signalAccount) { if (signalAccount) {
this.openConversation(signalAccount); this.startConversation(
signalAccount.phoneNumber,
signalAccount.uuid
);
} }
}, },
}, },
@ -2390,6 +2405,19 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.listenBack(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( async openConversation(
conversationId: string, conversationId: string,
messageId?: string messageId?: string