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",
"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": {
"message": "Avatar for contact $name$",
"description": "Used in the alt tag for the image avatar of a contact",

View File

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

View File

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

View File

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

View File

@ -360,18 +360,8 @@
: null;
return {
id: this.model.id,
name: this.model.getName(),
phoneNumber: this.model.getNumber(),
profileName: this.model.getProfileName(),
color: this.model.getColor(),
avatarPath: this.model.getAvatarPath(),
...this.model.cachedProps,
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'),
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;
}
}
@mixin dark-mouse-mode() {
.dark-theme.mouse-mode & {
@content;
}
}
@mixin ios-mouse-mode() {
.ios-theme.mouse-mode & {
@content;
}
}
@mixin dark-ios-keyboard-mode() {
.dark-theme.ios-theme.keyboard-mode & {
@content;

View File

@ -36,12 +36,6 @@
}
}
// Module: Contact Name
.module-contact-name__profile-name {
font-style: italic;
}
// Module: Message
// 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 {
&--audio {
text-align: center;
@ -2612,18 +2610,40 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
cursor: inherit;
padding-top: 8px;
padding-bottom: 8px;
padding: 8px;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
@include light-theme {
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 {
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 {
@ -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 {
@ -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 {
@include font-body-1-bold-italic;
}
@ -4380,7 +4486,10 @@ button.module-image__border-overlay:focus {
left: 0;
right: 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;

View File

@ -228,11 +228,10 @@ export class ConversationController {
}
/**
* 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
* contacts.
* database id of the given contact. In high trust mode, it may create new contacts,
* and it may merge contacts.
*
* lowTrust = uuid/e164 pairing came from source like GroupV1 member list
* highTrust = uuid/e164 pairing came from source like CDS
* highTrust = uuid/e164 pairing came from CDS, the server, or your own device
*/
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
ensureContactIds({

View File

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

View File

@ -1,6 +1,5 @@
import * as React from 'react';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { Avatar, Props as AvatarProps } from './Avatar';
import { useRestoreFocus } from '../util/hooks';
@ -22,14 +21,16 @@ export const AvatarPopup = (props: Props) => {
const focusRef = React.useRef<HTMLButtonElement>(null);
const {
i18n,
name,
profileName,
phoneNumber,
title,
onViewPreferences,
onViewArchive,
style,
} = props;
const hasProfileName = !isEmpty(profileName);
const shouldShowNumber = Boolean(name || profileName);
// 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">
<Avatar {...props} size={52} />
<div className="module-avatar-popup__profile__text">
<div className="module-avatar-popup__profile__name">
{hasProfileName ? profileName : phoneNumber}
</div>
{hasProfileName ? (
<div className="module-avatar-popup__profile__name">{title}</div>
{shouldShowNumber ? (
<div className="module-avatar-popup__profile__number">
{phoneNumber}
</div>

View File

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

View File

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

View File

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

View File

@ -114,18 +114,19 @@ export const CompositionArea = ({
showPickerHint,
clearShowPickerHint,
// Message Requests
messageRequestsEnabled,
acceptedMessageRequest,
conversationType,
isBlocked,
messageRequestsEnabled,
name,
onAccept,
onBlock,
onBlockAndDelete,
onUnblock,
onDelete,
profileName,
onUnblock,
phoneNumber,
profileName,
title,
}: Props) => {
const [disabled, setDisabled] = React.useState(false);
const [showMic, setShowMic] = React.useState(!startingText);
@ -333,6 +334,7 @@ export const CompositionArea = ({
name={name}
profileName={profileName}
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 { Emojify } from './conversation/Emojify';
import { InContactsIcon } from './InContactsIcon';
import { ColorType, LocalizerType } from '../types/Util';
interface Props {
phoneNumber: string;
title: string;
phoneNumber?: string;
isMe?: boolean;
name?: string;
color: ColorType;
verified: boolean;
color?: ColorType;
isVerified?: boolean;
profileName?: string;
avatarPath?: string;
i18n: LocalizerType;
@ -27,6 +29,7 @@ export class ContactListItem extends React.Component<Props> {
name,
phoneNumber,
profileName,
title,
} = this.props;
return (
@ -38,6 +41,7 @@ export class ContactListItem extends React.Component<Props> {
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
size={52}
/>
);
@ -51,21 +55,15 @@ export class ContactListItem extends React.Component<Props> {
isMe,
phoneNumber,
profileName,
verified,
title,
isVerified,
} = this.props;
const title = name ? name : phoneNumber;
const displayName = isMe ? i18n('you') : title;
const shouldShowIcon = Boolean(name);
const profileElement =
!isMe && profileName && !name ? (
<span className="module-contact-list-item__text__profile-name">
~<Emojify text={profileName} />
</span>
) : null;
const showNumber = isMe || name;
const showVerified = !isMe && verified;
const showNumber = Boolean(isMe || name || profileName);
const showVerified = !isMe && isVerified;
return (
<button
@ -78,7 +76,13 @@ export class ContactListItem extends React.Component<Props> {
{this.renderAvatar()}
<div className="module-contact-list-item__text">
<div className="module-contact-list-item__text__name">
<Emojify text={displayName} /> {profileElement}
<Emojify text={displayName} />
{shouldShowIcon ? (
<span>
{' '}
<InContactsIcon i18n={i18n} />
</span>
) : null}
</div>
<div className="module-contact-list-item__text__additional-data">
{showVerified ? (

View File

@ -12,9 +12,10 @@ import { ColorType, LocalizerType } from '../types/Util';
export type PropsData = {
id: string;
phoneNumber: string;
phoneNumber?: string;
color?: ColorType;
profileName?: string;
title: string;
name?: string;
type: 'group' | 'direct';
avatarPath?: string;
@ -54,6 +55,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
name,
phoneNumber,
profileName,
title,
} = this.props;
return (
@ -67,6 +69,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
size={52}
/>
{this.renderUnread()}
@ -97,6 +100,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
name,
phoneNumber,
profileName,
title,
} = this.props;
return (
@ -116,6 +120,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
phoneNumber={phoneNumber}
name={name}
profileName={profileName}
title={title}
i18n={i18n}
/>
)}
</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 = {
acceptCall: action('accept-call'),
callDetails: {
avatarPath: undefined,
callId: 0,
contactColor: 'ultramarine' as ColorType,
isIncoming: true,
isVideoCall: true,
avatarPath: undefined,
contactColor: 'ultramarine' as ColorType,
name: 'Rick Sanchez',
phoneNumber: '3051234567',
profileName: 'Rick Sanchez',
title: 'Rick Sanchez',
},
declineCall: action('decline-call'),
i18n,
@ -72,7 +74,7 @@ const permutations = [
storiesOf('Components/IncomingCallBar', module)
.add('Knobs Playground', () => {
const contactColor = select('contactColor', colors, 'ultramarine');
const color = select('color', colors, 'ultramarine');
const isVideoCall = boolean('isVideoCall', false);
const name = text(
'name',
@ -84,7 +86,7 @@ storiesOf('Components/IncomingCallBar', module)
{...defaultProps}
callDetails={{
...defaultProps.callDetails,
contactColor,
color,
isVideoCall,
name,
}}

View File

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

View File

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

View File

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

View File

@ -12,19 +12,44 @@ import { storiesOf } from '@storybook/react';
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,
color: 'signal-blue',
profileName: undefined,
name: 'Rick Sanchez',
phoneNumber: '3051234567',
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;
storiesOf('Components/SafetyNumberChangeDialog', module)
.add('Single Contact Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[contact]}
contacts={[contactWithAllData]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
@ -38,7 +63,12 @@ storiesOf('Components/SafetyNumberChangeDialog', module)
.add('Multi Contact Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[contact, contact, contact, contact]}
contacts={[
contactWithAllData,
contactWithJustProfile,
contactWithJustNumber,
contactWithNothing,
]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
@ -53,16 +83,16 @@ storiesOf('Components/SafetyNumberChangeDialog', module)
return (
<SafetyNumberChangeDialog
contacts={[
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contactWithAllData,
contactWithJustProfile,
contactWithJustNumber,
contactWithNothing,
contactWithAllData,
contactWithAllData,
contactWithAllData,
contactWithAllData,
contactWithAllData,
contactWithAllData,
]}
i18n={i18n}
onCancel={action('cancel')}

View File

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

View File

@ -13,11 +13,39 @@ import { storiesOf } from '@storybook/react';
const i18n = setupI18n('en', enMessages);
const contactWithAllData = {
name: 'Summer Smith',
phoneNumber: '(305) 123-4567',
isVerified: true,
} 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: {
title: 'Summer Smith',
isVerified: true,
} as ConversationType,
contact: contactWithAllData,
generateSafetyNumber: action('generate-safety-number'),
i18n,
safetyNumber: 'XXX',
@ -35,9 +63,9 @@ const permutations = [
title: 'Safety Number (not verified)',
props: {
contact: {
title: 'Morty Smith',
isVerified: false,
} as ConversationType,
...contactWithAllData,
verified: false,
},
},
},
{
@ -58,6 +86,24 @@ const permutations = [
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)

View File

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

View File

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

View File

@ -20,7 +20,7 @@ export class StartNewConversation extends React.PureComponent<Props> {
color="grey"
conversationType="direct"
i18n={i18n}
phoneNumber={phoneNumber}
title={phoneNumber}
size={52}
/>
<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 { Emojify } from './Emojify';
import { LocalizerType } from '../../types/Util';
export interface Props {
title: string;
phoneNumber?: string;
name?: string;
profileName?: string;
module?: string;
i18n: LocalizerType;
}
export class ContactName extends React.Component<Props> {
public render() {
const { phoneNumber, name, profileName, module } = this.props;
const { module, title } = this.props;
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 (
<span className={prefix} dir="auto">
<Emojify text={title || ''} />
{shouldShowProfile ? ' ' : null}
{profileElement}
</span>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,11 @@
<SafetyNumberNotification
i18n={util.i18n}
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')}
/>
</util.ConversationContext>
@ -18,7 +22,11 @@
<SafetyNumberNotification
i18n={util.i18n}
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')}
/>
</util.ConversationContext>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -409,14 +409,11 @@ export class CallingClass {
call: Call
): CallDetailsType {
return {
avatarPath: conversation.getAvatarPath(),
...conversation.cachedProps,
callId: call.callId,
contactColor: conversation.getColor(),
isIncoming: call.isIncoming,
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 CallDetailsType = {
avatarPath?: string;
callId: CallId;
contactColor?: ColorType;
isIncoming: boolean;
isVideoCall: boolean;
avatarPath?: string;
color?: ColorType;
name?: string;
phoneNumber: string;
phoneNumber?: string;
profileName?: string;
title: string;
};
export type CallingStateType = {
@ -221,10 +223,10 @@ async function showCallNotification(callDetails: CallDetailsType) {
if (!canNotify) {
return;
}
const { name, phoneNumber, profileName, isVideoCall } = callDetails;
const { title, isVideoCall } = callDetails;
notify({
platform: window.platform,
title: `${name || phoneNumber} ${profileName || ''}`,
title,
icon: isVideoCall
? 'images/icons/v2/video-solid-24.svg'
: 'images/icons/v2/phone-right-solid-24.svg',

View File

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

View File

@ -1,9 +1,7 @@
import memoizee from 'memoizee';
import { fromPairs, isNumber } from 'lodash';
import { createSelector } from 'reselect';
import { format } from '../../types/PhoneNumber';
import { LocalizerType } from '../../types/Util';
import { StateType } from '../reducer';
import {
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();
export const _getConversationComparator = (
i18n: LocalizerType,
ourRegionCode: string
) => {
// Note: we will probably want to put i18n and regionCode back when we are formatting
// phone numbers and contacts from scratch here again.
export const _getConversationComparator = () => {
return (left: ConversationType, right: ConversationType): number => {
const leftTimestamp = left.timestamp;
const rightTimestamp = right.timestamp;
@ -132,16 +112,7 @@ export const _getConversationComparator = (
return 1;
}
const leftTitle = getConversationTitle(left, {
i18n,
ourRegionCode,
});
const rightTitle = getConversationTitle(right, {
i18n,
ourRegionCode,
});
return collator.compare(leftTitle, rightTitle);
return collator.compare(left.title, right.title);
};
};
export const getConversationComparator = createSelector(

View File

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

View File

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

View File

@ -42,6 +42,13 @@ export async function generateSecurityNumberBlock(
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(
ourNumber,
ourKey,