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

22
images/profile-group.svg Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Group/group-28</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M18.7272727,13.2857143 C20.6890909,13.2857143 22.2609091,11.6585714 22.2609091,9.64285714 C22.2609091,7.62714286 20.6890909,6 18.7272727,6 C16.7654545,6 15.1818182,7.62714286 15.1818182,9.64285714 C15.1818182,11.6585714 16.7654545,13.2857143 18.7272727,13.2857143 Z M9.27272727,13.2857143 C11.2345455,13.2857143 12.8063636,11.6585714 12.8063636,9.64285714 C12.8063636,7.62714286 11.2345455,6 9.27272727,6 C7.31090909,6 5.72727273,7.62714286 5.72727273,9.64285714 C5.72727273,11.6585714 7.31090909,13.2857143 9.27272727,13.2857143 Z M9.27272727,15.7142857 C6.51909091,15.7142857 1,17.135 1,19.9642857 L1,23 L17.5454545,23 L17.5454545,19.9642857 C17.5454545,17.135 12.0263636,15.7142857 9.27272727,15.7142857 Z M18.7272727,15.7142857 C18.3845455,15.7142857 17.9945455,15.7385714 17.5809091,15.775 C18.9518182,16.795 19.9090909,18.1671429 19.9090909,19.9642857 L19.9090909,23 L27,23 L27,19.9642857 C27,17.135 21.4809091,15.7142857 18.7272727,15.7142857 Z" id="path-1"></path>
<rect id="path-3" x="0" y="0" width="28" height="28"></rect>
</defs>
<g id="Group/group-28" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Color/UI/Black" mask="url(#mask-2)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<use id="fill" fill="#000000" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Profile/profile-28</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M14,14 C16.7625,14 19,11.7625 19,9 C19,6.2375 16.7625,4 14,4 C11.2375,4 9,6.2375 9,9 C9,11.7625 11.2375,14 14,14 Z M14,16.5 C10.6625,16.5 4,18.175 4,21.5 L4,24 L24,24 L24,21.5 C24,18.175 17.3375,16.5 14,16.5 Z" id="path-1"></path>
<rect id="path-3" x="0" y="0" width="28" height="28"></rect>
</defs>
<g id="Profile/profile-28" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Color/UI/Black" mask="url(#mask-2)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<use id="fill" fill="#000000" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -174,8 +174,6 @@
format() { format() {
const { format } = PhoneNumber; const { format } = PhoneNumber;
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
const avatar = this.getAvatar();
const color = this.getColor(); const color = this.getColor();
return { return {
@ -183,7 +181,7 @@
ourRegionCode: regionCode, ourRegionCode: regionCode,
}), }),
color, color,
avatarPath: avatar ? avatar.url : null, avatarPath: this.getAvatarPath(),
name: this.getName(), name: this.getName(),
profileName: this.getProfileName(), profileName: this.getProfileName(),
title: this.getTitle(), title: this.getTitle(),
@ -192,6 +190,7 @@
getPropsForListItem() { getPropsForListItem() {
const result = { const result = {
...this.format(), ...this.format(),
conversationType: this.isPrivate() ? 'direct' : 'group',
lastUpdated: this.get('timestamp'), lastUpdated: this.get('timestamp'),
unreadCount: this.get('unreadCount') || 0, unreadCount: this.get('unreadCount') || 0,
@ -1369,6 +1368,15 @@
const { migrateColor } = Util; const { migrateColor } = Util;
return migrateColor(this.get('color')); return migrateColor(this.get('color'));
}, },
getAvatarPath() {
const avatar = this.get('avatar') || this.get('profileAvatar');
if (avatar && avatar.path) {
return getAbsoluteAttachmentPath(avatar.path);
}
return null;
},
getAvatar() { getAvatar() {
const title = this.get('name'); const title = this.get('name');
const color = this.getColor(); const color = this.getColor();

View file

@ -300,7 +300,6 @@
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
const contactModel = this.findContact(phoneNumber); const contactModel = this.findContact(phoneNumber);
const avatar = contactModel ? contactModel.getAvatar() : null;
const color = contactModel ? contactModel.getColor() : null; const color = contactModel ? contactModel.getColor() : null;
return { return {
@ -308,7 +307,7 @@
ourRegionCode: regionCode, ourRegionCode: regionCode,
}), }),
color, color,
avatarPath: avatar ? avatar.url : null, avatarPath: contactModel ? contactModel.getAvatarPath() : null,
name: contactModel ? contactModel.getName() : null, name: contactModel ? contactModel.getName() : null,
profileName: contactModel ? contactModel.getProfileName() : null, profileName: contactModel ? contactModel.getProfileName() : null,
title: contactModel ? contactModel.getTitle() : null, title: contactModel ? contactModel.getTitle() : null,
@ -394,8 +393,9 @@
const contact = this.findAndFormatContact(phoneNumber); const contact = this.findAndFormatContact(phoneNumber);
const contactModel = this.findContact(phoneNumber); const contactModel = this.findContact(phoneNumber);
const authorAvatar = contactModel ? contactModel.getAvatar() : null; const authorAvatarPath = contactModel
const authorAvatarPath = authorAvatar ? authorAvatar.url : null; ? contactModel.getAvatarPath()
: null;
const expirationLength = this.get('expireTimer') * 1000; const expirationLength = this.get('expireTimer') * 1000;
const expireTimerStart = this.get('expirationStartTimestamp'); const expireTimerStart = this.get('expirationStartTimestamp');
@ -530,10 +530,16 @@
return null; return null;
} }
const { format } = PhoneNumber;
const regionCode = storage.get('regionCode');
const conversation = this.getConversation();
const { author, id, referencedMessageNotFound } = quote; const { author, id, referencedMessageNotFound } = quote;
const contact = author && ConversationController.get(author); const contact = author && ConversationController.get(author);
const authorPhoneNumber = author; const authorPhoneNumber = format(author, {
ourRegionCode: regionCode,
});
const authorProfileName = contact ? contact.getProfileName() : null; const authorProfileName = contact ? contact.getProfileName() : null;
const authorName = contact ? contact.getName() : null; const authorName = contact ? contact.getName() : null;
const isFromMe = contact ? contact.id === this.OUR_NUMBER : false; const isFromMe = contact ? contact.id === this.OUR_NUMBER : false;
@ -556,6 +562,7 @@
authorPhoneNumber, authorPhoneNumber,
authorProfileName, authorProfileName,
authorName, authorName,
conversationColor: conversation && conversation.getColor(),
onClick, onClick,
referencedMessageNotFound, referencedMessageNotFound,
}; };

