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

View File

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

View File

@ -25,9 +25,6 @@
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;
this.contactView = new Whisper.ReactWrapperView({
@ -35,8 +32,8 @@
Component: window.Signal.Components.ContactListItem,
props: {
isMe,
color,
avatarPath,
color: this.model.getColor(),
avatarPath: this.model.getAvatarPath(),
phoneNumber: this.model.getNumber(),
name: this.model.getName(),
profileName: this.model.getProfileName(),

View File

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

View File

@ -560,74 +560,6 @@
// This accounts for the weird extra 3px we get at the bottom of messages
bottom: -3px;
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
@ -1025,37 +957,6 @@
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 {
flex-grow: 1;
margin-left: 8px;
@ -1106,31 +1007,8 @@
margin-right: auto;
}
.module-contact-detail__image-container {
height: 80px;
width: 80px;
.module-contact-detail__avatar {
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 {
@ -1366,69 +1244,6 @@
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 {
margin-left: 8px;
}
@ -1506,54 +1321,8 @@
max-width: 100%;
}
.module-conversation-header___avatar {
height: 32px;
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__avatar {
min-width: 28px;
}
.module-conversation-header__title {
@ -1565,8 +1334,8 @@
font-weight: 300;
color: $color-light-90;
// width of avatar and our 8px left margin
max-width: calc(100% - 40px);
// width of avatar (28px) and our 8px left margin
max-width: calc(100% - 36px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -1674,56 +1443,6 @@
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 {
margin-left: 10px;
flex-grow: 1;
@ -2013,58 +1732,8 @@
.module-conversation-list-item__avatar-container {
position: relative;
}
.module-conversation-list-item__avatar {
margin-top: 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 {
@ -2073,8 +1742,8 @@
text-align: center;
padding-top: 1px;
padding-left: 2px;
padding-right: 2px;
padding-left: 3px;
padding-right: 3px;
position: absolute;
right: -6px;
@ -2089,7 +1758,7 @@
line-height: 16px;
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 {
@ -2118,6 +1787,9 @@
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 300;
color: $color-gray-90;
}
.module-conversation-list-item__header__name--with-unread {
@ -2137,10 +1809,13 @@
text-overflow: ellipsis;
text-transform: uppercase;
color: $color-gray-60;
}
.module-conversation-list-item__header__date--has-unread {
font-weight: 300;
color: $color-gray-90;
}
.module-conversation-list-item__message {
@ -2168,6 +1843,7 @@
.module-conversation-list-item__message__text--has-unread {
font-weight: 300;
color: $color-gray-90;
}
.module-conversation-list-item__message__status-icon {
@ -2208,6 +1884,167 @@
@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
.react-contextmenu {

View File

@ -2,7 +2,7 @@
body.dark-theme {
background-color: $color-black;
color: $color-gray-95;
color: $color-gray-05;
}
.dark-theme {
@ -797,44 +797,6 @@ body.dark-theme {
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 {
@ -1076,11 +1038,6 @@ body.dark-theme {
// Module: Embedded Contact
.module-embedded-contact__image-container__default-avatar {
background-color: $color-conversation-grey;
color: $color-white;
}
.module-embedded-contact__contact-name {
color: $color-dark-05;
}
@ -1099,11 +1056,6 @@ body.dark-theme {
// Module: Contact Detail
.module-contact-detail__image-container__default-avatar {
background-color: $color-conversation-grey;
color: $color-white;
}
.module-contact-detail__send-message {
background-color: $blue;
color: $color-white;
@ -1145,7 +1097,7 @@ body.dark-theme {
.module-verification-notification__button {
color: $color-signal-blue;
background-color: $color-light-02;
background-color: $color-gray-75;
}
// Module: Verification Notification
@ -1182,48 +1134,6 @@ body.dark-theme {
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 {
@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);
}
.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 {
color: $color-dark-05;
}
@ -1308,45 +1179,6 @@ body.dark-theme {
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 {
color: $color-core-red;
}
@ -1437,43 +1269,30 @@ body.dark-theme {
background-color: $color-dark-70;
}
.module-conversation-list-item__default-avatar {
color: white;
background-color: $color-conversation-grey;
.module-conversation-list-item__unread-count {
color: $color-white;
background-color: $color-signal-blue;
box-shadow: 0px 0px 0px 1px $color-dark-85;
}
.module-conversation-list-item__default-avatar--red {
background-color: $color-conversation-red;
.module-conversation-list-item__header__name {
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__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__text--has-unread {
color: $color-gray-05;
}
.module-conversation-list-item__message__status-icon--sending {
@ -1492,6 +1311,58 @@ body.dark-theme {
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
.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 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('');
}