Update to new design for avatars: individual/group icons/colors

And two initials.
This commit is contained in:
Scott Nonnenberg 2018-09-26 17:23:17 -07:00
parent cf16ced91c
commit 8f3e3b7aaf
21 changed files with 1210 additions and 1017 deletions

299
ts/components/Avatar.md Normal file
View file

@ -0,0 +1,299 @@
### With avatar
```jsx
<Avatar
size={28}
color="pink"
name="John Smith"
avatarPath={util.gifObjectUrl}
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="pink"
name="Puppies"
avatarPath={util.gifObjectUrl}
conversationType="group"
i18n={util.i18n}
/>
```
### With only name
```jsx
<Avatar
size={28}
color="blue"
name="John"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="green"
name="John Smith"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="red"
name="Puppies"
conversationType="group"
i18n={util.i18n}
/>
```
### Just phone number
```jsx
<Avatar
size={28}
color="pink"
phoneNumber="(555) 353-3433"
conversationType="direct"
i18n={util.i18n}
/>
```
### All colors
```jsx
<Avatar
size={28}
color="red"
name="Red"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="deep_orange"
name="Deep Orange"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="brown"
name="Broen"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="pink"
name="Pink"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="purple"
name="Purple"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="indigo"
name="Indigo"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="blue"
name="Blue"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="teal"
name="Teal"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="green"
name="Green"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="light_green"
name="Light Green"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="blue_grey"
name="Blue Grey"
conversationType="direct"
i18n={util.i18n}
/>
```
### 36px
```jsx
<Avatar
size={36}
color="teal"
name="John Smith"
avatarPath={util.gifObjectUrl}
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={36}
color="teal"
name="John"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={36}
color="teal"
name="John Smith"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={36}
color="teal"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={36}
color="teal"
name="Pupplies"
conversationType="group"
i18n={util.i18n}
/>
```
### 48px
```jsx
<Avatar
size={48}
color="teal"
name="John Smith"
avatarPath={util.gifObjectUrl}
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={48}
color="teal"
name="John"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={48}
color="teal"
name="John Smith"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={48}
color="teal"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={48}
color="teal"
name="Pupplies"
conversationType="group"
i18n={util.i18n}
/>
```
### 80px
```jsx
<Avatar
size={80}
color="teal"
name="John Smith"
avatarPath={util.gifObjectUrl}
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={80}
color="teal"
name="John"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={80}
color="teal"
name="John Smith"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={80}
color="teal"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={80}
color="teal"
name="Pupplies"
conversationType="group"
i18n={util.i18n}
/>
```
### Broken color
```jsx
<Avatar
size={28}
color="fake"
name="F"
conversationType="direct"
i18n={util.i18n}
/>
```
### Broken image
```jsx
<Avatar
size={28}
color="pink"
name="John Smith"
avatarPath="nonexistent"
conversationType="direct"
i18n={util.i18n}
/>
```
### Broken image for group
```jsx
<Avatar
size={28}
avatarPath="nonexistent"
color="pink"
name="Puppies"
avatarPath="nonexistent"
conversationType="group"
i18n={util.i18n}
/>
```

118
ts/components/Avatar.tsx Normal file
View file

