From 8f3e3b7aaf92fe368dc0464ce0314d4e714a030b Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 26 Sep 2018 17:23:17 -0700 Subject: [PATCH] Update to new design for avatars: individual/group icons/colors And two initials. --- images/profile-group.svg | 22 + images/profile-individual.svg | 22 + js/models/conversations.js | 14 +- js/models/messages.js | 17 +- js/views/contact_list_view.js | 7 +- js/views/conversation_view.js | 5 +- stylesheets/_modules.scss | 515 +++++--------- stylesheets/_theme_dark.scss | 273 ++------ ts/components/Avatar.md | 299 +++++++++ ts/components/Avatar.tsx | 118 ++++ ts/components/ContactListItem.tsx | 48 +- ts/components/ConversationListItem.md | 626 ++++++++++-------- ts/components/ConversationListItem.tsx | 43 +- ts/components/conversation/ContactDetail.tsx | 4 +- .../conversation/ConversationHeader.tsx | 44 +- .../conversation/EmbeddedContact.tsx | 36 +- ts/components/conversation/Message.tsx | 41 +- ts/components/conversation/MessageDetail.tsx | 38 +- ts/styleguide/LeftPaneContext.tsx | 25 + ts/styleguide/StyleGuideUtil.ts | 1 + ts/util/getInitials.ts | 21 + 21 files changed, 1206 insertions(+), 1013 deletions(-) create mode 100644 images/profile-group.svg create mode 100644 images/profile-individual.svg create mode 100644 ts/components/Avatar.md create mode 100644 ts/components/Avatar.tsx create mode 100644 ts/styleguide/LeftPaneContext.tsx create mode 100644 ts/util/getInitials.ts diff --git a/images/profile-group.svg b/images/profile-group.svg new file mode 100644 index 000000000..a6c9788bc --- /dev/null +++ b/images/profile-group.svg @@ -0,0 +1,22 @@ + + + + Group/group-28 + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/profile-individual.svg b/images/profile-individual.svg new file mode 100644 index 000000000..f28b9ea45 --- /dev/null +++ b/images/profile-individual.svg @@ -0,0 +1,22 @@ + + + + Profile/profile-28 + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/models/conversations.js b/js/models/conversations.js index 099d4dab1..994239934 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -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(); diff --git a/js/models/messages.js b/js/models/messages.js index 924289cfa..2be343b0c 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -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, }; diff --git a/js/views/contact_list_view.js b/js/views/contact_list_view.js index bb813e545..106598813 100644 --- a/js/views/contact_list_view.js +++ b/js/views/contact_list_view.js @@ -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(), diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 1646bedd9..838b26c4d 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -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; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 4bf1f6d02..8b2219ba0 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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 { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index e48bd08d4..274016ccc 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -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__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__header__name { + color: $color-gray-05; } - .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__header__timestamp { + color: $color-gray-25; } - .module-conversation-list-item__default-avatar--green { - background-color: $color-conversation-green; + + .module-conversation-list-item__header__date--has-unread { + color: $color-gray-05; } - .module-conversation-list-item__default-avatar--light_green { - background-color: $color-conversation-light_green; + + .module-conversation-list-item__message__text { + color: $color-gray-25; } - .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 { diff --git a/ts/components/Avatar.md b/ts/components/Avatar.md new file mode 100644 index 000000000..c80850e44 --- /dev/null +++ b/ts/components/Avatar.md @@ -0,0 +1,299 @@ +### With avatar + +```jsx + + +``` + +### With only name + +```jsx + + + +``` + +### Just phone number + +```jsx + +``` + +### All colors + +```jsx + + + + + + + + + + + +``` + +### 36px + +```jsx + + + + + +``` + +### 48px + +```jsx + + + + + +``` + +### 80px + +```jsx + + + + + +``` + +### Broken color + +```jsx + +``` + +### Broken image + +```jsx + +``` + +### Broken image for group + +```jsx + +``` diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx new file mode 100644 index 000000000..9a921ac1d --- /dev/null +++ b/ts/components/Avatar.tsx @@ -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 { + 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 ( + {i18n('contactAvatarAlt', + ); + } + + public renderNoImage() { + const { conversationType, name, size } = this.props; + + const initials = getInitials(name); + const isGroup = conversationType === 'group'; + + if (!isGroup && initials) { + return ( +
+ {initials} +
+ ); + } + + return ( +
+ ); + } + + 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 ( +
+ {hasImage ? this.renderImage() : this.renderNoImage()} +
+ ); + } +} diff --git a/ts/components/ContactListItem.tsx b/ts/components/ContactListItem.tsx index 4ef71c75e..936ba4b4c 100644 --- a/ts/components/ContactListItem.tsx +++ b/ts/components/ContactListItem.tsx @@ -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 { - public renderAvatar({ displayName }: { displayName: string }) { - const { avatarPath, i18n, color, name } = this.props; - - if (avatarPath) { - return ( -
- {i18n('contactAvatarAlt', -
- ); - } - - const title = name ? getInitial(name) : '#'; + public renderAvatar() { + const { + avatarPath, + i18n, + color, + name, + phoneNumber, + profileName, + } = this.props; return ( -
-
- {title} -
-
+ ); } @@ -82,7 +76,7 @@ export class ContactListItem extends React.Component { onClick ? 'module-contact-list-item--with-click-handler' : null )} > - {this.renderAvatar({ displayName })} + {this.renderAvatar()}
{profileElement} diff --git a/ts/components/ConversationListItem.md b/ts/components/ConversationListItem.md index 59d0213b4..1fc94bd76 100644 --- a/ts/components/ConversationListItem.md +++ b/ts/components/ConversationListItem.md @@ -1,131 +1,167 @@ #### With name and profile ```jsx - console.log('onClick')} - i18n={util.i18n} -/> -``` - -#### Profile, with name, no avatar - -```jsx - console.log('onClick')} - i18n={util.i18n} -/> -``` - -#### All types of status - -```jsx -
+ console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} i18n={util.i18n} /> + +``` + +#### Profile, with name, no avatar + +```jsx + console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} i18n={util.i18n} /> - console.log('onClick')} - i18n={util.i18n} - /> -
+ +``` + +#### All types of status + +```jsx + +
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> +
+
``` #### With unread ```jsx -
- console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> + +
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> +
+
+``` + +#### Selected + +```jsx + console.log('onClick')} i18n={util.i18n} /> -
-``` - -#### Selected - -```jsx - console.log('onClick')} - i18n={util.i18n} -/> + ``` #### With emoji/links in message, no status @@ -156,26 +177,30 @@ We don't want Jumbomoji or links. ```jsx -
- console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> -
+ +
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> +
+
``` #### Long content @@ -183,72 +208,80 @@ We don't want Jumbomoji or links. We only show one line. ```jsx -
- console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> + +
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> - console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> -
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> +
+ ``` #### 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 -
- console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> -
+ +
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> +
+
``` #### With various ages ```jsx -
- console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> -
+ +
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> +
+
``` #### Missing data ```jsx -
- console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> - console.log('onClick')} - i18n={util.i18n} - /> -
+ +
+ console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> + console.log('onClick')} + i18n={util.i18n} + /> +
+
``` diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 14e65c4ed..deeb5dbfe 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -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 { public renderAvatar() { const { avatarPath, color, + conversationType, i18n, name, phoneNumber, profileName, } = this.props; - if (!avatarPath) { - const initial = getInitial(name || ''); - - return ( -
-
- {initial} -
- {this.renderUnread()} -
- ); - } - - const title = `${name || phoneNumber}${ - !name && profileName ? ` ~${profileName}` : '' - }`; - return (
- {i18n('contactAvatarAlt', {this.renderUnread()}
diff --git a/ts/components/conversation/ContactDetail.tsx b/ts/components/conversation/ContactDetail.tsx index 5a3b4fd5b..cbf33c7a7 100644 --- a/ts/components/conversation/ContactDetail.tsx +++ b/ts/components/conversation/ContactDetail.tsx @@ -207,7 +207,9 @@ export class ContactDetail extends React.Component { return (
- {renderAvatar({ contact, i18n, module })} +
+ {renderAvatar({ contact, i18n, size: 80 })} +
{renderName({ contact, isIncoming, module })} {renderContactShorthand({ contact, isIncoming, module })} {this.renderSendMessage({ hasSignalAccount, i18n, onSendMessage })} diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 0229db359..a81ded580 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -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 { public captureMenuTriggerBound: (trigger: any) => void; public showMenuBound: (event: React.MouseEvent) => void; @@ -116,37 +112,25 @@ export class ConversationHeader extends React.Component { avatarPath, color, i18n, + isGroup, name, phoneNumber, profileName, } = this.props; - if (!avatarPath) { - const initial = getInitial(name || ''); - - return ( -
- {initial} -
- ); - } - - const title = `${name || phoneNumber}${ - !name && profileName ? ` ~${profileName}` : '' - }`; - return ( - {i18n('contactAvatarAlt', + + + ); } diff --git a/ts/components/conversation/EmbeddedContact.tsx b/ts/components/conversation/EmbeddedContact.tsx index 607a2b554..b9a818d75 100644 --- a/ts/components/conversation/EmbeddedContact.tsx +++ b/ts/components/conversation/EmbeddedContact.tsx @@ -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 { role="button" onClick={onClick} > - {renderAvatar({ contact, i18n, module })} + {renderAvatar({ contact, i18n, size: 48 })}
{renderName({ contact, isIncoming, module })} {renderContactShorthand({ contact, isIncoming, module })} @@ -53,40 +54,29 @@ export class EmbeddedContact extends React.Component { // 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 ( -
-
- {initials} -
-
- ); - } - return ( -
- {i18n('contactAvatarAlt', -
+ ); } diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 146826628..f9fb54019 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -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 { 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 { return; } - if (!authorAvatarPath) { - const label = authorName ? getInitial(authorName) : '#'; - - return ( -
-
- {label} -
-
- ); - } - return (
- {i18n('contactAvatarAlt', +
); } diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index fc20817f7..cb830fdea 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -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 { public renderAvatar(contact: Contact) { const { i18n } = this.props; const { avatarPath, color, phoneNumber, name, profileName } = contact; - if (!avatarPath) { - const initial = getInitial(name || ''); - - return ( -
- {initial} -
- ); - } - - const title = `${name || phoneNumber}${ - !name && profileName ? ` ~${profileName}` : '' - }`; - return ( - {i18n('contactAvatarAlt', ); } diff --git a/ts/styleguide/LeftPaneContext.tsx b/ts/styleguide/LeftPaneContext.tsx new file mode 100644 index 000000000..98a67ea17 --- /dev/null +++ b/ts/styleguide/LeftPaneContext.tsx @@ -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 { + public render() { + const { theme } = this.props; + + return ( +
+
{this.props.children}
+
+ ); + } +} diff --git a/ts/styleguide/StyleGuideUtil.ts b/ts/styleguide/StyleGuideUtil.ts index 0c4788881..a7679c0aa 100644 --- a/ts/styleguide/StyleGuideUtil.ts +++ b/ts/styleguide/StyleGuideUtil.ts @@ -6,6 +6,7 @@ import classNames from 'classnames'; import { default as _ } from 'lodash'; export { ConversationContext } from './ConversationContext'; +export { LeftPaneContext } from './LeftPaneContext'; export { _, classNames }; diff --git a/ts/util/getInitials.ts b/ts/util/getInitials.ts new file mode 100644 index 000000000..b2c89ffc5 --- /dev/null +++ b/ts/util/getInitials.ts @@ -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(''); +}