View file

@ -25,9 +25,6 @@
this.contactView = null; this.contactView = null;
} }
const avatar = this.model.getAvatar();
const avatarPath = avatar && avatar.url;
const color = avatar && avatar.color;
const isMe = this.ourNumber === this.model.id; const isMe = this.ourNumber === this.model.id;
this.contactView = new Whisper.ReactWrapperView({ this.contactView = new Whisper.ReactWrapperView({
@ -35,8 +32,8 @@
Component: window.Signal.Components.ContactListItem, Component: window.Signal.Components.ContactListItem,
props: { props: {
isMe, isMe,
color, color: this.model.getColor(),
avatarPath, avatarPath: this.model.getAvatarPath(),
phoneNumber: this.model.getNumber(), phoneNumber: this.model.getNumber(),
name: this.model.getName(), name: this.model.getName(),
profileName: this.model.getProfileName(), profileName: this.model.getProfileName(),

View file

@ -145,8 +145,6 @@
}); });
const getHeaderProps = () => { const getHeaderProps = () => {
const avatar = this.model.getAvatar();
const avatarPath = avatar ? avatar.url : null;
const expireTimer = this.model.get('expireTimer'); const expireTimer = this.model.get('expireTimer');
const expirationSettingName = expireTimer const expirationSettingName = expireTimer
? Whisper.ExpirationTimerOptions.getName(expireTimer || 0) ? Whisper.ExpirationTimerOptions.getName(expireTimer || 0)
@ -158,7 +156,7 @@
phoneNumber: this.model.getNumber(), phoneNumber: this.model.getNumber(),
profileName: this.model.getProfileName(), profileName: this.model.getProfileName(),
color: this.model.getColor(), color: this.model.getColor(),
avatarPath, avatarPath: this.model.getAvatarPath(),
isVerified: this.model.isVerified(), isVerified: this.model.isVerified(),
isMe: this.model.isMe(), isMe: this.model.isMe(),
isGroup: !this.model.isPrivate(), isGroup: !this.model.isPrivate(),
@ -1401,6 +1399,7 @@
} }
const message = new Whisper.Message({ const message = new Whisper.Message({
conversationId: this.model.id,
quote: this.quote, quote: this.quote,
}); });
message.quotedMessage = this.quotedMessage; message.quotedMessage = this.quotedMessage;

View file

@ -560,74 +560,6 @@
// This accounts for the weird extra 3px we get at the bottom of messages // This accounts for the weird extra 3px we get at the bottom of messages
bottom: -3px; bottom: -3px;
right: calc(100% + 4px); right: calc(100% + 4px);
img {
height: 36px;
width: 36px;
border-radius: 18px;
object-fit: cover;
}
}
.module-message__author-default-avatar {
position: absolute;
bottom: 0px;
right: calc(100% + 4px);
height: 36px;
width: 36px;
border-radius: 18px;
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
// Default, in case we have no color
background-color: $color-conversation-grey;
}
.module-message__author-default-avatar--red {
background-color: $color-conversation-red;
}
.module-message__author-default-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-message__author-default-avatar--brown {
background-color: $color-conversation-brown;
}
.module-message__author-default-avatar--pink {
background-color: $color-conversation-pink;
}
.module-message__author-default-avatar--purple {
background-color: $color-conversation-purple;
}
.module-message__author-default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-message__author-default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-message__author-default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-message__author-default-avatar--green {
background-color: $color-conversation-green;
}
.module-message__author-default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-message__author-default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
}
.module-message__author-default-avatar__label {
width: 100%;
font-size: 18px;
color: $color-white;
// Because it just doesn't look properly centered
padding-right: 1px;
} }
// Module: Expire Timer // Module: Expire Timer
@ -1025,37 +957,6 @@
padding-bottom: 4px; padding-bottom: 4px;
} }
.module-embedded-contact__image-container {
flex: initial;
min-width: 50px;
width: 50px;
height: 50px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
object-fit: cover;
img {
border-radius: 50%;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.module-embedded-contact__image-container__default-avatar {
border-radius: 50%;
width: 100%;
height: 100%;
background-color: $color-conversation-grey;
color: $color-white;
font-size: 25px;
line-height: 52px;
}
.module-embedded-contact__text-container { .module-embedded-contact__text-container {
flex-grow: 1; flex-grow: 1;
margin-left: 8px; margin-left: 8px;
@ -1106,31 +1007,8 @@
margin-right: auto; margin-right: auto;
} }
.module-contact-detail__image-container { .module-contact-detail__avatar {
height: 80px;
width: 80px;
margin-bottom: 4px; margin-bottom: 4px;
text-align: center;
display: inline-block;
object-fit: cover;
img {
border-radius: 50%;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.module-contact-detail__image-container__default-avatar {
border-radius: 50%;
width: 100%;
height: 100%;
background-color: $color-conversation-grey;
color: $color-white;
font-size: 50px;
line-height: 82px;
} }
.module-contact-detail__contact-name { .module-contact-detail__contact-name {
@ -1366,69 +1244,6 @@
cursor: pointer; cursor: pointer;
} }
.module-contact-list-item__avatar {
display: inline-block;
img {
height: 44px;
width: 44px;
border-radius: 22px;
}
}
.module-contact-list-item__avatar-default {
height: 44px;
width: 44px;
border-radius: 22px;
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
background-color: $color-conversation-grey;
}
.module-contact-list-item__avatar-default--red {
background-color: $color-conversation-red;
}
.module-contact-list-item__avatar-default--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-contact-list-item__avatar-default--brown {
background-color: $color-conversation-brown;
}
.module-contact-list-item__avatar-default--pink {
background-color: $color-conversation-pink;
}
.module-contact-list-item__avatar-default--purple {
background-color: $color-conversation-purple;
}
.module-contact-list-item__avatar-default--indigo {
background-color: $color-conversation-indigo;
}
.module-contact-list-item__avatar-default--blue {
background-color: $color-conversation-blue;
}
.module-contact-list-item__avatar-default--teal {
background-color: $color-conversation-teal;
}
.module-contact-list-item__avatar-default--green {
background-color: $color-conversation-green;
}
.module-contact-list-item__avatar-default--light_green {
background-color: $color-conversation-light_green;
}
.module-contact-list-item__avatar-default--blue_grey {
background-color: $color-conversation-blue_grey;
}
.module-contact-list-item__avatar-default__label {
width: 100%;
color: $color-white;
font-size: 18px;
}
.module-contact-list-item__text { .module-contact-list-item__text {
margin-left: 8px; margin-left: 8px;
} }
@ -1506,54 +1321,8 @@
max-width: 100%; max-width: 100%;
} }
.module-conversation-header___avatar { .module-conversation-header__avatar {
height: 32px; min-width: 28px;
width: 32px;
min-width: 32px;
border-radius: 16px;
}
.module-conversation-header___default-avatar {
background-color: $color-conversation-grey;
line-height: 32px;
font-size: 16px;
color: $color-white;
text-align: center;
}
.module-conversation-header___default-avatar--red {
background-color: $color-conversation-red;
}
.module-conversation-header___default-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-conversation-header___default-avatar--brown {
background-color: $color-conversation-brown;
}
.module-conversation-header___default-avatar--pink {
background-color: $color-conversation-pink;
}
.module-conversation-header___default-avatar--purple {
background-color: $color-conversation-purple;
}
.module-conversation-header___default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-conversation-header___default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-conversation-header___default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-conversation-header___default-avatar--green {
background-color: $color-conversation-green;
}
.module-conversation-header___default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-conversation-header___default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
} }
.module-conversation-header__title { .module-conversation-header__title {
@ -1565,8 +1334,8 @@
font-weight: 300; font-weight: 300;
color: $color-light-90; color: $color-light-90;
// width of avatar and our 8px left margin // width of avatar (28px) and our 8px left margin
max-width: calc(100% - 40px); max-width: calc(100% - 36px);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -1674,56 +1443,6 @@
align-items: center; align-items: center;
} }
.module-message-detail__contact__avatar {
height: 44px;
width: 44px;
min-width: 44px;
border-radius: 22px;
}
.module-message-detail__contact__default-avatar {
background-color: $color-conversation-grey;
line-height: 44px;
font-size: 20px;
color: $color-white;
text-align: center;
}
.module-message-detail__contact__default-avatar--red {
background-color: $color-conversation-red;
}
.module-message-detail__contact__default-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-message-detail__contact__default-avatar--brown {
background-color: $color-conversation-brown;
}
.module-message-detail__contact__default-avatar--pink {
background-color: $color-conversation-pink;
}
.module-message-detail__contact__default-avatar--purple {
background-color: $color-conversation-purple;
}
.module-message-detail__contact__default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-message-detail__contact__default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-message-detail__contact__default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-message-detail__contact__default-avatar--green {
background-color: $color-conversation-green;
}
.module-message-detail__contact__default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-message-detail__contact__default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
}
.module-message-detail__contact__text { .module-message-detail__contact__text {
margin-left: 10px; margin-left: 10px;
flex-grow: 1; flex-grow: 1;
@ -2013,58 +1732,8 @@
.module-conversation-list-item__avatar-container { .module-conversation-list-item__avatar-container {
position: relative; position: relative;
}
.module-conversation-list-item__avatar {
margin-top: 8px; margin-top: 8px;
margin-bottom: 8px; margin-bottom: 8px;
height: 48px;
width: 48px;
border-radius: 24px;
min-width: 48px;
object-fit: cover;
}
.module-conversation-list-item__default-avatar {
color: white;
font-size: 26px;
line-height: 48px;
text-align: center;
background-color: $color-conversation-grey;
}
.module-conversation-list-item__default-avatar--red {
background-color: $color-conversation-red;
}
.module-conversation-list-item__default-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-conversation-list-item__default-avatar--brown {
background-color: $color-conversation-brown;
}
.module-conversation-list-item__default-avatar--pink {
background-color: $color-conversation-pink;
}
.module-conversation-list-item__default-avatar--purple {
background-color: $color-conversation-purple;
}
.module-conversation-list-item__default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-conversation-list-item__default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-conversation-list-item__default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-conversation-list-item__default-avatar--green {
background-color: $color-conversation-green;
}
.module-conversation-list-item__default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-conversation-list-item__default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
} }
.module-conversation-list-item__unread-count { .module-conversation-list-item__unread-count {
@ -2073,8 +1742,8 @@
text-align: center; text-align: center;
padding-top: 1px; padding-top: 1px;
padding-left: 2px; padding-left: 3px;
padding-right: 2px; padding-right: 3px;
position: absolute; position: absolute;
right: -6px; right: -6px;
@ -2089,7 +1758,7 @@
line-height: 16px; line-height: 16px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0px 0px 1px 2px $color-white-05; box-shadow: 0px 0px 0px 1px $color-gray-02;
} }
.module-conversation-list-item__content { .module-conversation-list-item__content {
@ -2118,6 +1787,9 @@
overflow-x: hidden; overflow-x: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
font-weight: 300;
color: $color-gray-90;
} }
.module-conversation-list-item__header__name--with-unread { .module-conversation-list-item__header__name--with-unread {
@ -2137,10 +1809,13 @@
text-overflow: ellipsis; text-overflow: ellipsis;
text-transform: uppercase; text-transform: uppercase;
color: $color-gray-60;
} }
.module-conversation-list-item__header__date--has-unread { .module-conversation-list-item__header__date--has-unread {
font-weight: 300; font-weight: 300;
color: $color-gray-90;
} }
.module-conversation-list-item__message { .module-conversation-list-item__message {
@ -2168,6 +1843,7 @@
.module-conversation-list-item__message__text--has-unread { .module-conversation-list-item__message__text--has-unread {
font-weight: 300; font-weight: 300;
color: $color-gray-90;
} }
.module-conversation-list-item__message__status-icon { .module-conversation-list-item__message__status-icon {
@ -2208,6 +1884,167 @@
@include color-svg('../images/error.svg', $color-core-red); @include color-svg('../images/error.svg', $color-core-red);
} }
// Module: Avatar
.module-avatar {
position: relative;
vertical-align: middle;
display: inline-block;
border-radius: 50%;
img {
object-fit: cover;
border-radius: 50%;
}
}
.module-avatar__label {
width: 100%;
text-align: center;
font-weight: 300;
text-transform: uppercase;
color: $color-white;
}
.module-avatar__icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.module-avatar__icon--group {
@include color-svg('../images/profile-group.svg', $color-white);
}
.module-avatar__icon--direct {
@include color-svg('../images/profile-individual.svg', $color-white);
}
.module-avatar--28 {
height: 28px;
width: 28px;
img {
height: 28px;
width: 28px;
}
}
.module-avatar__label--28 {
font-size: 14px;
line-height: 28px;
}
.module-avatar__icon--28 {
height: 16px;
width: 16px;
}
.module-avatar--36 {
height: 36px;
width: 36px;
img {
height: 36px;
width: 36px;
}
}
.module-avatar__label--36 {
margin-top: 1px;
width: 36px;
font-size: 16px;
letter-spacing: 0.19px;
line-height: 36px;
}
.module-avatar__icon--36 {
height: 20px;
width: 20px;
}
.module-avatar--48 {
height: 48px;
width: 48px;
img {
height: 48px;
width: 48px;
}
}
.module-avatar__label--48 {
width: 48px;
font-size: 20px;
letter-spacing: 0.19px;
line-height: 48px;
}
.module-avatar__icon--48 {
height: 26px;
width: 26px;
}
.module-avatar--80 {
height: 80px;
width: 80px;
img {
height: 80px;
width: 80px;
}
}
.module-avatar__label--80 {
width: 80px;
font-size: 40px;
line-height: 82px;
}
.module-avatar__icon--80 {
height: 42px;
width: 42px;
}
.module-avatar--no-image {
background-color: $color-conversation-grey;
}
.module-avatar--red {
background-color: $color-conversation-red;
}
.module-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-avatar--brown {
background-color: $color-conversation-brown;
}
.module-avatar--pink {
background-color: $color-conversation-pink;
}
.module-avatar--purple {
background-color: $color-conversation-purple;
}
.module-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-avatar--blue {
background-color: $color-conversation-blue;
}
.module-avatar--teal {
background-color: $color-conversation-teal;
}
.module-avatar--green {
background-color: $color-conversation-green;
}
.module-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
}
// Third-party module: react-contextmenu // Third-party module: react-contextmenu
.react-contextmenu { .react-contextmenu {

View file

@ -2,7 +2,7 @@
body.dark-theme { body.dark-theme {
background-color: $color-black; background-color: $color-black;
color: $color-gray-95; color: $color-gray-05;
} }
.dark-theme { .dark-theme {
@ -797,44 +797,6 @@ body.dark-theme {
border: 1px solid $color-dark-60; border: 1px solid $color-dark-60;
} }
.module-message__author-default-avatar--red {
background-color: $color-conversation-red;
}
.module-message__author-default-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-message__author-default-avatar--brown {
background-color: $color-conversation-brown;
}
.module-message__author-default-avatar--pink {
background-color: $color-conversation-pink;
}
.module-message__author-default-avatar--purple {
background-color: $color-conversation-purple;
}
.module-message__author-default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-message__author-default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-message__author-default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-message__author-default-avatar--green {
background-color: $color-conversation-green;
}
.module-message__author-default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-message__author-default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
}
.module-message__author-default-avatar__label {
color: $color-white;
}
// Module: Expire Timer // Module: Expire Timer
.module-expire-timer { .module-expire-timer {
@ -1076,11 +1038,6 @@ body.dark-theme {
// Module: Embedded Contact // Module: Embedded Contact
.module-embedded-contact__image-container__default-avatar {
background-color: $color-conversation-grey;
color: $color-white;
}
.module-embedded-contact__contact-name { .module-embedded-contact__contact-name {
color: $color-dark-05; color: $color-dark-05;
} }
@ -1099,11 +1056,6 @@ body.dark-theme {
// Module: Contact Detail // Module: Contact Detail
.module-contact-detail__image-container__default-avatar {
background-color: $color-conversation-grey;
color: $color-white;
}
.module-contact-detail__send-message { .module-contact-detail__send-message {
background-color: $blue; background-color: $blue;
color: $color-white; color: $color-white;
@ -1145,7 +1097,7 @@ body.dark-theme {
.module-verification-notification__button { .module-verification-notification__button {
color: $color-signal-blue; color: $color-signal-blue;
background-color: $color-light-02; background-color: $color-gray-75;
} }
// Module: Verification Notification // Module: Verification Notification
@ -1182,48 +1134,6 @@ body.dark-theme {
color: $color-dark-30; color: $color-dark-30;
} }
.module-contact-list-item__avatar-default {
background-color: $color-conversation-grey;
}
.module-contact-list-item__avatar-default--red {
background-color: $color-conversation-red;
}
.module-contact-list-item__avatar-default--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-contact-list-item__avatar-default--brown {
background-color: $color-conversation-brown;
}
.module-contact-list-item__avatar-default--pink {
background-color: $color-conversation-pink;
}
.module-contact-list-item__avatar-default--purple {
background-color: $color-conversation-purple;
}
.module-contact-list-item__avatar-default--indigo {
background-color: $color-conversation-indigo;
}
.module-contact-list-item__avatar-default--blue {
background-color: $color-conversation-blue;
}
.module-contact-list-item__avatar-default--teal {
background-color: $color-conversation-teal;
}
.module-contact-list-item__avatar-default--green {
background-color: $color-conversation-green;
}
.module-contact-list-item__avatar-default--light_green {
background-color: $color-conversation-light_green;
}
.module-contact-list-item__avatar-default--blue_grey {
background-color: $color-conversation-blue_grey;
}
.module-contact-list-item__avatar-default__label {
color: $color-white;
}
.module-contact-list-item__text__verified-icon { .module-contact-list-item__text__verified-icon {
@include color-svg('../images/verified-check.svg', $color-dark-30); @include color-svg('../images/verified-check.svg', $color-dark-30);
} }
@ -1240,45 +1150,6 @@ body.dark-theme {
@include color-svg('../images/back.svg', $color-dark-05); @include color-svg('../images/back.svg', $color-dark-05);
} }
.module-conversation-header___default-avatar {
background-color: $color-conversation-grey;
color: $color-white;
}
.module-conversation-header___default-avatar--red {
background-color: $color-conversation-red;
}
.module-conversation-header___default-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-conversation-header___default-avatar--brown {
background-color: $color-conversation-brown;
}
.module-conversation-header___default-avatar--pink {
background-color: $color-conversation-pink;
}
.module-conversation-header___default-avatar--purple {
background-color: $color-conversation-purple;
}
.module-conversation-header___default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-conversation-header___default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-conversation-header___default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-conversation-header___default-avatar--green {
background-color: $color-conversation-green;
}
.module-conversation-header___default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-conversation-header___default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
}
.module-conversation-header__title { .module-conversation-header__title {
color: $color-dark-05; color: $color-dark-05;
} }
@ -1308,45 +1179,6 @@ body.dark-theme {
border: solid 1px $color-light-35; border: solid 1px $color-light-35;
} }
.module-message-detail__contact__default-avatar {
background-color: $color-conversation-grey;
color: $color-white;
}
.module-message-detail__contact__default-avatar--red {
background-color: $color-conversation-red;
}
.module-message-detail__contact__default-avatar--deep_orange {
background-color: $color-conversation-deep_orange;
}
.module-message-detail__contact__default-avatar--brown {
background-color: $color-conversation-brown;
}
.module-message-detail__contact__default-avatar--pink {
background-color: $color-conversation-pink;
}
.module-message-detail__contact__default-avatar--purple {
background-color: $color-conversation-purple;
}
.module-message-detail__contact__default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-message-detail__contact__default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-message-detail__contact__default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-message-detail__contact__default-avatar--green {
background-color: $color-conversation-green;
}
.module-message-detail__contact__default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-message-detail__contact__default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
}
.module-message-detail__contact__error { .module-message-detail__contact__error {
color: $color-core-red; color: $color-core-red;
} }
@ -1437,43 +1269,30 @@ body.dark-theme {
background-color: $color-dark-70; background-color: $color-dark-70;
} }
.module-conversation-list-item__default-avatar { .module-conversation-list-item__unread-count {
color: white; color: $color-white;
background-color: $color-conversation-grey; background-color: $color-signal-blue;
box-shadow: 0px 0px 0px 1px $color-dark-85;
} }
.module-conversation-list-item__default-avatar--red { .module-conversation-list-item__header__name {
background-color: $color-conversation-red; color: $color-gray-05;
} }
.module-conversation-list-item__default-avatar--deep_orange {
background-color: $color-conversation-deep_orange; .module-conversation-list-item__header__timestamp {
color: $color-gray-25;
} }
.module-conversation-list-item__default-avatar--brown {
background-color: $color-conversation-brown; .module-conversation-list-item__header__date--has-unread {
color: $color-gray-05;
} }
.module-conversation-list-item__default-avatar--pink {
background-color: $color-conversation-pink; .module-conversation-list-item__message__text {
color: $color-gray-25;
} }
.module-conversation-list-item__default-avatar--purple {
background-color: $color-conversation-purple; .module-conversation-list-item__message__text--has-unread {
} color: $color-gray-05;
.module-conversation-list-item__default-avatar--indigo {
background-color: $color-conversation-indigo;
}
.module-conversation-list-item__default-avatar--blue {
background-color: $color-conversation-blue;
}
.module-conversation-list-item__default-avatar--teal {
background-color: $color-conversation-teal;
}
.module-conversation-list-item__default-avatar--green {
background-color: $color-conversation-green;
}
.module-conversation-list-item__default-avatar--light_green {
background-color: $color-conversation-light_green;
}
.module-conversation-list-item__default-avatar--blue_grey {
background-color: $color-conversation-blue_grey;
} }
.module-conversation-list-item__message__status-icon--sending { .module-conversation-list-item__message__status-icon--sending {
@ -1492,6 +1311,58 @@ body.dark-theme {
width: 18px; width: 18px;
} }
// Module: Avatar
.module-avatar__label {
color: $color-gray-05;
}
.module-avatar__icon--group {
@include color-svg('../images/profile-group.svg', $color-gray-05);
}
.module-avatar__icon--direct {
@include color-svg('../images/profile-individual.svg', $color-gray-05);
}
.module-avatar--no-image {
background-color: $color-conversation-grey-shade;
}
.module-avatar--red {
background-color: $color-conversation-red-shade;
}
.module-avatar--deep_orange {
background-color: $color-conversation-deep_orange-shade;
}
.module-avatar--brown {
background-color: $color-conversation-brown-shade;
}
.module-avatar--pink {
background-color: $color-conversation-pink-shade;
}
.module-avatar--purple {
background-color: $color-conversation-purple-shade;
}
.module-avatar--indigo {
background-color: $color-conversation-indigo-shade;
}
.module-avatar--blue {
background-color: $color-conversation-blue-shade;
}
.module-avatar--teal {
background-color: $color-conversation-teal-shade;
}
.module-avatar--green {
background-color: $color-conversation-green-shade;
}
.module-avatar--light_green {
background-color: $color-conversation-light_green-shade;
}
.module-avatar--blue_grey {
background-color: $color-conversation-blue_grey-shade;
}
// Third-party module: react-contextmenu // Third-party module: react-contextmenu
.react-contextmenu { .react-contextmenu {

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

View file

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

View file

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

View file

@ -207,7 +207,9 @@ export class ContactDetail extends React.Component<Props> {
return ( return (
<div className="module-contact-detail"> <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 })} {renderName({ contact, isIncoming, module })}
{renderContactShorthand({ contact, isIncoming, module })} {renderContactShorthand({ contact, isIncoming, module })}
{this.renderSendMessage({ hasSignalAccount, i18n, onSendMessage })} {this.renderSendMessage({ hasSignalAccount, i18n, onSendMessage })}

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
import { Avatar } from '../Avatar';
import { Localizer } from '../../types/Util'; import { Localizer } from '../../types/Util';
import { import {
ContextMenu, ContextMenu,
@ -45,10 +45,6 @@ interface Props {
onGoBack: () => void; onGoBack: () => void;
} }
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
export class ConversationHeader extends React.Component<Props> { export class ConversationHeader extends React.Component<Props> {
public captureMenuTriggerBound: (trigger: any) => void; public captureMenuTriggerBound: (trigger: any) => void;
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void; public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
@ -116,37 +112,25 @@ export class ConversationHeader extends React.Component<Props> {
avatarPath, avatarPath,
color, color,
i18n, i18n,
isGroup,
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
} = this.props; } = 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 ( return (
<img <span className="module-conversation-header__avatar">
className="module-conversation-header___avatar" <Avatar
alt={i18n('contactAvatarAlt', [title])} avatarPath={avatarPath}
src={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 React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Avatar } from '../Avatar';
import { Contact, getName } from '../../types/Contact'; import { Contact, getName } from '../../types/Contact';
import { Localizer } from '../../types/Util'; import { Localizer } from '../../types/Util';
@ -41,7 +42,7 @@ export class EmbeddedContact extends React.Component<Props> {
role="button" role="button"
onClick={onClick} onClick={onClick}
> >
{renderAvatar({ contact, i18n, module })} {renderAvatar({ contact, i18n, size: 48 })}
<div className="module-embedded-contact__text-container"> <div className="module-embedded-contact__text-container">
{renderName({ contact, isIncoming, module })} {renderName({ contact, isIncoming, module })}
{renderContactShorthand({ 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 // 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({ export function renderAvatar({
contact, contact,
i18n, i18n,
module, size,
}: { }: {
contact: Contact; contact: Contact;
i18n: Localizer; i18n: Localizer;
module: string; size: number;
}) { }) {
const { avatar } = contact; const { avatar } = contact;
const path = avatar && avatar.avatar && avatar.avatar.path; const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
const name = getName(contact) || ''; 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 ( return (
<div className={`module-${module}__image-container`}> <Avatar
<img src={path} alt={i18n('contactAvatarAlt', [name])} /> avatarPath={avatarPath}
</div> color="grey"
conversationType="direct"
i18n={i18n}
name={name}
size={size}
/>
); );
} }

View file

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

View file

@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
import { Avatar } from '../Avatar';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { Message, Props as MessageProps } from './Message'; import { Message, Props as MessageProps } from './Message';
import { Localizer } from '../../types/Util'; import { Localizer } from '../../types/Util';
@ -31,40 +32,21 @@ interface Props {
i18n: Localizer; i18n: Localizer;
} }
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
export class MessageDetail extends React.Component<Props> { export class MessageDetail extends React.Component<Props> {
public renderAvatar(contact: Contact) { public renderAvatar(contact: Contact) {
const { i18n } = this.props; const { i18n } = this.props;
const { avatarPath, color, phoneNumber, name, profileName } = contact; const { avatarPath, color, phoneNumber, name, profileName } = 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 ( return (
<img <Avatar
className="module-message-detail__contact__avatar" avatarPath={avatarPath}
alt={i18n('contactAvatarAlt', [title])} color={color}
src={avatarPath} 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'; import { default as _ } from 'lodash';
export { ConversationContext } from './ConversationContext'; export { ConversationContext } from './ConversationContext';
export { LeftPaneContext } from './LeftPaneContext';
export { _, classNames }; 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('');
}