@ -0,0 +1,118 @@
import React from 'react';
import classNames from 'classnames';
import { getInitials } from '../util/getInitials';
import { Localizer } from '../types/Util';
interface Props {
avatarPath?: string;
color?: string;
conversationType: 'group' | 'direct';
i18n: Localizer;
name?: string;
phoneNumber?: string;
profileName?: string;
size: number;
}
interface State {
imageBroken: boolean;
}
export class Avatar extends React.Component<Props, State> {
public handleImageErrorBound: () => void;
public constructor(props: Props) {
super(props);
this.handleImageErrorBound = this.handleImageError.bind(this);
this.state = {
imageBroken: false,
};
}
public handleImageError() {
// tslint:disable-next-line no-console
console.log('Avatar: Image failed to load; failing over to placeholder');
this.setState({
imageBroken: true,
});
}
public renderImage() {
const { avatarPath, i18n, name, phoneNumber, profileName } = this.props;
const { imageBroken } = this.state;
const hasImage = avatarPath && !imageBroken;
if (!hasImage) {
return null;
}
const title = `${name || phoneNumber}${
!name && profileName ? ` ~${profileName}` : ''
}`;
return (
<img
onError={this.handleImageErrorBound}
alt={i18n('contactAvatarAlt', [title])}
src={avatarPath}
/>
);
}
public renderNoImage() {
const { conversationType, name, size } = this.props;
const initials = getInitials(name);
const isGroup = conversationType === 'group';
if (!isGroup && initials) {
return (
<div
className={classNames(
'module-avatar__label',
`module-avatar__label--${size}`
)}
>
{initials}
</div>
);
}
return (
<div
className={classNames(
'module-avatar__icon',
`module-avatar__icon--${conversationType}`,
`module-avatar__icon--${size}`
)}
/>
);
}
public render() {
const { avatarPath, color, size } = this.props;
const { imageBroken } = this.state;
const hasImage = avatarPath && !imageBroken;
if (size !== 28 && size !== 36 && size !== 48 && size !== 80) {
throw new Error(`Size ${size} is not supported!`);
}
return (
<div
className={classNames(
'module-avatar',
`module-avatar--${size}`,
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
!hasImage ? `module-avatar--${color}` : null
)}
>
{hasImage ? this.renderImage() : this.renderNoImage()}
</div>
);
}
}

View file

