First-class profile name rendering

This commit is contained in:
Scott Nonnenberg 2020-07-23 18:35:32 -07:00
parent 632cd0e87e
commit d07b8e82b2
63 changed files with 1044 additions and 454 deletions

View file

@ -715,6 +715,10 @@
"message": "Typing animation for this conversation", "message": "Typing animation for this conversation",
"description": "Used as the 'title' attibute for the typing animation" "description": "Used as the 'title' attibute for the typing animation"
}, },
"contactInAddressBook": {
"message": "This person is in your contacts.",
"description": "Description of icon denoting that contact is from your address book"
},
"contactAvatarAlt": { "contactAvatarAlt": {
"message": "Avatar for contact $name$", "message": "Avatar for contact $name$",
"description": "Used in the alt tag for the image avatar of a contact", "description": "Used in the alt tag for the image avatar of a contact",

View file

@ -467,6 +467,7 @@
uuid: this.get('uuid'), uuid: this.get('uuid'),
e164: this.get('e164'), e164: this.get('e164'),
isAccepted: this.getAccepted(),
isArchived: this.get('isArchived'), isArchived: this.get('isArchived'),
isBlocked: this.isBlocked(), isBlocked: this.isBlocked(),
isVerified: this.isVerified(), isVerified: this.isVerified(),
@ -477,7 +478,7 @@
isMe: this.isMe(), isMe: this.isMe(),
typingContact: typingContact ? typingContact.format() : null, typingContact: typingContact ? typingContact.format() : null,
lastUpdated: this.get('timestamp'), lastUpdated: this.get('timestamp'),
name: this.getName(), name: this.get('name'),
profileName: this.getProfileName(), profileName: this.getProfileName(),
timestamp, timestamp,
inboxPosition, inboxPosition,
@ -589,10 +590,9 @@
const receiptSpecs = readMessages.map(m => ({ const receiptSpecs = readMessages.map(m => ({
senderE164: m.get('source'), senderE164: m.get('source'),
senderUuid: m.get('sourceUuid'), senderUuid: m.get('sourceUuid'),
senderId: ConversationController.get({ senderId: ConversationController.ensureContactIds({
e164: m.get('source'), e164: m.get('source'),
uuid: m.get('sourceUuid'), uuid: m.get('sourceUuid'),
lowTrust: true,
}), }),
timestamp: m.get('sent_at'), timestamp: m.get('sent_at'),
hasErrors: m.hasErrors(), hasErrors: m.hasErrors(),
@ -2651,16 +2651,14 @@
}); });
}, },
getName() {
if (this.isPrivate()) {
return this.get('name');
}
return this.get('name') || i18n('unknownGroup');
},
getTitle() { getTitle() {
if (this.isPrivate()) { if (this.isPrivate()) {
return this.get('name') || this.getNumber() || i18n('unknownContact'); return (
this.get('name') ||
this.getProfileName() ||
this.getNumber() ||
i18n('unknownContact')
);
} }
return this.get('name') || i18n('unknownGroup'); return this.get('name') || i18n('unknownGroup');
}, },
@ -2675,24 +2673,6 @@
return null; return null;
}, },
getDisplayName() {
if (!this.isPrivate()) {
return this.getTitle();
}
const name = this.get('name');
if (name) {
return name;
}
const profileName = this.get('profileName');
if (profileName) {
return `${this.getNumber()} ~${profileName}`;
}
return this.getNumber();
},
getNumber() { getNumber() {
if (!this.isPrivate()) { if (!this.isPrivate()) {
return ''; return '';

View file

@ -594,6 +594,7 @@
status: this.getMessagePropStatus(), status: this.getMessagePropStatus(),
contact: this.getPropsForEmbeddedContact(), contact: this.getPropsForEmbeddedContact(),
canReply: this.canReply(), canReply: this.canReply(),
authorTitle: contact.title,
authorColor, authorColor,
authorName: contact.name, authorName: contact.name,
authorProfileName: contact.profileName, authorProfileName: contact.profileName,
@ -781,7 +782,8 @@
ourRegionCode: regionCode, ourRegionCode: regionCode,
}); });
const authorProfileName = contact ? contact.getProfileName() : null; const authorProfileName = contact ? contact.getProfileName() : null;
const authorName = contact ? contact.getName() : null; const authorName = contact ? contact.get('name') : null;
const authorTitle = contact ? contact.getTitle() : null;
const isFromMe = contact ? contact.isMe() : false; const isFromMe = contact ? contact.isMe() : false;
const firstAttachment = quote.attachments && quote.attachments[0]; const firstAttachment = quote.attachments && quote.attachments[0];
@ -795,6 +797,7 @@
authorId: author, authorId: author,
authorPhoneNumber, authorPhoneNumber,
authorProfileName, authorProfileName,
authorTitle,
authorName, authorName,
authorColor, authorColor,
referencedMessageNotFound, referencedMessageNotFound,
@ -891,7 +894,7 @@
if (fromContact.isMe()) { if (fromContact.isMe()) {
messages.push(i18n('youUpdatedTheGroup')); messages.push(i18n('youUpdatedTheGroup'));
} else { } else {
messages.push(i18n('updatedTheGroup', fromContact.getDisplayName())); messages.push(i18n('updatedTheGroup', fromContact.getTitle()));
} }
if (groupUpdate.joined && groupUpdate.joined.length) { if (groupUpdate.joined && groupUpdate.joined.length) {
@ -906,9 +909,7 @@
messages.push( messages.push(
i18n( i18n(
'multipleJoinedTheGroup', 'multipleJoinedTheGroup',
_.map(joinedWithoutMe, contact => _.map(joinedWithoutMe, contact => contact.getTitle()).join(', ')
contact.getDisplayName()
).join(', ')
) )
); );
@ -924,7 +925,7 @@
messages.push(i18n('youJoinedTheGroup')); messages.push(i18n('youJoinedTheGroup'));
} else { } else {
messages.push( messages.push(
i18n('joinedTheGroup', joinedContacts[0].getDisplayName()) i18n('joinedTheGroup', joinedContacts[0].getTitle())
); );
} }
} }
@ -1018,7 +1019,7 @@
if (!conversation) { if (!conversation) {
return number; return number;
} }
return conversation.getDisplayName(); return conversation.getTitle();
}, },
onDestroy() { onDestroy() {
this.cleanup(); this.cleanup();
@ -2346,11 +2347,11 @@
if ( if (
// Avatar added // Avatar added
!existingAvatar || (!existingAvatar && avatarAttachment) ||
// Avatar changed // Avatar changed
(existingAvatar && existingAvatar.hash !== hash) || (existingAvatar && existingAvatar.hash !== hash) ||
// Avatar removed // Avatar removed
avatarAttachment === null (existingAvatar && !avatarAttachment)
) { ) {
// Removes existing avatar from disk // Removes existing avatar from disk
if (existingAvatar && existingAvatar.path) { if (existingAvatar && existingAvatar.path) {
@ -2396,10 +2397,11 @@
conversation.set({ addedBy: message.getContactId() }); conversation.set({ addedBy: message.getContactId() });
} }
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { } else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
const sender = ConversationController.ensureContactIds({ const senderId = ConversationController.ensureContactIds({
e164: source, e164: source,
uuid: sourceUuid, uuid: sourceUuid,
}); });
const sender = ConversationController.get(senderId);
const inGroup = Boolean( const inGroup = Boolean(
sender && sender &&
(conversation.get('members') || []).includes(sender.id) (conversation.get('members') || []).includes(sender.id)

View file

@ -26,21 +26,12 @@
this.contactView = null; this.contactView = null;
} }
const isMe = this.model.isMe();
this.contactView = new Whisper.ReactWrapperView({ this.contactView = new Whisper.ReactWrapperView({
className: 'contact-wrapper', className: 'contact-wrapper',
Component: window.Signal.Components.ContactListItem, Component: window.Signal.Components.ContactListItem,
props: { props: {
isMe, ...this.model.cachedProps,
color: this.model.getColor(),
avatarPath: this.model.getAvatarPath(),
phoneNumber: this.model.getNumber(),
name: this.model.getName(),
profileName: this.model.getProfileName(),
verified: this.model.isVerified(),
onClick: this.showIdentity.bind(this), onClick: this.showIdentity.bind(this),
disabled: this.loading,
}, },
}); });
this.$el.append(this.contactView.el); this.$el.append(this.contactView.el);

View file

@ -360,18 +360,8 @@
: null; : null;
return { return {
id: this.model.id, ...this.model.cachedProps,
name: this.model.getName(),
phoneNumber: this.model.getNumber(),
profileName: this.model.getProfileName(),
color: this.model.getColor(),
avatarPath: this.model.getAvatarPath(),
isAccepted: this.model.getAccepted(),
isVerified: this.model.isVerified(),
isMe: this.model.isMe(),
isGroup: !this.model.isPrivate(),
isArchived: this.model.get('isArchived'),
leftGroup: this.model.get('left'), leftGroup: this.model.get('left'),
expirationSettingName, expirationSettingName,

View file

@ -0,0 +1,60 @@
diff --git a/node_modules/react-tooltip-lite/dist/index.js b/node_modules/react-tooltip-lite/dist/index.js
index 32ce07d..6461913 100644
--- a/node_modules/react-tooltip-lite/dist/index.js
+++ b/node_modules/react-tooltip-lite/dist/index.js
@@ -80,7 +80,7 @@ function (_React$Component) {
_this.state = {
showTip: false,
- hasHover: false,
+ hasHover: 0,
ignoreShow: false,
hasBeenShown: false
};
@@ -232,7 +232,7 @@ function (_React$Component) {
var _this3 = this;
this.setState({
- hasHover: false
+ hasHover: 0
});
if (this.state.showTip) {
@@ -250,7 +250,7 @@ function (_React$Component) {
value: function startHover() {
if (!this.state.ignoreShow) {
this.setState({
- hasHover: true
+ hasHover: (this.state.hasHover || 0) + 1,
});
clearTimeout(this.hoverTimeout);
this.hoverTimeout = setTimeout(this.checkHover, this.props.hoverDelay);
@@ -260,7 +260,7 @@ function (_React$Component) {
key: "endHover",
value: function endHover() {
this.setState({
- hasHover: false
+ hasHover: Math.max((this.state.hasHover || 0) - 1, 0),
});
clearTimeout(this.hoverTimeout);
this.hoverTimeout = setTimeout(this.checkHover, this.props.mouseOutDelay || this.props.hoverDelay);
@@ -268,7 +268,7 @@ function (_React$Component) {
}, {
key: "checkHover",
value: function checkHover() {
- this.state.hasHover ? this.showTip() : this.hideTip();
+ this.state.hasHover > 0 ? this.showTip() : this.hideTip();
}
}, {
key: "render",
@@ -330,7 +330,9 @@ function (_React$Component) {
props[eventToggle] = this.toggleTip; // only use hover if they don't have a toggle event
} else if (useHover && !isControlledByProps) {
props.onMouseEnter = this.startHover;
- props.onMouseLeave = tipContentHover || mouseOutDelay ? this.endHover : this.hideTip;
+ props.onMouseLeave = this.endHover;
+ props.onFocus = this.startHover;
+ props.onBlur = this.endHover;
props.onTouchStart = this.targetTouchStart;
props.onTouchEnd = this.targetTouchEnd;

View file

@ -152,6 +152,18 @@
@content; @content;
} }
} }
@mixin dark-mouse-mode() {
.dark-theme.mouse-mode & {
@content;
}
}
@mixin ios-mouse-mode() {
.ios-theme.mouse-mode & {
@content;
}
}
@mixin dark-ios-keyboard-mode() { @mixin dark-ios-keyboard-mode() {
.dark-theme.ios-theme.keyboard-mode & { .dark-theme.ios-theme.keyboard-mode & {
@content; @content;

View file

@ -36,12 +36,6 @@
} }
} }
// Module: Contact Name
.module-contact-name__profile-name {
font-style: italic;
}
// Module: Message // Module: Message
// Note: this does the same thing as module-timeline__message-container but // Note: this does the same thing as module-timeline__message-container but
@ -2389,6 +2383,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
} }
} }
.module-safety-number__bold-name {
font-weight: bold;
}
.module-message-calling { .module-message-calling {
&--audio { &--audio {
text-align: center; text-align: center;
@ -2612,18 +2610,40 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
cursor: inherit; cursor: inherit;
padding-top: 8px; padding: 8px;
padding-bottom: 8px; width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@include light-theme { @include light-theme {
color: $color-gray-60; color: $color-gray-60;
@include mouse-mode {
&:hover {
background-color: $color-gray-02;
}
}
@include keyboard-mode {
&:focus {
background-color: $color-gray-02;
}
}
} }
@include dark-theme { @include dark-theme {
color: $color-gray-15; color: $color-gray-15;
} }
@include dark-mouse-mode {
&:hover {
background-color: $color-gray-80;
}
}
@include dark-keyboard-mode {
&:focus {
background-color: $color-gray-80;
}
}
} }
.module-contact-list-item--with-click-handler { .module-contact-list-item--with-click-handler {
@ -2665,6 +2685,61 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
} }
} }
// Module: In Contacts Icon
.module-in-contacts-icon__icon {
display: inline-block;
height: 15px;
width: 15px;
margin-bottom: 2px;
vertical-align: middle;
@include light-theme {
@include color-svg(
'../images/icons/v2/profile-circle-outline-24.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/profile-circle-outline-24.svg',
$color-gray-25
);
}
@include keyboard-mode {
&:focus {
@include color-svg(
'../images/icons/v2/profile-circle-outline-24.svg',
$ultramarine-ui-light
);
}
}
}
.module-in-contacts-icon__tooltip {
.react-tooltip-lite {
color: $color-white;
background-color: $ultramarine-ui-light;
}
.react-tooltip-lite-arrow {
border-color: $ultramarine-ui-light;
}
@include dark-theme {
.react-tooltip-lite {
color: $color-white;
background-color: $ultramarine-ui-light;
}
.react-tooltip-lite-arrow {
border-color: $ultramarine-ui-light;
}
}
}
// Module: Conversation Header // Module: Conversation Header
.module-conversation-header { .module-conversation-header {
@ -2771,6 +2846,37 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
} }
} }
.module-conversation-header__contacts-icon {
display: inline-block;
height: 15px;
width: 15px;
margin-bottom: 3px;
vertical-align: middle;
@include light-theme {
@include color-svg(
'../images/icons/v2/profile-circle-outline-24.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/profile-circle-outline-24.svg',
$color-gray-25
);
}
@include keyboard-mode {
&:focus {
@include color-svg(
'../images/icons/v2/profile-circle-outline-24.svg',
$ultramarine-ui-light
);
}
}
}
.module-conversation-header__title__profile-name { .module-conversation-header__title__profile-name {
@include font-body-1-bold-italic; @include font-body-1-bold-italic;
} }
@ -4380,7 +4486,10 @@ button.module-image__border-overlay:focus {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 2; z-index: 3;
// This allows click-through to the overlay button behind it
pointer-events: none;
color: $color-white; color: $color-white;

View file

@ -228,11 +228,10 @@ export class ConversationController {
} }
/** /**
* Given a UUID and/or an E164, resolves to a string representing the local * Given a UUID and/or an E164, resolves to a string representing the local
* database id of the given contact. It may create new contacts, and it may merge * database id of the given contact. In high trust mode, it may create new contacts,
* contacts. * and it may merge contacts.
* *
* lowTrust = uuid/e164 pairing came from source like GroupV1 member list * highTrust = uuid/e164 pairing came from CDS, the server, or your own device
* highTrust = uuid/e164 pairing came from source like CDS
*/ */
// tslint:disable-next-line cyclomatic-complexity max-func-body-length // tslint:disable-next-line cyclomatic-complexity max-func-body-length
ensureContactIds({ ensureContactIds({

View file

@ -10,6 +10,7 @@ export type Props = {
conversationType: 'group' | 'direct'; conversationType: 'group' | 'direct';
noteToSelf?: boolean; noteToSelf?: boolean;
title: string;
name?: string; name?: string;
phoneNumber?: string; phoneNumber?: string;
profileName?: string; profileName?: string;
@ -63,17 +64,13 @@ export class Avatar extends React.Component<Props, State> {
} }
public renderImage() { public renderImage() {
const { avatarPath, i18n, name, phoneNumber, profileName } = this.props; const { avatarPath, i18n, title } = this.props;
const { imageBroken } = this.state; const { imageBroken } = this.state;
if (!avatarPath || imageBroken) { if (!avatarPath || imageBroken) {
return null; return null;
} }
const title = `${name || phoneNumber}${
!name && profileName ? ` ~${profileName}` : ''
}`;
return ( return (
<img <img
onError={this.handleImageErrorBound} onError={this.handleImageErrorBound}

View file

@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { Avatar, Props as AvatarProps } from './Avatar'; import { Avatar, Props as AvatarProps } from './Avatar';
import { useRestoreFocus } from '../util/hooks'; import { useRestoreFocus } from '../util/hooks';
@ -22,14 +21,16 @@ export const AvatarPopup = (props: Props) => {
const focusRef = React.useRef<HTMLButtonElement>(null); const focusRef = React.useRef<HTMLButtonElement>(null);
const { const {
i18n, i18n,
name,
profileName, profileName,
phoneNumber, phoneNumber,
title,
onViewPreferences, onViewPreferences,
onViewArchive, onViewArchive,
style, style,
} = props; } = props;
const hasProfileName = !isEmpty(profileName); const shouldShowNumber = Boolean(name || profileName);
// Note: mechanisms to dismiss this view are all in its host, MainHeader // Note: mechanisms to dismiss this view are all in its host, MainHeader
@ -41,10 +42,8 @@ export const AvatarPopup = (props: Props) => {
<div className="module-avatar-popup__profile"> <div className="module-avatar-popup__profile">
<Avatar {...props} size={52} /> <Avatar {...props} size={52} />
<div className="module-avatar-popup__profile__text"> <div className="module-avatar-popup__profile__text">
<div className="module-avatar-popup__profile__name"> <div className="module-avatar-popup__profile__name">{title}</div>
{hasProfileName ? profileName : phoneNumber} {shouldShowNumber ? (
</div>
{hasProfileName ? (
<div className="module-avatar-popup__profile__number"> <div className="module-avatar-popup__profile__number">
{phoneNumber} {phoneNumber}
</div> </div>

View file

@ -14,11 +14,13 @@ import { action } from '@storybook/addon-actions';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const callDetails = { const callDetails = {
avatarPath: undefined,
callId: 0, callId: 0,
contactColor: 'ultramarine' as ColorType,
isIncoming: true, isIncoming: true,
isVideoCall: true, isVideoCall: true,
avatarPath: undefined,
color: 'ultramarine' as ColorType,
title: 'Rick Sanchez',
name: 'Rick Sanchez', name: 'Rick Sanchez',
phoneNumber: '3051234567', phoneNumber: '3051234567',
profileName: 'Rick Sanchez', profileName: 'Rick Sanchez',

View file

@ -15,11 +15,13 @@ import { action } from '@storybook/addon-actions';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const callDetails = { const callDetails = {
avatarPath: undefined,
callId: 0, callId: 0,
contactColor: 'ultramarine' as ColorType,
isIncoming: true, isIncoming: true,
isVideoCall: true, isVideoCall: true,
avatarPath: undefined,
color: 'ultramarine' as ColorType,
title: 'Rick Sanchez',
name: 'Rick Sanchez', name: 'Rick Sanchez',
phoneNumber: '3051234567', phoneNumber: '3051234567',
profileName: 'Rick Sanchez', profileName: 'Rick Sanchez',

View file

@ -275,22 +275,24 @@ export class CallScreen extends React.Component<PropsType, StateType> {
const { i18n } = this.props; const { i18n } = this.props;
const { const {
avatarPath, avatarPath,
contactColor, color,
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
} = callDetails; } = callDetails;
return ( return (
<div className="module-ongoing-call__remote-video-disabled"> <div className="module-ongoing-call__remote-video-disabled">
<Avatar <Avatar
avatarPath={avatarPath} avatarPath={avatarPath}
color={contactColor || 'ultramarine'} color={color || 'ultramarine'}
noteToSelf={false} noteToSelf={false}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
size={112} size={112}
/> />
</div> </div>

View file

@ -114,18 +114,19 @@ export const CompositionArea = ({
showPickerHint, showPickerHint,
clearShowPickerHint, clearShowPickerHint,
// Message Requests // Message Requests
messageRequestsEnabled,
acceptedMessageRequest, acceptedMessageRequest,
conversationType, conversationType,
isBlocked, isBlocked,
messageRequestsEnabled,
name, name,
onAccept, onAccept,
onBlock, onBlock,
onBlockAndDelete, onBlockAndDelete,
onUnblock,
onDelete, onDelete,
profileName, onUnblock,
phoneNumber, phoneNumber,
profileName,
title,
}: Props) => { }: Props) => {
const [disabled, setDisabled] = React.useState(false); const [disabled, setDisabled] = React.useState(false);
const [showMic, setShowMic] = React.useState(!startingText); const [showMic, setShowMic] = React.useState(!startingText);
@ -333,6 +334,7 @@ export const CompositionArea = ({
name={name} name={name}
profileName={profileName} profileName={profileName}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
title={title}
/> />
); );
} }

View file

@ -1,110 +0,0 @@
#### It's me!
```jsx
<ContactListItem
i18n={util.i18n}
isMe
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
verified
profileName="🔥Flames🔥"
avatarPath={util.gifObjectUrl}
onClick={() => console.log('onClick')}
/>
```
#### With name and profile
Note the proper spacing between these two.
```jsx
<div>
<ContactListItem
i18n={util.i18n}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
avatarPath={util.gifObjectUrl}
onClick={() => console.log('onClick')}
/>
<ContactListItem
i18n={util.i18n}
name="Another ❄️ Yes"
phoneNumber="(202) 555-0011"
profileName="❄Ice❄"
avatarPath={util.gifObjectUrl}
onClick={() => console.log('onClick')}
/>
</div>
```
#### With name and profile, verified
```jsx
<ContactListItem
i18n={util.i18n}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
verified
avatarPath={util.gifObjectUrl}
onClick={() => console.log('onClick')}
/>
```
#### With name and profile, no avatar
```jsx
<ContactListItem
i18n={util.i18n}
name="Someone 🔥 Somewhere"
color="teal"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
onClick={() => console.log('onClick')}
/>
```
#### Profile, no name, no avatar
```jsx
<ContactListItem
i18n={util.i18n}
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
onClick={() => console.log('onClick')}
/>
```
#### Verified, profile, no name, no avatar
```jsx
<ContactListItem
i18n={util.i18n}
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
verified
onClick={() => console.log('onClick')}
/>
```
#### No name, no profile, no avatar
```jsx
<ContactListItem
i18n={util.i18n}
phoneNumber="(202) 555-0011"
onClick={() => console.log('onClick')}
/>
```
#### Verified, no name, no profile, no avatar
```jsx
<ContactListItem
i18n={util.i18n}
phoneNumber="(202) 555-0011"
verified
onClick={() => console.log('onClick')}
/>
```

View file

@ -0,0 +1,133 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { gifUrl } from '../storybook/Fixtures';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../\_locales/en/messages.json';
import { ContactListItem } from './ContactListItem';
const i18n = setupI18n('en', enMessages);
const onClick = action('onClick');
storiesOf('Components/ContactListItem', module)
.add("It's me!", () => {
return (
<ContactListItem
i18n={i18n}
isMe
title="Someone 🔥 Somewhere"
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
isVerified
profileName="🔥Flames🔥"
avatarPath={gifUrl}
onClick={onClick}
/>
);
})
.add('With name and profile (note vertical spacing)', () => {
return (
<div>
<ContactListItem
i18n={i18n}
title="Someone 🔥 Somewhere"
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
avatarPath={gifUrl}
onClick={onClick}
/>
<ContactListItem
i18n={i18n}
title="Another ❄️ Yes"
name="Another ❄️ Yes"
phoneNumber="(202) 555-0011"
profileName="❄Ice❄"
avatarPath={gifUrl}
onClick={onClick}
/>
</div>
);
})
.add('With name and profile, verified', () => {
return (
<ContactListItem
i18n={i18n}
title="Someone 🔥 Somewhere"
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
isVerified
avatarPath={gifUrl}
onClick={onClick}
/>
);
})
.add('With name and profile, no avatar', () => {
return (
<ContactListItem
i18n={i18n}
title="Someone 🔥 Somewhere"
name="Someone 🔥 Somewhere"
color="teal"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
onClick={onClick}
/>
);
})
.add('Profile, no name, no avatar', () => {
return (
<ContactListItem
i18n={i18n}
phoneNumber="(202) 555-0011"
title="🔥Flames🔥"
profileName="🔥Flames🔥"
onClick={onClick}
/>
);
})
.add('Verified, profile, no name, no avatar', () => {
return (
<ContactListItem
i18n={i18n}
phoneNumber="(202) 555-0011"
title="🔥Flames🔥"
profileName="🔥Flames🔥"
isVerified
onClick={onClick}
/>
);
})
.add('No name, no profile, no avatar', () => {
return (
<ContactListItem
i18n={i18n}
phoneNumber="(202) 555-0011"
title="(202) 555-0011"
onClick={onClick}
/>
);
})
.add('Verified, no name, no profile, no avatar', () => {
return (
<ContactListItem
i18n={i18n}
title="(202) 555-0011"
phoneNumber="(202) 555-0011"
isVerified
onClick={onClick}
/>
);
})
.add('No name, no profile, no number', () => {
return (
<ContactListItem i18n={i18n} title="Unknown contact" onClick={onClick} />
);
});

View file

@ -3,15 +3,17 @@ import classNames from 'classnames';
import { Avatar } from './Avatar'; import { Avatar } from './Avatar';
import { Emojify } from './conversation/Emojify'; import { Emojify } from './conversation/Emojify';
import { InContactsIcon } from './InContactsIcon';
import { ColorType, LocalizerType } from '../types/Util'; import { ColorType, LocalizerType } from '../types/Util';
interface Props { interface Props {
phoneNumber: string; title: string;
phoneNumber?: string;
isMe?: boolean; isMe?: boolean;
name?: string; name?: string;
color: ColorType; color?: ColorType;
verified: boolean; isVerified?: boolean;
profileName?: string; profileName?: string;
avatarPath?: string; avatarPath?: string;
i18n: LocalizerType; i18n: LocalizerType;
@ -27,6 +29,7 @@ export class ContactListItem extends React.Component<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
} = this.props; } = this.props;
return ( return (
@ -38,6 +41,7 @@ export class ContactListItem extends React.Component<Props> {
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
size={52} size={52}
/> />
); );
@ -51,21 +55,15 @@ export class ContactListItem extends React.Component<Props> {
isMe, isMe,
phoneNumber, phoneNumber,
profileName, profileName,
verified, title,
isVerified,
} = this.props; } = this.props;
const title = name ? name : phoneNumber;
const displayName = isMe ? i18n('you') : title; const displayName = isMe ? i18n('you') : title;
const shouldShowIcon = Boolean(name);
const profileElement = const showNumber = Boolean(isMe || name || profileName);
!isMe && profileName && !name ? ( const showVerified = !isMe && isVerified;
<span className="module-contact-list-item__text__profile-name">
~<Emojify text={profileName} />
</span>
) : null;
const showNumber = isMe || name;
const showVerified = !isMe && verified;
return ( return (
<button <button
@ -78,7 +76,13 @@ export class ContactListItem extends React.Component<Props> {
{this.renderAvatar()} {this.renderAvatar()}
<div className="module-contact-list-item__text"> <div className="module-contact-list-item__text">
<div className="module-contact-list-item__text__name"> <div className="module-contact-list-item__text__name">
<Emojify text={displayName} /> {profileElement} <Emojify text={displayName} />
{shouldShowIcon ? (
<span>
{' '}
<InContactsIcon i18n={i18n} />
</span>
) : null}
</div> </div>
<div className="module-contact-list-item__text__additional-data"> <div className="module-contact-list-item__text__additional-data">
{showVerified ? ( {showVerified ? (

View file

@ -12,9 +12,10 @@ import { ColorType, LocalizerType } from '../types/Util';
export type PropsData = { export type PropsData = {
id: string; id: string;
phoneNumber: string; phoneNumber?: string;
color?: ColorType; color?: ColorType;
profileName?: string; profileName?: string;
title: string;
name?: string; name?: string;
type: 'group' | 'direct'; type: 'group' | 'direct';
avatarPath?: string; avatarPath?: string;
@ -54,6 +55,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
} = this.props; } = this.props;
return ( return (
@ -67,6 +69,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
size={52} size={52}
/> />
{this.renderUnread()} {this.renderUnread()}
@ -97,6 +100,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
} = this.props; } = this.props;
return ( return (
@ -116,6 +120,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
name={name} name={name}
profileName={profileName} profileName={profileName}
title={title}
i18n={i18n}
/> />
)} )}
</div> </div>

View file

@ -0,0 +1,16 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../\_locales/en/messages.json';
import { InContactsIcon } from './InContactsIcon';
const i18n = setupI18n('en', enMessages);
storiesOf('Components/InContactsIcon', module).add('Default', () => {
return <InContactsIcon i18n={i18n} />;
});

View file

@ -0,0 +1,31 @@
import React from 'react';
import Tooltip from 'react-tooltip-lite';
import { LocalizerType } from '../types/Util';
type PropsType = {
i18n: LocalizerType;
};
export const InContactsIcon = (props: PropsType): JSX.Element => {
const { i18n } = props;
return (
<Tooltip
tagName="span"
direction="bottom"
className="module-in-contacts-icon__tooltip"
arrowSize={8}
content={i18n('contactInAddressBook')}
distance={13}
hoverDelay={0}
>
<span
tabIndex={0}
role="img"
aria-label={i18n('contactInAddressBook')}
className="module-in-contacts-icon__icon"
/>
</Tooltip>
);
};

View file

@ -16,14 +16,16 @@ const i18n = setupI18n('en', enMessages);
const defaultProps = { const defaultProps = {
acceptCall: action('accept-call'), acceptCall: action('accept-call'),
callDetails: { callDetails: {
avatarPath: undefined,
callId: 0, callId: 0,
contactColor: 'ultramarine' as ColorType,
isIncoming: true, isIncoming: true,
isVideoCall: true, isVideoCall: true,
avatarPath: undefined,
contactColor: 'ultramarine' as ColorType,
name: 'Rick Sanchez', name: 'Rick Sanchez',
phoneNumber: '3051234567', phoneNumber: '3051234567',
profileName: 'Rick Sanchez', profileName: 'Rick Sanchez',
title: 'Rick Sanchez',
}, },
declineCall: action('decline-call'), declineCall: action('decline-call'),
i18n, i18n,
@ -72,7 +74,7 @@ const permutations = [
storiesOf('Components/IncomingCallBar', module) storiesOf('Components/IncomingCallBar', module)
.add('Knobs Playground', () => { .add('Knobs Playground', () => {
const contactColor = select('contactColor', colors, 'ultramarine'); const color = select('color', colors, 'ultramarine');
const isVideoCall = boolean('isVideoCall', false); const isVideoCall = boolean('isVideoCall', false);
const name = text( const name = text(
'name', 'name',
@ -84,7 +86,7 @@ storiesOf('Components/IncomingCallBar', module)
{...defaultProps} {...defaultProps}
callDetails={{ callDetails={{
...defaultProps.callDetails, ...defaultProps.callDetails,
contactColor, color,
isVideoCall, isVideoCall,
name, name,
}} }}

View file

@ -62,7 +62,8 @@ export const IncomingCallBar = ({
const { const {
avatarPath, avatarPath,
callId, callId,
contactColor, color,
title,
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
@ -74,22 +75,25 @@ export const IncomingCallBar = ({
<div className="module-incoming-call__contact--avatar"> <div className="module-incoming-call__contact--avatar">
<Avatar <Avatar
avatarPath={avatarPath} avatarPath={avatarPath}
color={contactColor || 'ultramarine'} color={color || 'ultramarine'}
noteToSelf={false} noteToSelf={false}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
size={52} size={52}
/> />
</div> </div>
<div className="module-incoming-call__contact--name"> <div className="module-incoming-call__contact--name">
<div className="module-incoming-call__contact--name-header"> <div className="module-incoming-call__contact--name-header">
<ContactName <ContactName
phoneNumber={phoneNumber}
name={name} name={name}
phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
i18n={i18n}
/> />
</div> </div>
<div <div

View file

@ -22,12 +22,13 @@ export interface PropsType {
regionCode: string; regionCode: string;
// For display // For display
phoneNumber: string; phoneNumber?: string;
isMe: boolean; isMe: boolean;
name?: string; name?: string;
color?: ColorType; color?: ColorType;
verified: boolean; isVerified?: boolean;
profileName?: string; profileName?: string;
title: string;
avatarPath?: string; avatarPath?: string;
i18n: LocalizerType; i18n: LocalizerType;
@ -295,6 +296,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
searchConversationId, searchConversationId,
searchConversationName, searchConversationName,
searchTerm, searchTerm,
@ -319,6 +321,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
size={28} size={28}
innerRef={ref} innerRef={ref}
onClick={this.showAvatarPopup} onClick={this.showAvatarPopup}
@ -338,6 +341,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
avatarPath={avatarPath} avatarPath={avatarPath}
size={28} size={28}
onViewPreferences={() => { onViewPreferences={() => {

View file

@ -19,7 +19,8 @@ export type PropsDataType = {
snippet: string; snippet: string;
from: { from: {
phoneNumber: string; phoneNumber?: string;
title: string;
isMe?: boolean; isMe?: boolean;
name?: string; name?: string;
color?: ColorType; color?: ColorType;
@ -29,7 +30,8 @@ export type PropsDataType = {
to: { to: {
groupName?: string; groupName?: string;
phoneNumber: string; phoneNumber?: string;
title: string;
isMe?: boolean; isMe?: boolean;
name?: string; name?: string;
profileName?: string; profileName?: string;
@ -70,7 +72,9 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}
name={from.name} name={from.name}
profileName={from.profileName} profileName={from.profileName}
title={from.title}
module="module-message-search-result__header__name" module="module-message-search-result__header__name"
i18n={i18n}
/> />
); );
} }
@ -88,6 +92,8 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
phoneNumber={to.phoneNumber} phoneNumber={to.phoneNumber}
name={to.name} name={to.name}
profileName={to.profileName} profileName={to.profileName}
title={to.title}
i18n={i18n}
/> />
</span> </span>
</div> </div>
@ -115,6 +121,7 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
noteToSelf={isNoteToSelf} noteToSelf={isNoteToSelf}
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}
profileName={from.profileName} profileName={from.profileName}
title={from.title}
size={52} size={52}
/> />
); );

View file

@ -12,19 +12,44 @@ import { storiesOf } from '@storybook/react';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const contact = { const contactWithAllData = {
avatarPath: undefined,
color: 'signal-blue',
profileName: '-*Smartest Dude*-',
name: 'Rick Sanchez',
phoneNumber: '(305) 123-4567',
} as ConversationType;
const contactWithJustProfile = {
avatarPath: undefined,
color: 'signal-blue',
profileName: '-*Smartest Dude*-',
name: undefined,
phoneNumber: '(305) 123-4567',
} as ConversationType;
const contactWithJustNumber = {
avatarPath: undefined, avatarPath: undefined,
color: 'signal-blue', color: 'signal-blue',
profileName: undefined, profileName: undefined,
name: 'Rick Sanchez', name: undefined,
phoneNumber: '3051234567', phoneNumber: '(305) 123-4567',
} as ConversationType;
const contactWithNothing = {
id: 'some-guid',
avatarPath: undefined,
color: 'signal-blue',
profileName: undefined,
name: undefined,
phoneNumber: undefined,
} as ConversationType; } as ConversationType;
storiesOf('Components/SafetyNumberChangeDialog', module) storiesOf('Components/SafetyNumberChangeDialog', module)
.add('Single Contact Dialog', () => { .add('Single Contact Dialog', () => {
return ( return (
<SafetyNumberChangeDialog <SafetyNumberChangeDialog
contacts={[contact]} contacts={[contactWithAllData]}
i18n={i18n} i18n={i18n}
onCancel={action('cancel')} onCancel={action('cancel')}
onConfirm={action('confirm')} onConfirm={action('confirm')}
@ -38,7 +63,12 @@ storiesOf('Components/SafetyNumberChangeDialog', module)
.add('Multi Contact Dialog', () => { .add('Multi Contact Dialog', () => {
return ( return (
<SafetyNumberChangeDialog <SafetyNumberChangeDialog
contacts={[contact, contact, contact, contact]} contacts={[
contactWithAllData,
contactWithJustProfile,
contactWithJustNumber,
contactWithNothing,
]}
i18n={i18n} i18n={i18n}
onCancel={action('cancel')} onCancel={action('cancel')}
onConfirm={action('confirm')} onConfirm={action('confirm')}
@ -53,16 +83,16 @@ storiesOf('Components/SafetyNumberChangeDialog', module)
return ( return (
<SafetyNumberChangeDialog <SafetyNumberChangeDialog
contacts={[ contacts={[
contact, contactWithAllData,
contact, contactWithJustProfile,
contact, contactWithJustNumber,
contact, contactWithNothing,
contact, contactWithAllData,
contact, contactWithAllData,
contact, contactWithAllData,
contact, contactWithAllData,
contact, contactWithAllData,
contact, contactWithAllData,
]} ]}
i18n={i18n} i18n={i18n}
onCancel={action('cancel')} onCancel={action('cancel')}

View file

@ -1,6 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { Avatar } from './Avatar'; import { Avatar } from './Avatar';
import { ConfirmationModal } from './ConfirmationModal'; import { ConfirmationModal } from './ConfirmationModal';
import { InContactsIcon } from './InContactsIcon';
import { ConversationType } from '../state/ducks/conversations'; import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
@ -45,8 +48,14 @@ const SafetyDialogContents = ({
{i18n('changedVerificationWarning')} {i18n('changedVerificationWarning')}
</div> </div>
<ul className="module-sfn-dialog__contacts"> <ul className="module-sfn-dialog__contacts">
{contacts.map((contact: ConversationType) => ( {contacts.map((contact: ConversationType) => {
<li className="module-sfn-dialog__contact" key={contact.phoneNumber}> const shouldShowNumber = Boolean(contact.name || contact.profileName);
return (
<li
className="module-sfn-dialog__contact"
key={contact.phoneNumber}
>
<Avatar <Avatar
avatarPath={contact.avatarPath} avatarPath={contact.avatarPath}
color={contact.color} color={contact.color}
@ -55,24 +64,24 @@ const SafetyDialogContents = ({
name={contact.name} name={contact.name}
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
profileName={contact.profileName} profileName={contact.profileName}
title={contact.title}
size={52} size={52}
/> />
<div className="module-sfn-dialog__contact--wrapper"> <div className="module-sfn-dialog__contact--wrapper">
{contact.name && (
<>
<div className="module-sfn-dialog__contact--name"> <div className="module-sfn-dialog__contact--name">
{contact.name} {contact.title}
{contact.name ? (
<span>
{' '}
<InContactsIcon i18n={i18n} />
</span>
) : null}
</div> </div>
{shouldShowNumber ? (
<div className="module-sfn-dialog__contact--number"> <div className="module-sfn-dialog__contact--number">
{contact.phoneNumber} {contact.phoneNumber}
</div> </div>
</> ) : null}
)}
{!contact.name && (
<div className="module-sfn-dialog__contact--name">
{contact.phoneNumber}
</div>
)}
</div> </div>
<button <button
className="module-sfn-dialog__contact--view" className="module-sfn-dialog__contact--view"
@ -84,7 +93,8 @@ const SafetyDialogContents = ({
{i18n('view')} {i18n('view')}
</button> </button>
</li> </li>
))} );
})}
</ul> </ul>
<div className="module-sfn-dialog__actions"> <div className="module-sfn-dialog__actions">
<button <button

View file

@ -13,11 +13,39 @@ import { storiesOf } from '@storybook/react';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const defaultProps = { const contactWithAllData = {
contact: { name: 'Summer Smith',
title: 'Summer Smith', phoneNumber: '(305) 123-4567',
isVerified: true, isVerified: true,
} as ConversationType, } as ConversationType;
const contactWithJustProfile = {
avatarPath: undefined,
color: 'signal-blue',
profileName: '-*Smartest Dude*-',
name: undefined,
phoneNumber: '(305) 123-4567',
} as ConversationType;
const contactWithJustNumber = {
avatarPath: undefined,
color: 'signal-blue',
profileName: undefined,
name: undefined,
phoneNumber: '(305) 123-4567',
} as ConversationType;
const contactWithNothing = {
id: 'some-guid',
avatarPath: undefined,
color: 'signal-blue',
profileName: undefined,
name: undefined,
phoneNumber: undefined,
} as ConversationType;
const defaultProps = {
contact: contactWithAllData,
generateSafetyNumber: action('generate-safety-number'), generateSafetyNumber: action('generate-safety-number'),
i18n, i18n,
safetyNumber: 'XXX', safetyNumber: 'XXX',
@ -35,9 +63,9 @@ const permutations = [
title: 'Safety Number (not verified)', title: 'Safety Number (not verified)',
props: { props: {
contact: { contact: {
title: 'Morty Smith', ...contactWithAllData,
isVerified: false, verified: false,
} as ConversationType, },
}, },
}, },
{ {
@ -58,6 +86,24 @@ const permutations = [
onClose: action('close'), onClose: action('close'),
}, },
}, },
{
title: 'Just Profile',
props: {
contact: contactWithJustProfile,
},
},
{
title: 'Just Number',
props: {
contact: contactWithJustNumber,
},
},
{
title: 'No display info',
props: {
contact: contactWithNothing,
},
},
]; ];
storiesOf('Components/SafetyNumberViewer', module) storiesOf('Components/SafetyNumberViewer', module)

View file

@ -2,6 +2,7 @@ import React from 'react';
import { ConversationType } from '../state/ducks/conversations'; import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { getPlaceholder } from '../util/safetyNumber'; import { getPlaceholder } from '../util/safetyNumber';
import { Intl } from './Intl';
type SafetyNumberViewerProps = { type SafetyNumberViewerProps = {
contact?: ConversationType; contact?: ConversationType;
@ -32,11 +33,20 @@ export const SafetyNumberViewer = ({
generateSafetyNumber(contact); generateSafetyNumber(contact);
}, [safetyNumber]); }, [safetyNumber]);
const name = contact.title; const showNumber = Boolean(contact.name || contact.profileName);
const numberFragment = showNumber ? ` · ${contact.phoneNumber}` : '';
const name = `${contact.title}${numberFragment}`;
const boldName = (key?: number) => (
<span className="module-safety-number__bold-name" key={key}>
{name}
</span>
);
const isVerified = contact.isVerified; const isVerified = contact.isVerified;
const verifiedStatus = isVerified const verifiedStatusKey = isVerified ? 'isVerified' : 'isNotVerified';
? i18n('isVerified', [name]) const safetyNumberChangedKey = safetyNumberChanged
: i18n('isNotVerified', [name]); ? 'changedRightAfterVerify'
: 'yourSafetyNumberWith';
const verifyButtonText = isVerified ? i18n('unverify') : i18n('verify'); const verifyButtonText = isVerified ? i18n('unverify') : i18n('verify');
return ( return (
@ -49,21 +59,23 @@ export const SafetyNumberViewer = ({
</div> </div>
)} )}
<div className="module-safety-number__verification-label"> <div className="module-safety-number__verification-label">
{safetyNumberChanged <Intl
? i18n('changedRightAfterVerify', [name, name]) i18n={i18n}
: i18n('yourSafetyNumberWith', [name])} id={safetyNumberChangedKey}
components={[boldName(1), boldName(2)]}
/>
</div> </div>
<div className="module-safety-number__number"> <div className="module-safety-number__number">
{safetyNumber || getPlaceholder()} {safetyNumber || getPlaceholder()}
</div> </div>
{i18n('verifyHelp', [name])} <Intl i18n={i18n} id="verifyHelp" components={[boldName()]} />
<div className="module-safety-number__verification-status"> <div className="module-safety-number__verification-status">
{isVerified ? ( {isVerified ? (
<span className="module-safety-number__icon--verified" /> <span className="module-safety-number__icon--verified" />
) : ( ) : (
<span className="module-safety-number__icon--shield" /> <span className="module-safety-number__icon--shield" />
)} )}
{verifiedStatus} <Intl i18n={i18n} id={verifiedStatusKey} components={[boldName()]} />
</div> </div>
<div className="module-safety-number__verify-container"> <div className="module-safety-number__verify-container">
<button <button

View file

@ -45,12 +45,14 @@ messageLookup.set('1-guid-guid-guid-guid-guid', {
from: { from: {
phoneNumber: '(202) 555-0020', phoneNumber: '(202) 555-0020',
title: '(202) 555-0020',
isMe: true, isMe: true,
color: 'blue', color: 'blue',
avatarPath: gifUrl, avatarPath: gifUrl,
}, },
to: { to: {
phoneNumber: '(202) 555-0015', phoneNumber: '(202) 555-0015',
title: 'Mr. Fire 🔥',
name: 'Mr. Fire 🔥', name: 'Mr. Fire 🔥',
}, },
}); });
@ -63,10 +65,12 @@ messageLookup.set('2-guid-guid-guid-guid-guid', {
from: { from: {
phoneNumber: '(202) 555-0016', phoneNumber: '(202) 555-0016',
name: 'Jon ❄️', name: 'Jon ❄️',
title: 'Jon ❄️',
color: 'green', color: 'green',
}, },
to: { to: {
phoneNumber: '(202) 555-0020', phoneNumber: '(202) 555-0020',
title: '(202) 555-0020',
isMe: true, isMe: true,
}, },
}); });
@ -79,12 +83,14 @@ messageLookup.set('3-guid-guid-guid-guid-guid', {
from: { from: {
phoneNumber: '(202) 555-0011', phoneNumber: '(202) 555-0011',
name: 'Someone', name: 'Someone',
title: 'Someone',
color: 'green', color: 'green',
avatarPath: pngUrl, avatarPath: pngUrl,
}, },
to: { to: {
phoneNumber: '(202) 555-0016', phoneNumber: '(202) 555-0016',
name: "Y'all 🌆", name: "Y'all 🌆",
title: "Y'all 🌆",
}, },
}); });
@ -95,6 +101,7 @@ messageLookup.set('4-guid-guid-guid-guid-guid', {
snippet: 'Well, <<left>>everyone<<right>>, happy new year!', snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
from: { from: {
phoneNumber: '(202) 555-0020', phoneNumber: '(202) 555-0020',
title: '(202) 555-0020',
isMe: true, isMe: true,
color: 'light_green', color: 'light_green',
avatarPath: gifUrl, avatarPath: gifUrl,
@ -102,6 +109,7 @@ messageLookup.set('4-guid-guid-guid-guid-guid', {
to: { to: {
phoneNumber: '(202) 555-0016', phoneNumber: '(202) 555-0016',
name: "Y'all 🌆", name: "Y'all 🌆",
title: "Y'all 🌆",
}, },
}); });
@ -142,6 +150,7 @@ const conversations = [
id: '+12025550011', id: '+12025550011',
phoneNumber: '(202) 555-0011', phoneNumber: '(202) 555-0011',
name: 'Everyone 🌆', name: 'Everyone 🌆',
title: 'Everyone 🌆',
type: GROUP, type: GROUP,
color: 'signal-blue' as 'signal-blue', color: 'signal-blue' as 'signal-blue',
avatarPath: landscapeGreenUrl, avatarPath: landscapeGreenUrl,
@ -161,6 +170,7 @@ const conversations = [
id: '+12025550012', id: '+12025550012',
phoneNumber: '(202) 555-0012', phoneNumber: '(202) 555-0012',
name: 'Everyone Else 🔥', name: 'Everyone Else 🔥',
title: 'Everyone Else 🔥',
color: 'pink' as 'pink', color: 'pink' as 'pink',
type: DIRECT, type: DIRECT,
avatarPath: landscapePurpleUrl, avatarPath: landscapePurpleUrl,
@ -183,6 +193,7 @@ const contacts = [
id: '+12025550013', id: '+12025550013',
phoneNumber: '(202) 555-0013', phoneNumber: '(202) 555-0013',
name: 'The one Everyone', name: 'The one Everyone',
title: 'The one Everyone',
color: 'blue' as 'blue', color: 'blue' as 'blue',
type: DIRECT, type: DIRECT,
avatarPath: gifUrl, avatarPath: gifUrl,
@ -198,6 +209,7 @@ const contacts = [
id: '+12025550014', id: '+12025550014',
phoneNumber: '(202) 555-0014', phoneNumber: '(202) 555-0014',
name: 'No likey everyone', name: 'No likey everyone',
title: 'No likey everyone',
type: DIRECT, type: DIRECT,
color: 'red' as 'red', color: 'red' as 'red',
isMe: false, isMe: false,

View file

@ -20,7 +20,7 @@ export class StartNewConversation extends React.PureComponent<Props> {
color="grey" color="grey"
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
phoneNumber={phoneNumber} title={phoneNumber}
size={52} size={52}
/> />
<div className="module-start-new-conversation__content"> <div className="module-start-new-conversation__content">

View file

@ -1,21 +0,0 @@
#### Number, name and profile
```jsx
<ContactName
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
/>
```
#### Number and profile, no name
```jsx
<ContactName phoneNumber="(202) 555-0011" profileName="🔥Flames🔥" />
```
#### No name, no profile
```jsx
<ContactName phoneNumber="(202) 555-0011" />
```

View file

@ -0,0 +1,47 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../\_locales/en/messages.json';
import { ContactName } from './ContactName';
const i18n = setupI18n('en', enMessages);
storiesOf('Components/Conversation/ContactName', module)
.add('Number, name and profile', () => {
return (
<ContactName
title="Someone 🔥 Somewhere"
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
i18n={i18n}
/>
);
})
.add('Number and profile, no name', () => {
return (
<ContactName
title="🔥Flames🔥"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
i18n={i18n}
/>
);
})
.add('No name, no profile', () => {
return (
<ContactName
title="(202) 555-0011"
phoneNumber="(202) 555-0011"
i18n={i18n}
/>
);
})
.add('No data provided', () => {
return <ContactName title="unknownContact" i18n={i18n} />;
});

View file

@ -1,32 +1,25 @@
import React from 'react'; import React from 'react';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
import { LocalizerType } from '../../types/Util';
export interface Props { export interface Props {
title: string;
phoneNumber?: string; phoneNumber?: string;
name?: string; name?: string;
profileName?: string; profileName?: string;
module?: string; module?: string;
i18n: LocalizerType;
} }
export class ContactName extends React.Component<Props> { export class ContactName extends React.Component<Props> {
public render() { public render() {
const { phoneNumber, name, profileName, module } = this.props; const { module, title } = this.props;
const prefix = module ? module : 'module-contact-name'; const prefix = module ? module : 'module-contact-name';
const title = name ? name : phoneNumber;
const shouldShowProfile = Boolean(profileName && !name);
const profileElement = shouldShowProfile ? (
<span className={`${prefix}__profile-name`}>
~<Emojify text={profileName || ''} />
</span>
) : null;
return ( return (
<span className={prefix} dir="auto"> <span className={prefix} dir="auto">
<Emojify text={title || ''} /> <Emojify text={title || ''} />
{shouldShowProfile ? ' ' : null}
{profileElement}
</span> </span>
); );
} }

View file

@ -10,9 +10,9 @@ import enMessages from '../../../\_locales/en/messages.json';
import { import {
ConversationHeader, ConversationHeader,
Props, PropsActionsType,
PropsActions, PropsHousekeepingType,
PropsHousekeeping, PropsType,
} from './ConversationHeader'; } from './ConversationHeader';
import { gifUrl } from '../../storybook/Fixtures'; import { gifUrl } from '../../storybook/Fixtures';
@ -25,11 +25,11 @@ type ConversationHeaderStory = {
description: string; description: string;
items: Array<{ items: Array<{
title: string; title: string;
props: Props; props: PropsType;
}>; }>;
}; };
const actionProps: PropsActions = { const actionProps: PropsActionsType = {
onSetDisappearingMessages: action('onSetDisappearingMessages'), onSetDisappearingMessages: action('onSetDisappearingMessages'),
onDeleteMessages: action('onDeleteMessages'), onDeleteMessages: action('onDeleteMessages'),
onResetSession: action('onResetSession'), onResetSession: action('onResetSession'),
@ -50,7 +50,7 @@ const actionProps: PropsActions = {
onMoveToInbox: action('onMoveToInbox'), onMoveToInbox: action('onMoveToInbox'),
}; };
const housekeepingProps: PropsHousekeeping = { const housekeepingProps: PropsHousekeepingType = {
i18n, i18n,
}; };
@ -66,8 +66,10 @@ const stories: Array<ConversationHeaderStory> = [
color: 'red', color: 'red',
isVerified: true, isVerified: true,
avatarPath: gifUrl, avatarPath: gifUrl,
title: 'Someone 🔥 Somewhere',
name: 'Someone 🔥 Somewhere', name: 'Someone 🔥 Somewhere',
phoneNumber: '(202) 555-0001', phoneNumber: '(202) 555-0001',
type: 'direct',
id: '1', id: '1',
profileName: '🔥Flames🔥', profileName: '🔥Flames🔥',
isAccepted: true, isAccepted: true,
@ -80,8 +82,25 @@ const stories: Array<ConversationHeaderStory> = [
props: { props: {
color: 'blue', color: 'blue',
isVerified: false, isVerified: false,
title: 'Someone 🔥 Somewhere',
name: 'Someone 🔥 Somewhere', name: 'Someone 🔥 Somewhere',
phoneNumber: '(202) 555-0002', phoneNumber: '(202) 555-0002',
type: 'direct',
id: '2',
isAccepted: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'With name, not verified, descenders',
props: {
color: 'blue',
isVerified: false,
title: 'Joyrey 🔥 Leppey',
name: 'Joyrey 🔥 Leppey',
phoneNumber: '(202) 555-0002',
type: 'direct',
id: '2', id: '2',
isAccepted: true, isAccepted: true,
...actionProps, ...actionProps,
@ -94,7 +113,9 @@ const stories: Array<ConversationHeaderStory> = [
color: 'teal', color: 'teal',
isVerified: false, isVerified: false,
phoneNumber: '(202) 555-0003', phoneNumber: '(202) 555-0003',
type: 'direct',
id: '3', id: '3',
title: '🔥Flames🔥',
profileName: '🔥Flames🔥', profileName: '🔥Flames🔥',
isAccepted: true, isAccepted: true,
...actionProps, ...actionProps,
@ -104,7 +125,9 @@ const stories: Array<ConversationHeaderStory> = [
{ {
title: 'No name, no profile, no color', title: 'No name, no profile, no color',
props: { props: {
title: '(202) 555-0011',
phoneNumber: '(202) 555-0011', phoneNumber: '(202) 555-0011',
type: 'direct',
id: '11', id: '11',
isAccepted: true, isAccepted: true,
...actionProps, ...actionProps,
@ -117,6 +140,8 @@ const stories: Array<ConversationHeaderStory> = [
showBackButton: true, showBackButton: true,
color: 'deep_orange', color: 'deep_orange',
phoneNumber: '(202) 555-0004', phoneNumber: '(202) 555-0004',
title: '(202) 555-0004',
type: 'direct',
id: '4', id: '4',
isAccepted: true, isAccepted: true,
...actionProps, ...actionProps,
@ -127,7 +152,9 @@ const stories: Array<ConversationHeaderStory> = [
title: 'Disappearing messages set', title: 'Disappearing messages set',
props: { props: {
color: 'indigo', color: 'indigo',
title: '(202) 555-0005',
phoneNumber: '(202) 555-0005', phoneNumber: '(202) 555-0005',
type: 'direct',
id: '5', id: '5',
expirationSettingName: '10 seconds', expirationSettingName: '10 seconds',
timerOptions: [ timerOptions: [
@ -156,10 +183,11 @@ const stories: Array<ConversationHeaderStory> = [
title: 'Basic', title: 'Basic',
props: { props: {
color: 'signal-blue', color: 'signal-blue',
title: 'Typescript support group',
name: 'Typescript support group', name: 'Typescript support group',
phoneNumber: '', phoneNumber: '',
id: '1', id: '1',
isGroup: true, type: 'group',
expirationSettingName: '10 seconds', expirationSettingName: '10 seconds',
timerOptions: [ timerOptions: [
{ {
@ -180,10 +208,11 @@ const stories: Array<ConversationHeaderStory> = [
title: 'In a group you left - no disappearing messages', title: 'In a group you left - no disappearing messages',
props: { props: {
color: 'signal-blue', color: 'signal-blue',
title: 'Typescript support group',
name: 'Typescript support group', name: 'Typescript support group',
phoneNumber: '', phoneNumber: '',
id: '2', id: '2',
isGroup: true, type: 'group',
leftGroup: true, leftGroup: true,
expirationSettingName: '10 seconds', expirationSettingName: '10 seconds',
timerOptions: [ timerOptions: [
@ -211,8 +240,10 @@ const stories: Array<ConversationHeaderStory> = [
title: 'In chat with yourself', title: 'In chat with yourself',
props: { props: {
color: 'blue', color: 'blue',
title: '(202) 555-0007',
phoneNumber: '(202) 555-0007', phoneNumber: '(202) 555-0007',
id: '7', id: '7',
type: 'direct',
isMe: true, isMe: true,
isAccepted: true, isAccepted: true,
...actionProps, ...actionProps,
@ -229,8 +260,10 @@ const stories: Array<ConversationHeaderStory> = [
title: '1:1 conversation', title: '1:1 conversation',
props: { props: {
color: 'blue', color: 'blue',
title: '(202) 555-0007',
phoneNumber: '(202) 555-0007', phoneNumber: '(202) 555-0007',
id: '7', id: '7',
type: 'direct',
isMe: false, isMe: false,
isAccepted: false, isAccepted: false,
...actionProps, ...actionProps,

View file

@ -1,9 +1,5 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Emojify } from './Emojify';
import { Avatar } from '../Avatar';
import { ColorType, LocalizerType } from '../../types/Util';
import { import {
ContextMenu, ContextMenu,
ContextMenuTrigger, ContextMenuTrigger,
@ -11,24 +7,31 @@ import {
SubMenu, SubMenu,
} from 'react-contextmenu'; } from 'react-contextmenu';
import { Emojify } from './Emojify';
import { Avatar } from '../Avatar';
import { InContactsIcon } from '../InContactsIcon';
import { ColorType, LocalizerType } from '../../types/Util';
interface TimerOption { interface TimerOption {
name: string; name: string;
value: number; value: number;
} }
export interface PropsData { export interface PropsDataType {
id: string; id: string;
name?: string; name?: string;
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
color?: ColorType; color?: ColorType;
avatarPath?: string; avatarPath?: string;
type: 'direct' | 'group';
title: string;
isAccepted?: boolean; isAccepted?: boolean;
isVerified?: boolean; isVerified?: boolean;
isMe?: boolean; isMe?: boolean;
isGroup?: boolean;
isArchived?: boolean; isArchived?: boolean;
leftGroup?: boolean; leftGroup?: boolean;
@ -37,7 +40,7 @@ export interface PropsData {
timerOptions?: Array<TimerOption>; timerOptions?: Array<TimerOption>;
} }
export interface PropsActions { export interface PropsActionsType {
onSetDisappearingMessages: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void;
onDeleteMessages: () => void; onDeleteMessages: () => void;
onResetSession: () => void; onResetSession: () => void;
@ -54,17 +57,19 @@ export interface PropsActions {
onMoveToInbox: () => void; onMoveToInbox: () => void;
} }
export interface PropsHousekeeping { export interface PropsHousekeepingType {
i18n: LocalizerType; i18n: LocalizerType;
} }
export type Props = PropsData & PropsActions & PropsHousekeeping; export type PropsType = PropsDataType &
PropsActionsType &
PropsHousekeepingType;
export class ConversationHeader extends React.Component<Props> { export class ConversationHeader extends React.Component<PropsType> {
public showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void; public showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
public menuTriggerRef: React.RefObject<any>; public menuTriggerRef: React.RefObject<any>;
public constructor(props: Props) { public constructor(props: PropsType) {
super(props); super(props);
this.menuTriggerRef = React.createRef(); this.menuTriggerRef = React.createRef();
@ -96,6 +101,8 @@ export class ConversationHeader extends React.Component<Props> {
const { const {
name, name,
phoneNumber, phoneNumber,
title,
type,
i18n, i18n,
isMe, isMe,
profileName, profileName,
@ -110,19 +117,22 @@ export class ConversationHeader extends React.Component<Props> {
); );
} }
const shouldShowIcon = Boolean(name && type === 'direct');
const shouldShowNumber = Boolean(phoneNumber && (name || profileName));
return ( return (
<div className="module-conversation-header__title"> <div className="module-conversation-header__title">
{name ? <Emojify text={name} /> : null} <Emojify text={title} />
{name && phoneNumber ? ' · ' : null} {shouldShowIcon ? (
{phoneNumber ? phoneNumber : null}{' '} <span>
{profileName && !name ? ( {' '}
<span className="module-conversation-header__title__profile-name"> <InContactsIcon i18n={i18n} />
~<Emojify text={profileName} />
</span> </span>
) : null} ) : null}
{isVerified ? ' · ' : null} {shouldShowNumber ? ` · ${phoneNumber}` : null}
{isVerified ? ( {isVerified ? (
<span> <span>
{' · '}
<span className="module-conversation-header__title__verified-icon" /> <span className="module-conversation-header__title__verified-icon" />
{i18n('verified')} {i18n('verified')}
</span> </span>
@ -136,23 +146,23 @@ export class ConversationHeader extends React.Component<Props> {
avatarPath, avatarPath,
color, color,
i18n, i18n,
isGroup, type,
isMe, isMe,
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
} = this.props; } = this.props;
const conversationType = isGroup ? 'group' : 'direct';
return ( return (
<span className="module-conversation-header__avatar"> <span className="module-conversation-header__avatar">
<Avatar <Avatar
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
conversationType={conversationType} conversationType={type}
i18n={i18n} i18n={i18n}
noteToSelf={isMe} noteToSelf={isMe}
title={title}
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
@ -226,7 +236,7 @@ export class ConversationHeader extends React.Component<Props> {
if (!window.CALLING) { if (!window.CALLING) {
return null; return null;
} }
if (this.props.isGroup || this.props.isMe) { if (this.props.type === 'group' || this.props.isMe) {
return null; return null;
} }
@ -250,7 +260,7 @@ export class ConversationHeader extends React.Component<Props> {
if (!window.CALLING) { if (!window.CALLING) {
return null; return null;
} }
if (this.props.isGroup || this.props.isMe) { if (this.props.type === 'group' || this.props.isMe) {
return null; return null;
} }
@ -275,7 +285,7 @@ export class ConversationHeader extends React.Component<Props> {
i18n, i18n,
isAccepted, isAccepted,
isMe, isMe,
isGroup, type,
isArchived, isArchived,
leftGroup, leftGroup,
onDeleteMessages, onDeleteMessages,
@ -290,6 +300,7 @@ export class ConversationHeader extends React.Component<Props> {
} = this.props; } = this.props;
const disappearingTitle = i18n('disappearingMessages') as any; const disappearingTitle = i18n('disappearingMessages') as any;
const isGroup = type === 'group';
return ( return (
<ContextMenu id={triggerId}> <ContextMenu id={triggerId}>

View file

@ -10,8 +10,9 @@ import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const getTitle = () => text('name', 'Cayce Bollard');
const getName = () => text('name', 'Cayce Bollard'); const getName = () => text('name', 'Cayce Bollard');
const getProfileName = () => text('profileName', 'Cayce Bollard'); const getProfileName = () => text('profileName', 'Cayce Bollard (profile)');
const getAvatarPath = () => const getAvatarPath = () =>
text('avatarPath', '/fixtures/kitten-4-112-112.jpg'); text('avatarPath', '/fixtures/kitten-4-112-112.jpg');
const getPhoneNumber = () => text('phoneNumber', '+1 (646) 327-2700'); const getPhoneNumber = () => text('phoneNumber', '+1 (646) 327-2700');
@ -22,6 +23,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()} avatarPath={getAvatarPath()}
name={getName()} name={getName()}
profileName={getProfileName()} profileName={getProfileName()}
@ -37,6 +39,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()} avatarPath={getAvatarPath()}
name={getName()} name={getName()}
profileName={getProfileName()} profileName={getProfileName()}
@ -52,6 +55,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()} avatarPath={getAvatarPath()}
name={getName()} name={getName()}
profileName={getProfileName()} profileName={getProfileName()}
@ -62,13 +66,30 @@ storiesOf('Components/Conversation/ConversationHero', module)
</div> </div>
); );
}) })
.add('Direct (No Other Groups)', () => { .add('Direct (No Groups, Name)', () => {
return ( return (
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
title={getTitle()}
avatarPath={getAvatarPath()} avatarPath={getAvatarPath()}
name={getName()} name={getName()}
profileName={text('profileName', '')}
phoneNumber={getPhoneNumber()}
conversationType="direct"
groups={[]}
/>
</div>
);
})
.add('Direct (No Groups, Just Profile)', () => {
return (
<div style={{ width: '480px' }}>
<ConversationHero
i18n={i18n}
title={text('title', 'Cayce Bollard (profile)')}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={getProfileName()} profileName={getProfileName()}
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
conversationType="direct" conversationType="direct"
@ -77,13 +98,45 @@ storiesOf('Components/Conversation/ConversationHero', module)
</div> </div>
); );
}) })
.add('Direct (No Groups, Just Phone Number)', () => {
return (
<div style={{ width: '480px' }}>
<ConversationHero
i18n={i18n}
title={text('title', '+1 (646) 327-2700')}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={text('profileName', '')}
phoneNumber={getPhoneNumber()}
conversationType="direct"
groups={[]}
/>
</div>
);
})
.add('Direct (No Groups, No Data)', () => {
return (
<div style={{ width: '480px' }}>
<ConversationHero
i18n={i18n}
title={text('title', 'Unknown contact')}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={text('profileName', '')}
phoneNumber={text('phoneNumber', '')}
conversationType="direct"
groups={[]}
/>
</div>
);
})
.add('Group (many members)', () => { .add('Group (many members)', () => {
return ( return (
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')} name={text('groupName', 'NYC Rock Climbers')}
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
conversationType="group" conversationType="group"
membersCount={numberKnob('membersCount', 22)} membersCount={numberKnob('membersCount', 22)}
/> />
@ -95,8 +148,8 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')} name={text('groupName', 'NYC Rock Climbers')}
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
conversationType="group" conversationType="group"
membersCount={1} membersCount={1}
/> />
@ -108,8 +161,21 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')} name={text('groupName', 'NYC Rock Climbers')}
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')} conversationType="group"
membersCount={0}
/>
</div>
);
})
.add('Group (No name)', () => {
return (
<div style={{ width: '480px' }}>
<ConversationHero
i18n={i18n}
title={text('title', 'Unknown group')}
name={text('groupName', '')}
conversationType="group" conversationType="group"
membersCount={0} membersCount={0}
/> />
@ -122,6 +188,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
isMe={true} isMe={true}
title={getTitle()}
conversationType="direct" conversationType="direct"
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}
/> />

View file

@ -11,7 +11,7 @@ export type Props = {
isMe?: boolean; isMe?: boolean;
groups?: Array<string>; groups?: Array<string>;
membersCount?: number; membersCount?: number;
phoneNumber: string; phoneNumber?: string;
onHeightChange?: () => unknown; onHeightChange?: () => unknown;
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>; } & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
@ -60,6 +60,7 @@ export const ConversationHero = ({
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
onHeightChange, onHeightChange,
}: Props) => { }: Props) => {
const firstRenderRef = React.useRef(true); const firstRenderRef = React.useRef(true);
@ -86,6 +87,12 @@ export const ConversationHero = ({
...groups.map(g => `g-${g}`), ...groups.map(g => `g-${g}`),
]); ]);
const displayName =
name || (conversationType === 'group' ? i18n('unknownGroup') : undefined);
const phoneNumberOnly = Boolean(
!name && !profileName && conversationType === 'direct'
);
return ( return (
<div className="module-conversation-hero"> <div className="module-conversation-hero">
<Avatar <Avatar
@ -96,6 +103,7 @@ export const ConversationHero = ({
conversationType={conversationType} conversationType={conversationType}
name={name} name={name}
profileName={profileName} profileName={profileName}
title={title}
size={112} size={112}
className="module-conversation-hero__avatar" className="module-conversation-hero__avatar"
/> />
@ -104,9 +112,11 @@ export const ConversationHero = ({
i18n('noteToSelf') i18n('noteToSelf')
) : ( ) : (
<ContactName <ContactName
name={name} title={title}
name={displayName}
profileName={profileName} profileName={profileName}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
i18n={i18n}
/> />
)} )}
</h1> </h1>
@ -116,6 +126,8 @@ export const ConversationHero = ({
? i18n('ConversationHero--members-1') ? i18n('ConversationHero--members-1')
: membersCount !== undefined : membersCount !== undefined
? i18n('ConversationHero--members', [`${membersCount}`]) ? i18n('ConversationHero--members', [`${membersCount}`])
: phoneNumberOnly
? null
: phoneNumber} : phoneNumber}
</div> </div>
) : null} ) : null}

View file

@ -20,6 +20,7 @@ const stories: Array<GroupNotificationStory> = [
[ [
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -28,12 +29,14 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: 'Mrs. Ice',
phoneNumber: '(202) 555-1001', phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice', profileName: 'Mrs. Ice',
}, },
{ {
phoneNumber: '(202) 555-1002', phoneNumber: '(202) 555-1002',
name: 'Ms. Earth', name: 'Ms. Earth',
title: 'Ms. Earth',
}, },
], ],
}, },
@ -44,6 +47,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
isMe: true, isMe: true,
@ -53,10 +57,12 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: 'Mrs. Ice',
phoneNumber: '(202) 555-1001', phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice', profileName: 'Mrs. Ice',
}, },
{ {
title: 'Ms. Earth',
phoneNumber: '(202) 555-1002', phoneNumber: '(202) 555-1002',
name: 'Ms. Earth', name: 'Ms. Earth',
}, },
@ -74,6 +80,7 @@ const stories: Array<GroupNotificationStory> = [
[ [
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -82,13 +89,16 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: '(202) 555-1000',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
{ {
title: 'Mrs. Ice',
phoneNumber: '(202) 555-1001', phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice', profileName: 'Mrs. Ice',
}, },
{ {
title: 'Ms. Earth',
phoneNumber: '(202) 555-1002', phoneNumber: '(202) 555-1002',
name: 'Ms. Earth', name: 'Ms. Earth',
}, },
@ -99,6 +109,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -107,14 +118,17 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: '(202) 555-1000',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
isMe: true, isMe: true,
}, },
{ {
title: 'Mrs. Ice',
phoneNumber: '(202) 555-1001', phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice', profileName: 'Mrs. Ice',
}, },
{ {
title: 'Ms. Earth',
phoneNumber: '(202) 555-1002', phoneNumber: '(202) 555-1002',
name: 'Ms. Earth', name: 'Ms. Earth',
}, },
@ -125,6 +139,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -133,6 +148,7 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: 'Mr. Fire',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire', profileName: 'Mr. Fire',
}, },
@ -143,6 +159,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
isMe: true, isMe: true,
@ -152,6 +169,7 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: 'Mr. Fire',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire', profileName: 'Mr. Fire',
}, },
@ -162,6 +180,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -170,6 +189,7 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: 'Mr. Fire',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire', profileName: 'Mr. Fire',
isMe: true, isMe: true,
@ -181,6 +201,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -189,11 +210,13 @@ const stories: Array<GroupNotificationStory> = [
type: 'add', type: 'add',
contacts: [ contacts: [
{ {
title: 'Mr. Fire',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire', profileName: 'Mr. Fire',
isMe: true, isMe: true,
}, },
{ {
title: 'Mrs. Ice',
phoneNumber: '(202) 555-1001', phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice', profileName: 'Mrs. Ice',
}, },
@ -209,6 +232,7 @@ const stories: Array<GroupNotificationStory> = [
[ [
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -217,14 +241,17 @@ const stories: Array<GroupNotificationStory> = [
type: 'remove', type: 'remove',
contacts: [ contacts: [
{ {
title: 'Mr. Fire',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire', profileName: 'Mr. Fire',
}, },
{ {
title: 'Mrs. Ice',
phoneNumber: '(202) 555-1001', phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice', profileName: 'Mrs. Ice',
}, },
{ {
title: 'Ms. Earth',
phoneNumber: '(202) 555-1002', phoneNumber: '(202) 555-1002',
name: 'Ms. Earth', name: 'Ms. Earth',
}, },
@ -235,6 +262,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -243,6 +271,7 @@ const stories: Array<GroupNotificationStory> = [
type: 'remove', type: 'remove',
contacts: [ contacts: [
{ {
title: 'Mr. Fire',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire', profileName: 'Mr. Fire',
}, },
@ -253,6 +282,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
isMe: true, isMe: true,
@ -262,6 +292,7 @@ const stories: Array<GroupNotificationStory> = [
type: 'remove', type: 'remove',
contacts: [ contacts: [
{ {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
isMe: true, isMe: true,
@ -278,6 +309,7 @@ const stories: Array<GroupNotificationStory> = [
[ [
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -291,6 +323,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
isMe: true, isMe: true,
@ -310,6 +343,7 @@ const stories: Array<GroupNotificationStory> = [
[ [
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },
@ -323,6 +357,7 @@ const stories: Array<GroupNotificationStory> = [
}, },
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
isMe: true, isMe: true,
@ -342,6 +377,7 @@ const stories: Array<GroupNotificationStory> = [
[ [
{ {
from: { from: {
title: 'Alice',
name: 'Alice', name: 'Alice',
phoneNumber: '(202) 555-1000', phoneNumber: '(202) 555-1000',
}, },

View file

@ -8,9 +8,10 @@ import { LocalizerType } from '../../types/Util';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
interface Contact { interface Contact {
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
name?: string; name?: string;
title: string;
isMe?: boolean; isMe?: boolean;
} }
@ -48,9 +49,11 @@ export class GroupNotification extends React.Component<Props> {
className="module-group-notification__contact" className="module-group-notification__contact"
> >
<ContactName <ContactName
title={contact.title}
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
profileName={contact.profileName} profileName={contact.profileName}
name={contact.name} name={contact.name}
i18n={i18n}
/> />
</span> </span>
); );
@ -128,9 +131,11 @@ export class GroupNotification extends React.Component<Props> {
const fromContact = ( const fromContact = (
<ContactName <ContactName
title={from.title}
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}
profileName={from.profileName} profileName={from.profileName}
name={from.name} name={from.name}
i18n={i18n}
/> />
); );

View file

@ -29,7 +29,7 @@ const baseDataProps: Pick<
| 'conversationType' | 'conversationType'
| 'previews' | 'previews'
| 'timestamp' | 'timestamp'
| 'authorPhoneNumber' | 'authorTitle'
> = { > = {
id: 'asdf', id: 'asdf',
canReply: true, canReply: true,
@ -38,7 +38,7 @@ const baseDataProps: Pick<
conversationType: 'direct', conversationType: 'direct',
previews: [], previews: [],
timestamp: Date.now(), timestamp: Date.now(),
authorPhoneNumber: '(202) 555-2001', authorTitle: '(202) 555-2001',
}; };
type MessageStory = [ type MessageStory = [

View file

@ -73,10 +73,10 @@ export type PropsData = {
timestamp: number; timestamp: number;
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
contact?: ContactType; contact?: ContactType;
authorTitle: string;
authorName?: string; authorName?: string;
authorProfileName?: string; authorProfileName?: string;
/** Note: this should be formatted for display */ authorPhoneNumber?: string;
authorPhoneNumber: string;
authorColor?: ColorType; authorColor?: ColorType;
conversationType: 'group' | 'direct'; conversationType: 'group' | 'direct';
attachments?: Array<AttachmentType>; attachments?: Array<AttachmentType>;
@ -86,8 +86,9 @@ export type PropsData = {
isFromMe: boolean; isFromMe: boolean;
sentAt: number; sentAt: number;
authorId: string; authorId: string;
authorPhoneNumber: string; authorPhoneNumber?: string;
authorProfileName?: string; authorProfileName?: string;
authorTitle: string;
authorName?: string; authorName?: string;
authorColor?: ColorType; authorColor?: ColorType;
referencedMessageNotFound: boolean; referencedMessageNotFound: boolean;
@ -483,12 +484,14 @@ export class Message extends React.PureComponent<Props, State> {
public renderAuthor() { public renderAuthor() {
const { const {
authorTitle,
authorName, authorName,
authorPhoneNumber, authorPhoneNumber,
authorProfileName, authorProfileName,
collapseMetadata, collapseMetadata,
conversationType, conversationType,
direction, direction,
i18n,
isSticker, isSticker,
isTapToView, isTapToView,
isTapToViewExpired, isTapToViewExpired,
@ -498,9 +501,11 @@ export class Message extends React.PureComponent<Props, State> {
return; return;
} }
const title = authorName ? authorName : authorPhoneNumber; if (
direction !== 'incoming' ||
if (direction !== 'incoming' || conversationType !== 'group' || !title) { conversationType !== 'group' ||
!authorTitle
) {
return null; return null;
} }
@ -515,10 +520,12 @@ export class Message extends React.PureComponent<Props, State> {
return ( return (
<div className={moduleName}> <div className={moduleName}>
<ContactName <ContactName
title={authorTitle}
phoneNumber={authorPhoneNumber} phoneNumber={authorPhoneNumber}
name={authorName} name={authorName}
profileName={authorProfileName} profileName={authorProfileName}
module={moduleName} module={moduleName}
i18n={i18n}
/> />
</div> </div>
); );
@ -847,6 +854,7 @@ export class Message extends React.PureComponent<Props, State> {
authorProfileName={quote.authorProfileName} authorProfileName={quote.authorProfileName}
authorName={quote.authorName} authorName={quote.authorName}
authorColor={quoteColor} authorColor={quoteColor}
authorTitle={quote.authorTitle}
referencedMessageNotFound={referencedMessageNotFound} referencedMessageNotFound={referencedMessageNotFound}
isFromMe={quote.isFromMe} isFromMe={quote.isFromMe}
withContentAbove={withContentAbove} withContentAbove={withContentAbove}
@ -917,6 +925,7 @@ export class Message extends React.PureComponent<Props, State> {
authorName, authorName,
authorPhoneNumber, authorPhoneNumber,
authorProfileName, authorProfileName,
authorTitle,
collapseMetadata, collapseMetadata,
authorColor, authorColor,
conversationType, conversationType,
@ -942,6 +951,7 @@ export class Message extends React.PureComponent<Props, State> {
name={authorName} name={authorName}
phoneNumber={authorPhoneNumber} phoneNumber={authorPhoneNumber}
profileName={authorProfileName} profileName={authorProfileName}
title={authorTitle}
size={28} size={28}
/> />
</div> </div>

View file

@ -9,7 +9,9 @@ import { ColorType, LocalizerType } from '../../types/Util';
interface Contact { interface Contact {
status: string; status: string;
phoneNumber: string;
title: string;
phoneNumber?: string;
name?: string; name?: string;
profileName?: string; profileName?: string;
avatarPath?: string; avatarPath?: string;
@ -49,7 +51,14 @@ export class MessageDetail extends React.Component<Props> {
public renderAvatar(contact: Contact) { public renderAvatar(contact: Contact) {
const { i18n } = this.props; const { i18n } = this.props;
const { avatarPath, color, phoneNumber, name, profileName } = contact; const {
avatarPath,
color,
phoneNumber,
name,
profileName,
title,
} = contact;
return ( return (
<Avatar <Avatar
@ -60,6 +69,7 @@ export class MessageDetail extends React.Component<Props> {
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
size={52} size={52}
/> />
); );
@ -123,6 +133,8 @@ export class MessageDetail extends React.Component<Props> {
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
name={contact.name} name={contact.name}
profileName={contact.profileName} profileName={contact.profileName}
title={contact.title}
i18n={i18n}
/> />
</div> </div>
{errors.map((error, index) => ( {errors.map((error, index) => (

View file

@ -17,7 +17,9 @@ const i18n = setupI18n('en', enMessages);
const getBaseProps = (isGroup = false): MessageRequestActionsProps => ({ const getBaseProps = (isGroup = false): MessageRequestActionsProps => ({
i18n, i18n,
conversationType: isGroup ? 'group' : 'direct', conversationType: isGroup ? 'group' : 'direct',
profileName: isGroup ? undefined : text('profileName', 'Cayce Bollard'), title: isGroup
? text('title', 'NYC Rock Climbers')
: text('title', 'Cayce Bollard'),
name: isGroup name: isGroup
? text('name', 'NYC Rock Climbers') ? text('name', 'NYC Rock Climbers')
: text('name', 'Cayce Bollard'), : text('name', 'Cayce Bollard'),

View file

@ -12,17 +12,19 @@ import { LocalizerType } from '../../types/Util';
export type Props = { export type Props = {
i18n: LocalizerType; i18n: LocalizerType;
onAccept(): unknown; onAccept(): unknown;
} & Omit<ContactNameProps, 'module'> & } & Omit<ContactNameProps, 'module' | 'i18n'> &
Omit< Omit<
MessageRequestActionsConfirmationProps, MessageRequestActionsConfirmationProps,
'i18n' | 'state' | 'onChangeState' 'i18n' | 'state' | 'onChangeState'
>; >;
// tslint:disable-next-line max-func-body-length
export const MessageRequestActions = ({ export const MessageRequestActions = ({
i18n, i18n,
name, name,
profileName, profileName,
phoneNumber, phoneNumber,
title,
conversationType, conversationType,
isBlocked, isBlocked,
onBlock, onBlock,
@ -45,6 +47,7 @@ export const MessageRequestActions = ({
name={name} name={name}
profileName={profileName} profileName={profileName}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
title={title}
conversationType={conversationType} conversationType={conversationType}
state={mrState} state={mrState}
onChangeState={setMrState} onChangeState={setMrState}
@ -66,6 +69,8 @@ export const MessageRequestActions = ({
name={name} name={name}
profileName={profileName} profileName={profileName}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
title={title}
i18n={i18n}
/> />
</strong>, </strong>,
]} ]}

View file

@ -21,7 +21,7 @@ export type Props = {
onDelete(): unknown; onDelete(): unknown;
state: MessageRequestState; state: MessageRequestState;
onChangeState(state: MessageRequestState): unknown; onChangeState(state: MessageRequestState): unknown;
} & Omit<ContactNameProps, 'module'>; } & Omit<ContactNameProps, 'module' | 'i18n'>;
// tslint:disable-next-line: max-func-body-length // tslint:disable-next-line: max-func-body-length
export const MessageRequestActionsConfirmation = ({ export const MessageRequestActionsConfirmation = ({
@ -29,6 +29,7 @@ export const MessageRequestActionsConfirmation = ({
name, name,
profileName, profileName,
phoneNumber, phoneNumber,
title,
conversationType, conversationType,
onBlock, onBlock,
onBlockAndDelete, onBlockAndDelete,
@ -55,6 +56,8 @@ export const MessageRequestActionsConfirmation = ({
name={name} name={name}
profileName={profileName} profileName={profileName}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
title={title}
i18n={i18n}
/>, />,
]} ]}
/> />
@ -95,6 +98,8 @@ export const MessageRequestActionsConfirmation = ({
name={name} name={name}
profileName={profileName} profileName={profileName}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
title={title}
i18n={i18n}
/>, />,
]} ]}
/> />
@ -135,6 +140,8 @@ export const MessageRequestActionsConfirmation = ({
name={name} name={name}
profileName={profileName} profileName={profileName}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
title={title}
i18n={i18n}
/>, />,
]} ]}
/> />

View file

@ -12,7 +12,8 @@ import { ContactName } from './ContactName';
interface Props { interface Props {
attachment?: QuotedAttachmentType; attachment?: QuotedAttachmentType;
authorPhoneNumber: string; authorTitle: string;
authorPhoneNumber?: string;
authorProfileName?: string; authorProfileName?: string;
authorName?: string; authorName?: string;
authorColor?: ColorType; authorColor?: ColorType;
@ -307,6 +308,7 @@ export class Quote extends React.Component<Props, State> {
const { const {
authorProfileName, authorProfileName,
authorPhoneNumber, authorPhoneNumber,
authorTitle,
authorName, authorName,
i18n, i18n,
isFromMe, isFromMe,
@ -327,6 +329,8 @@ export class Quote extends React.Component<Props, State> {
phoneNumber={authorPhoneNumber} phoneNumber={authorPhoneNumber}
name={authorName} name={authorName}
profileName={authorProfileName} profileName={authorProfileName}
title={authorTitle}
i18n={i18n}
/> />
)} )}
</div> </div>

View file

@ -16,6 +16,7 @@ export type Reaction = {
avatarPath?: string; avatarPath?: string;
name?: string; name?: string;
profileName?: string; profileName?: string;
title: string;
isMe?: boolean; isMe?: boolean;
phoneNumber?: string; phoneNumber?: string;
}; };
@ -156,6 +157,7 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
name={from.name} name={from.name}
profileName={from.profileName} profileName={from.profileName}
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}
title={from.title}
i18n={i18n} i18n={i18n}
/> />
</div> </div>
@ -168,6 +170,8 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
name={from.name} name={from.name}
profileName={from.profileName} profileName={from.profileName}
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}
title={from.title}
i18n={i18n}
/> />
)} )}
</div> </div>

View file

@ -5,7 +5,11 @@
<SafetyNumberNotification <SafetyNumberNotification
i18n={util.i18n} i18n={util.i18n}
isGroup={true} isGroup={true}
contact={{ phoneNumber: '(202) 500-1000', profileName: 'Mr. Fire' }} contact={{
phoneNumber: '(202) 500-1000',
profileName: 'Mr. Fire',
title: 'Mr. Fire',
}}
onVerify={() => console.log('onVerify')} onVerify={() => console.log('onVerify')}
/> />
</util.ConversationContext> </util.ConversationContext>
@ -18,7 +22,11 @@
<SafetyNumberNotification <SafetyNumberNotification
i18n={util.i18n} i18n={util.i18n}
isGroup={false} isGroup={false}
contact={{ phoneNumber: '(202) 500-1000', profileName: 'Mr. Fire' }} contact={{
phoneNumber: '(202) 500-1000',
profileName: 'Mr. Fire',
title: 'Mr. Fire',
}}
onVerify={() => console.log('onVerify')} onVerify={() => console.log('onVerify')}
/> />
</util.ConversationContext> </util.ConversationContext>

View file

@ -6,8 +6,9 @@ import { LocalizerType } from '../../types/Util';
interface ContactType { interface ContactType {
id: string; id: string;
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
title: string;
name?: string; name?: string;
} }
@ -48,7 +49,9 @@ export class SafetyNumberNotification extends React.Component<Props> {
name={contact.name} name={contact.name}
profileName={contact.profileName} profileName={contact.profileName}
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-safety-number-notification__contact" module="module-safety-number-notification__contact"
i18n={i18n}
/> />
</span>, </span>,
]} ]}

View file

@ -6,6 +6,7 @@
type="fromOther" type="fromOther"
phoneNumber="(202) 555-1000" phoneNumber="(202) 555-1000"
profileName="Mr. Fire" profileName="Mr. Fire"
title="Mr. Fire"
timespan="1 hour" timespan="1 hour"
i18n={util.i18n} i18n={util.i18n}
/> />
@ -13,6 +14,7 @@
type="fromOther" type="fromOther"
phoneNumber="(202) 555-1000" phoneNumber="(202) 555-1000"
profileName="Mr. Fire" profileName="Mr. Fire"
title="Mr. Fire"
disabled={true} disabled={true}
timespan="Off" timespan="Off"
i18n={util.i18n} i18n={util.i18n}
@ -27,12 +29,14 @@
<TimerNotification <TimerNotification
type="fromMe" type="fromMe"
phoneNumber="(202) 555-1000" phoneNumber="(202) 555-1000"
title="(202) 555-1000"
timespan="1 hour" timespan="1 hour"
i18n={util.i18n} i18n={util.i18n}
/> />
<TimerNotification <TimerNotification
type="fromMe" type="fromMe"
phoneNumber="(202) 555-1000" phoneNumber="(202) 555-1000"
title="(202) 555-1000"
disabled={true} disabled={true}
timespan="Off" timespan="Off"
i18n={util.i18n} i18n={util.i18n}
@ -47,12 +51,14 @@
<TimerNotification <TimerNotification
type="fromSync" type="fromSync"
phoneNumber="(202) 555-1000" phoneNumber="(202) 555-1000"
title="(202) 555-1000"
timespan="1 hour" timespan="1 hour"
i18n={util.i18n} i18n={util.i18n}
/> />
<TimerNotification <TimerNotification
type="fromSync" type="fromSync"
phoneNumber="(202) 555-1000" phoneNumber="(202) 555-1000"
title="(202) 555-1000"
disabled={true} disabled={true}
timespan="Off" timespan="Off"
i18n={util.i18n} i18n={util.i18n}

View file

@ -7,8 +7,9 @@ import { LocalizerType } from '../../types/Util';
export type PropsData = { export type PropsData = {
type: 'fromOther' | 'fromMe' | 'fromSync'; type: 'fromOther' | 'fromMe' | 'fromSync';
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
title: string;
name?: string; name?: string;
disabled: boolean; disabled: boolean;
timespan: string; timespan: string;
@ -27,6 +28,7 @@ export class TimerNotification extends React.Component<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
timespan, timespan,
type, type,
disabled, disabled,
@ -46,7 +48,9 @@ export class TimerNotification extends React.Component<Props> {
key="external-1" key="external-1"
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
name={name} name={name}
i18n={i18n}
/>, />,
timespan, timespan,
]} ]}

View file

@ -10,8 +10,9 @@ interface Props {
avatarPath?: string; avatarPath?: string;
color: ColorType; color: ColorType;
name?: string; name?: string;
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
title: string;
conversationType: 'group' | 'direct'; conversationType: 'group' | 'direct';
i18n: LocalizerType; i18n: LocalizerType;
} }
@ -24,6 +25,7 @@ export class TypingBubble extends React.PureComponent<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
title,
conversationType, conversationType,
i18n, i18n,
} = this.props; } = this.props;
@ -42,6 +44,7 @@ export class TypingBubble extends React.PureComponent<Props> {
name={name} name={name}
phoneNumber={phoneNumber} phoneNumber={phoneNumber}
profileName={profileName} profileName={profileName}
title={title}
size={28} size={28}
/> />
</div> </div>

View file

@ -7,8 +7,9 @@ import { LocalizerType } from '../../types/Util';
interface ContactType { interface ContactType {
id: string; id: string;
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
title: string;
name?: string; name?: string;
isMe: boolean; isMe: boolean;
} }
@ -63,7 +64,9 @@ export class UnsupportedMessage extends React.Component<Props> {
name={contact.name} name={contact.name}
profileName={contact.profileName} profileName={contact.profileName}
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-unsupported-message__contact" module="module-unsupported-message__contact"
i18n={i18n}
/> />
</span>, </span>,
]} ]}

View file

@ -8,9 +8,10 @@ import { LocalizerType } from '../../types/Util';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
interface Contact { interface Contact {
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
name?: string; name?: string;
title: string;
} }
export type PropsData = { export type PropsData = {
@ -56,7 +57,9 @@ export class VerificationNotification extends React.Component<Props> {
name={contact.name} name={contact.name}
profileName={contact.profileName} profileName={contact.profileName}
phoneNumber={contact.phoneNumber} phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-verification-notification__contact" module="module-verification-notification__contact"
i18n={i18n}
/>, />,
]} ]}
i18n={i18n} i18n={i18n}

View file

@ -24,7 +24,7 @@ export function renderAvatar({
const avatarPath = avatar && avatar.avatar && avatar.avatar.path; const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
const pending = avatar && avatar.avatar && avatar.avatar.pending; const pending = avatar && avatar.avatar && avatar.avatar.pending;
const name = getName(contact) || ''; const title = getName(contact) || '';
const spinnerSvgSize = size < 50 ? 'small' : 'normal'; const spinnerSvgSize = size < 50 ? 'small' : 'normal';
const spinnerSize = size < 50 ? '24px' : undefined; const spinnerSize = size < 50 ? '24px' : undefined;
@ -46,7 +46,7 @@ export function renderAvatar({
color="grey" color="grey"
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
name={name} title={title}
size={size} size={size}
/> />
); );

View file

@ -409,14 +409,11 @@ export class CallingClass {
call: Call call: Call
): CallDetailsType { ): CallDetailsType {
return { return {
avatarPath: conversation.getAvatarPath(), ...conversation.cachedProps,
callId: call.callId, callId: call.callId,
contactColor: conversation.getColor(),
isIncoming: call.isIncoming, isIncoming: call.isIncoming,
isVideoCall: call.isVideoCall, isVideoCall: call.isVideoCall,
name: conversation.getName(),
phoneNumber: conversation.getNumber(),
profileName: conversation.getProfileName(),
}; };
} }

View file

@ -16,14 +16,16 @@ import {
export type CallId = any; export type CallId = any;
export type CallDetailsType = { export type CallDetailsType = {
avatarPath?: string;
callId: CallId; callId: CallId;
contactColor?: ColorType;
isIncoming: boolean; isIncoming: boolean;
isVideoCall: boolean; isVideoCall: boolean;
avatarPath?: string;
color?: ColorType;
name?: string; name?: string;
phoneNumber: string; phoneNumber?: string;
profileName?: string; profileName?: string;
title: string;
}; };
export type CallingStateType = { export type CallingStateType = {
@ -221,10 +223,10 @@ async function showCallNotification(callDetails: CallDetailsType) {
if (!canNotify) { if (!canNotify) {
return; return;
} }
const { name, phoneNumber, profileName, isVideoCall } = callDetails; const { title, isVideoCall } = callDetails;
notify({ notify({
platform: window.platform, platform: window.platform,
title: `${name || phoneNumber} ${profileName || ''}`, title,
icon: isVideoCall icon: isVideoCall
? 'images/icons/v2/video-solid-24.svg' ? 'images/icons/v2/video-solid-24.svg'
: 'images/icons/v2/phone-right-solid-24.svg', : 'images/icons/v2/phone-right-solid-24.svg',

View file

@ -25,7 +25,7 @@ export type DBConversationType = {
export type ConversationType = { export type ConversationType = {
id: string; id: string;
uuid?: string; uuid?: string;
e164: string; e164?: string;
name?: string; name?: string;
profileName?: string; profileName?: string;
avatarPath?: string; avatarPath?: string;
@ -34,13 +34,13 @@ export type ConversationType = {
isBlocked?: boolean; isBlocked?: boolean;
isVerified?: boolean; isVerified?: boolean;
activeAt?: number; activeAt?: number;
timestamp: number; timestamp?: number;
inboxPosition: number; inboxPosition?: number;
lastMessage?: { lastMessage?: {
status: 'error' | 'sending' | 'sent' | 'delivered' | 'read'; status: 'error' | 'sending' | 'sent' | 'delivered' | 'read';
text: string; text: string;
}; };
phoneNumber: string; phoneNumber?: string;
membersCount?: number; membersCount?: number;
type: 'direct' | 'group'; type: 'direct' | 'group';
isMe: boolean; isMe: boolean;

View file

@ -1,9 +1,7 @@
import memoizee from 'memoizee'; import memoizee from 'memoizee';
import { fromPairs, isNumber } from 'lodash'; import { fromPairs, isNumber } from 'lodash';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { format } from '../../types/PhoneNumber';
import { LocalizerType } from '../../types/Util';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { import {
ConversationLookupType, ConversationLookupType,
@ -81,29 +79,11 @@ export const getMessagesByConversation = createSelector(
} }
); );
function getConversationTitle(
conversation: ConversationType,
options: { i18n: LocalizerType; ourRegionCode: string }
): string {
if (conversation.name) {
return conversation.name;
}
if (conversation.type === 'group') {
const { i18n } = options;
return i18n('unknownGroup');
}
return format(conversation.phoneNumber, options);
}
const collator = new Intl.Collator(); const collator = new Intl.Collator();
export const _getConversationComparator = ( // Note: we will probably want to put i18n and regionCode back when we are formatting
i18n: LocalizerType, // phone numbers and contacts from scratch here again.
ourRegionCode: string export const _getConversationComparator = () => {
) => {
return (left: ConversationType, right: ConversationType): number => { return (left: ConversationType, right: ConversationType): number => {
const leftTimestamp = left.timestamp; const leftTimestamp = left.timestamp;
const rightTimestamp = right.timestamp; const rightTimestamp = right.timestamp;
@ -132,16 +112,7 @@ export const _getConversationComparator = (
return 1; return 1;
} }
const leftTitle = getConversationTitle(left, { return collator.compare(left.title, right.title);
i18n,
ourRegionCode,
});
const rightTitle = getConversationTitle(right, {
i18n,
ourRegionCode,
});
return collator.compare(leftTitle, rightTitle);
}; };
}; };
export const getConversationComparator = createSelector( export const getConversationComparator = createSelector(

View file

@ -9,8 +9,6 @@ import {
describe('state/selectors/conversations', () => { describe('state/selectors/conversations', () => {
describe('#getLeftPaneList', () => { describe('#getLeftPaneList', () => {
it('sorts conversations based on timestamp then by intl-friendly title', () => { it('sorts conversations based on timestamp then by intl-friendly title', () => {
const i18n = (key: string) => key;
const regionCode = 'US';
const data: ConversationLookupType = { const data: ConversationLookupType = {
id1: { id1: {
id: 'id1', id: 'id1',
@ -133,7 +131,7 @@ describe('state/selectors/conversations', () => {
acceptedMessageRequest: true, acceptedMessageRequest: true,
}, },
}; };
const comparator = _getConversationComparator(i18n, regionCode); const comparator = _getConversationComparator();
const { conversations } = _getLeftPaneLists(data, comparator); const { conversations } = _getLeftPaneLists(data, comparator);
assert.strictEqual(conversations[0].name, 'First!'); assert.strictEqual(conversations[0].name, 'First!');

View file

@ -353,7 +353,7 @@
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "js/views/contact_list_view.js", "path": "js/views/contact_list_view.js",
"line": " this.$el.append(this.contactView.el);", "line": " this.$el.append(this.contactView.el);",
"lineNumber": 46, "lineNumber": 37,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-07-31T00:19:18.696Z", "updated": "2019-07-31T00:19:18.696Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -11546,7 +11546,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.tsx", "path": "ts/components/MainHeader.tsx",
"line": " this.inputRef = React.createRef();", "line": " this.inputRef = React.createRef();",
"lineNumber": 69, "lineNumber": 70,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"
@ -11555,7 +11555,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/SafetyNumberChangeDialog.js", "path": "ts/components/SafetyNumberChangeDialog.js",
"line": " const cancelButtonRef = React.createRef();", "line": " const cancelButtonRef = React.createRef();",
"lineNumber": 14, "lineNumber": 15,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z", "updated": "2020-06-23T06:48:06.829Z",
"reasonDetail": "Used to focus cancel button when dialog opens" "reasonDetail": "Used to focus cancel button when dialog opens"
@ -11573,7 +11573,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.js", "path": "ts/components/conversation/ConversationHeader.js",
"line": " this.menuTriggerRef = react_1.default.createRef();", "line": " this.menuTriggerRef = react_1.default.createRef();",
"lineNumber": 14, "lineNumber": 15,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-07-31T00:19:18.696Z", "updated": "2019-07-31T00:19:18.696Z",
"reasonDetail": "Used to reference popup menu" "reasonDetail": "Used to reference popup menu"
@ -11582,7 +11582,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx", "path": "ts/components/conversation/ConversationHeader.tsx",
"line": " this.menuTriggerRef = React.createRef();", "line": " this.menuTriggerRef = React.createRef();",
"lineNumber": 70, "lineNumber": 75,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-20T20:10:43.540Z", "updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Used to reference popup menu" "reasonDetail": "Used to reference popup menu"
@ -11626,7 +11626,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();", "line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
"lineNumber": 184, "lineNumber": 185,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-21T16:56:07.875Z" "updated": "2020-05-21T16:56:07.875Z"
}, },
@ -11634,7 +11634,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " > = React.createRef();", "line": " > = React.createRef();",
"lineNumber": 188, "lineNumber": 189,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-21T16:56:07.875Z" "updated": "2020-05-21T16:56:07.875Z"
}, },

View file

@ -42,6 +42,13 @@ export async function generateSecurityNumberBlock(
throw new Error('Could not load their key'); throw new Error('Could not load their key');
} }
if (!contact.e164) {
window.log.error(
'generateSecurityNumberBlock: Attempted to generate security number for contact with no e164'
);
return [];
}
const securityNumber = await generateSecurityNumber( const securityNumber = await generateSecurityNumber(
ourNumber, ourNumber,
ourKey, ourKey,