@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from './Avatar';
import { Emojify } from './conversation/Emojify';
import { Localizer } from '../types/Util';
@ -17,35 +18,28 @@ interface Props {
onClick?: () => void;
}
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
export class ContactListItem extends React.Component<Props> {
public renderAvatar({ displayName }: { displayName: string }) {
const { avatarPath, i18n, color, name } = this.props;
if (avatarPath) {
return (
<div className="module-contact-list-item__avatar">
<img alt={i18n('contactAvatarAlt', [displayName])} src={avatarPath} />
</div>
);
}
const title = name ? getInitial(name) : '#';
public renderAvatar() {
const {
avatarPath,
i18n,
color,
name,
phoneNumber,
profileName,
} = this.props;
return (
<div
className={classNames(
'module-contact-list-item__avatar-default',
`module-contact-list-item__avatar-default--${color}`
)}
>
<div className="module-contact-list-item__avatar-default__label">
{title}
</div>
</div>
<Avatar
avatarPath={avatarPath}
color={color}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
size={48}
/>
);
}
@ -82,7 +76,7 @@ export class ContactListItem extends React.Component<Props> {
onClick ? 'module-contact-list-item--with-click-handler' : null
)}
>
{this.renderAvatar({ displayName })}
{this.renderAvatar()}
<div className="module-contact-list-item__text">
<div className="module-contact-list-item__text__name">
<Emojify text={displayName} i18n={i18n} /> {profileElement}

View file

@ -1,154 +1,175 @@
#### With name and profile
```jsx
<ConversationListItem
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
avatarPath={util.gifObjectUrl}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: "What's going on?",
status: 'sent',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
```
#### Profile, with name, no avatar
```jsx
<ConversationListItem
phoneNumber="(202) 555-0011"
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Just a second',
status: 'read',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
```
#### All types of status
```jsx
<div>
<util.LeftPaneContext theme={util.theme}>
<ConversationListItem
name="Someone 🔥 Somewhere"
conversationType={'direct'}
phoneNumber="(202) 555-0011"
name="Mr. Fire🔥"
color="green"
avatarPath={util.gifObjectUrl}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Sending',
status: 'sending',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Sent',
text: "What's going on?",
status: 'sent',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</util.LeftPaneContext>
```
#### Profile, with name, no avatar
```jsx
<util.LeftPaneContext theme={util.theme}>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Delivered',
status: 'delivered',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Read',
text: 'Just a second',
status: 'read',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Error',
status: 'error',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### All types of status
```jsx
<util.LeftPaneContext theme={util.theme}>
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Sending',
status: 'sending',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Sent',
status: 'sent',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Delivered',
status: 'delivered',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Read',
status: 'read',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Error',
status: 'error',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### With unread
```jsx
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
unreadCount={4}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
unreadCount={10}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
unreadCount={250}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
<util.LeftPaneContext theme={util.theme}>
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
unreadCount={4}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
unreadCount={10}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
unreadCount={250}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### Selected
```jsx
<ConversationListItem
phoneNumber="(202) 555-0011"
isSelected={true}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<util.LeftPaneContext theme={util.theme}>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
isSelected={true}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Hey there!',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</util.LeftPaneContext>
```
#### With emoji/links in message, no status
@ -156,26 +177,30 @@
We don't want Jumbomoji or links.
```jsx
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Download at http://signal.org',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: '🔥',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
<util.LeftPaneContext theme={util.theme}>
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Download at http://signal.org',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: '🔥',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### Long content
@ -183,72 +208,80 @@ We don't want Jumbomoji or links.
We only show one line.
```jsx
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
name="Long contact name. Esquire. The third. And stuff. And more! And more!"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Normal message',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
status: 'read',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<util.LeftPaneContext theme={util.theme}>
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Long contact name. Esquire. The third. And stuff. And more! And more!"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Normal message',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
status: 'read',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
unreadCount={8}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Many lines. This is a many-line message.\nLine 2 is really exciting but it shouldn't be seen.\nLine three is even better.\nLine 4, well.",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Many lines. This is a many-line message.\nLine 2 is really exciting but it shouldn't be seen.\nLine three is even better.\nLine 4, well.",
status: 'delivered',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
unreadCount={8}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Many lines. This is a many-line message.\nLine 2 is really exciting but it shouldn't be seen.\nLine three is even better.\nLine 4, well.",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Many lines. This is a many-line message.\nLine 2 is really exciting but it shouldn't be seen.\nLine three is even better.\nLine 4, well.",
status: 'delivered',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### More narrow
@ -256,104 +289,119 @@ We only show one line.
On platforms that show scrollbars all the time, this is true all the time.
```jsx
<div style={{ width: '280px' }}>
<ConversationListItem
phoneNumber="(202) 555-0011"
name="Long contact name. Esquire. The third. And stuff. And more! And more!"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Normal message',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
<util.LeftPaneContext theme={util.theme}>
<div style={{ width: '280px' }}>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Long contact name. Esquire. The third. And stuff. And more! And more!"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Normal message',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text:
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### With various ages
```jsx
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 60 * 1000}
lastMessage={{
text: 'Five hours ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 24 * 60 * 60 * 1000}
lastMessage={{
text: 'One day ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 7 * 24 * 60 * 60 * 1000}
lastMessage={{
text: 'One week ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 365 * 24 * 60 * 60 * 1000}
lastMessage={{
text: 'One year ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
<util.LeftPaneContext theme={util.theme}>
<div>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 60 * 1000}
lastMessage={{
text: 'Five hours ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 24 * 60 * 60 * 1000}
lastMessage={{
text: 'One day ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 7 * 24 * 60 * 60 * 1000}
lastMessage={{
text: 'One week ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 365 * 24 * 60 * 60 * 1000}
lastMessage={{
text: 'One year ago',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### Missing data
```jsx
<div>
<ConversationListItem
name="John"
lastUpdated={null}
lastMessage={{
text: 'Missing last updated',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
name="Missing message"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: null,
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: null,
status: 'sent',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
<util.LeftPaneContext theme={util.theme}>
<div>
<ConversationListItem
name="John"
conversationType={'direct'}
lastUpdated={null}
lastMessage={{
text: 'Missing last updated',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
name="Missing message"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: null,
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
<ConversationListItem
phoneNumber="(202) 555-0011"
conversationType={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: null,
status: 'sent',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```

View file

@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from './Avatar';
import { MessageBody } from './conversation/MessageBody';
import { Timestamp } from './conversation/Timestamp';
import { ContactName } from './conversation/ContactName';
@ -11,6 +12,7 @@ interface Props {
profileName?: string;
name?: string;
color?: string;
conversationType: 'group' | 'direct';
avatarPath?: string;
lastUpdated: number;
@ -26,50 +28,29 @@ interface Props {
onClick?: () => void;
}
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
export class ConversationListItem extends React.Component<Props> {
public renderAvatar() {
const {
avatarPath,
color,
conversationType,
i18n,
name,
phoneNumber,
profileName,
} = this.props;
if (!avatarPath) {
const initial = getInitial(name || '');
return (
<div className="module-conversation-list-item__avatar-container">
<div
className={classNames(
'module-conversation-list-item__avatar',
'module-conversation-list-item__default-avatar',
`module-conversation-list-item__default-avatar--${color}`
)}
>
{initial}
</div>
{this.renderUnread()}
</div>
);
}
const title = `${name || phoneNumber}${
!name && profileName ? ` ~${profileName}` : ''
}`;
return (
<div className="module-conversation-list-item__avatar-container">
<img
className="module-conversation-list-item__avatar"
alt={i18n('contactAvatarAlt', [title])}
src={avatarPath}
<Avatar
avatarPath={avatarPath}
color={color}
conversationType={conversationType}
i18n={i18n}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
size={48}
/>
{this.renderUnread()}
</div>

View file

@ -207,7 +207,9 @@ export class ContactDetail extends React.Component<Props> {
return (
<div className="module-contact-detail">
{renderAvatar({ contact, i18n, module })}
<div className="module-contact-detail__avatar">
{renderAvatar({ contact, i18n, size: 80 })}
</div>
{renderName({ contact, isIncoming, module })}
{renderContactShorthand({ contact, isIncoming, module })}
{this.renderSendMessage({ hasSignalAccount, i18n, onSendMessage })}

View file

@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Emojify } from './Emojify';
import { Avatar } from '../Avatar';
import { Localizer } from '../../types/Util';
import {
ContextMenu,
@ -45,10 +45,6 @@ interface Props {
onGoBack: () => void;
}
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
export class ConversationHeader extends React.Component<Props> {
public captureMenuTriggerBound: (trigger: any) => void;
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
@ -116,37 +112,25 @@ export class ConversationHeader extends React.Component<Props> {
avatarPath,
color,
i18n,
isGroup,
name,
phoneNumber,
profileName,
} = this.props;
if (!avatarPath) {
const initial = getInitial(name || '');
return (
<div
className={classNames(
'module-conversation-header___avatar',
'module-conversation-header___default-avatar',
`module-conversation-header___default-avatar--${color}`
)}
>
{initial}
</div>
);
}
const title = `${name || phoneNumber}${
!name && profileName ? ` ~${profileName}` : ''
}`;
return (
<img
className="module-conversation-header___avatar"
alt={i18n('contactAvatarAlt', [title])}
src={avatarPath}
/>
<span className="module-conversation-header__avatar">
<Avatar
avatarPath={avatarPath}
color={color}
conversationType={isGroup ? 'group' : 'direct'}
i18n={i18n}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
size={28}
/>
</span>
);
}

View file

@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from '../Avatar';
import { Contact, getName } from '../../types/Contact';
import { Localizer } from '../../types/Util';
@ -41,7 +42,7 @@ export class EmbeddedContact extends React.Component<Props> {
role="button"
onClick={onClick}
>
{renderAvatar({ contact, i18n, module })}
{renderAvatar({ contact, i18n, size: 48 })}
<div className="module-embedded-contact__text-container">
{renderName({ contact, isIncoming, module })}
{renderContactShorthand({ contact, isIncoming, module })}
@ -53,40 +54,29 @@ export class EmbeddedContact extends React.Component<Props> {
// Note: putting these below the main component so style guide picks up EmbeddedContact
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
export function renderAvatar({
contact,
i18n,
module,
size,
}: {
contact: Contact;
i18n: Localizer;
module: string;
size: number;
}) {
const { avatar } = contact;
const path = avatar && avatar.avatar && avatar.avatar.path;
const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
const name = getName(contact) || '';
if (!path) {
const initials = getInitial(name);
return (
<div className={`module-${module}__image-container`}>
<div className={`module-${module}__image-container__default-avatar`}>
{initials}
</div>
</div>
);
}
return (
<div className={`module-${module}__image-container`}>
<img src={path} alt={i18n('contactAvatarAlt', [name])} />
</div>
<Avatar
avatarPath={avatarPath}
color="grey"
conversationType="direct"
i18n={i18n}
name={name}
size={size}
/>
);
}

View file

@ -6,6 +6,7 @@ import {
isVideoTypeSupported,
} from '../../util/GoogleChrome';
import { Avatar } from '../Avatar';
import { MessageBody } from './MessageBody';
import { ExpireTimer, getIncrement } from './ExpireTimer';
import { Timestamp } from './Timestamp';
@ -133,10 +134,6 @@ function canDisplayImage(attachment?: Attachment) {
return height > 0 && height <= 4096 && width > 0 && width <= 4096;
}
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
function getExtension({
fileName,
contentType,
@ -633,21 +630,17 @@ export class Message extends React.Component<Props, State> {
public renderAvatar() {
const {
authorAvatarPath,
authorName,
authorPhoneNumber,
authorProfileName,
authorAvatarPath,
conversationColor,
collapseMetadata,
conversationColor,
conversationType,
direction,
i18n,
} = this.props;
const title = `${authorName || authorPhoneNumber}${
!authorName && authorProfileName ? ` ~${authorProfileName}` : ''
}`;
if (
collapseMetadata ||
conversationType !== 'group' ||
@ -656,26 +649,18 @@ export class Message extends React.Component<Props, State> {
return;
}
if (!authorAvatarPath) {
const label = authorName ? getInitial(authorName) : '#';
return (
<div
className={classNames(
'module-message__author-default-avatar',
`module-message__author-default-avatar--${conversationColor}`
)}
>
<div className="module-message__author-default-avatar__label">
{label}
</div>
</div>
);
}
return (
<div className="module-message__author-avatar">
<img alt={i18n('contactAvatarAlt', [title])} src={authorAvatarPath} />
<Avatar
avatarPath={authorAvatarPath}
color={conversationColor}
conversationType="direct"
i18n={i18n}
name={authorName}
phoneNumber={authorPhoneNumber}
profileName={authorProfileName}
size={36}
/>
</div>
);
}

View file

@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import moment from 'moment';
import { Avatar } from '../Avatar';
import { ContactName } from './ContactName';
import { Message, Props as MessageProps } from './Message';
import { Localizer } from '../../types/Util';
@ -31,40 +32,21 @@ interface Props {
i18n: Localizer;
}
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
export class MessageDetail extends React.Component<Props> {
public renderAvatar(contact: Contact) {
const { i18n } = this.props;
const { avatarPath, color, phoneNumber, name, profileName } = contact;
if (!avatarPath) {
const initial = getInitial(name || '');
return (
<div
className={classNames(
'module-message-detail__contact__avatar',
'module-message-detail__contact__default-avatar',
`module-message-detail__contact__default-avatar--${color}`
)}
>
{initial}
</div>
);
}
const title = `${name || phoneNumber}${
!name && profileName ? ` ~${profileName}` : ''
}`;
return (
<img
className="module-message-detail__contact__avatar"
alt={i18n('contactAvatarAlt', [title])}
src={avatarPath}
<Avatar
avatarPath={avatarPath}
color={color}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
size={48}
/>
);
}

View file

@ -0,0 +1,25 @@
import React from 'react';
import classNames from 'classnames';
interface Props {
/**
* Corresponds to the theme setting in the app, and the class added to the root element.
*/
theme: 'light-theme' | 'dark-theme';
}
/**
* Provides the parent elements necessary to allow the main Signal Desktop stylesheet to
* apply (with no changes) to messages in the Style Guide.
*/
export class LeftPaneContext extends React.Component<Props> {
public render() {
const { theme } = this.props;
return (
<div className={classNames(theme || 'light-theme')}>
<div className="gutter">{this.props.children}</div>
</div>
);
}
}

View file

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { default as _ } from 'lodash';
export { ConversationContext } from './ConversationContext';
export { LeftPaneContext } from './LeftPaneContext';
export { _, classNames };

21
ts/util/getInitials.ts Normal file
View file

@ -0,0 +1,21 @@
const BAD_CHARACTERS = /[^A-Za-z\s]+/g;
const WHITESPACE = /\s+/g;
function removeNonInitials(name: string) {
return name.replace(BAD_CHARACTERS, '').replace(WHITESPACE, ' ');
}
export function getInitials(name?: string): string | null {
if (!name) {
return null;
}
const cleaned = removeNonInitials(name);
const parts = cleaned.split(' ');
const initials = parts.map(part => part.trim()[0]);
if (!initials.length) {
return null;
}
return initials.slice(0, 2).join('');
}