Update to new message bubble reactions design

This commit is contained in:
Ken Powers 2020-02-03 15:02:49 -05:00 committed by GitHub
parent 682ac656c6
commit 01d4aa0772
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 482 additions and 460 deletions

View file

@ -1152,6 +1152,40 @@
} }
} }
}, },
"notificationReactionMessage": {
"message": "$sender$ reacted $emoji$ to: $message$",
"placeholders": {
"sender": {
"content": "$1",
"example": "John"
},
"emoji": {
"content": "$2",
"example": "👍"
},
"message": {
"content": "$3",
"example": "Sounds good."
}
}
},
"notificationReactionMessageMostRecent": {
"message": "Most recent: $sender$ reacted $emoji$ to: $message$",
"placeholders": {
"sender": {
"content": "$1",
"example": "John"
},
"emoji": {
"content": "$2",
"example": "👍"
},
"message": {
"content": "$3",
"example": "Sounds good."
}
}
},
"sendFailed": { "sendFailed": {
"message": "Send failed", "message": "Send failed",
"description": "Shown on outgoing message if it fails to send" "description": "Shown on outgoing message if it fails to send"

View file

@ -126,9 +126,10 @@
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring
title = last.title; title = last.title;
if (last.reaction) { if (last.reaction) {
message = i18n('notificationReaction', [ message = i18n('notificationReactionMessage', [
last.title, last.title,
last.reaction.emoji, last.reaction.emoji,
last.message,
]); ]);
} else { } else {
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring
@ -136,9 +137,10 @@
} }
} else if (last.reaction) { } else if (last.reaction) {
title = newMessageCountLabel; title = newMessageCountLabel;
message = i18n('notificationReactionMostRecent', [ message = i18n('notificationReactionMessageMostRecent', [
last.title, last.title,
last.reaction.emoji, last.reaction.emoji,
last.message,
]); ]);
} else { } else {
title = newMessageCountLabel; title = newMessageCountLabel;

View file

@ -90,16 +90,10 @@
.module-message__buttons--incoming { .module-message__buttons--incoming {
left: calc(100% + 8px); left: calc(100% + 8px);
&.module-message__buttons--has-reactions {
padding-left: 40px - 12px; // Adjust 40px by 12px margin on the button
}
} }
.module-message__buttons--outgoing { .module-message__buttons--outgoing {
right: calc(100% + 8px); right: calc(100% + 8px);
flex-direction: row-reverse; flex-direction: row-reverse;
&.module-message__buttons--has-reactions {
padding-right: 40px - 12px; // Adjust 40px by 12px margin on the button
}
} }
.module-message__buttons__download { .module-message__buttons__download {
@ -1096,6 +1090,14 @@
align-items: center; align-items: center;
margin-top: 3px; margin-top: 3px;
margin-bottom: -3px; margin-bottom: -3px;
&--outgoing {
justify-content: flex-end;
}
&--with-reactions {
margin-bottom: -2px;
}
} }
// With an image and no caption, this section needs to be on top of the image overlay // With an image and no caption, this section needs to be on top of the image overlay
@ -1171,10 +1173,6 @@
} }
} }
.module-message__metadata__spacer {
flex-grow: 1;
}
.module-message__metadata__status-icon { .module-message__metadata__status-icon {
width: 12px; width: 12px;
height: 12px; height: 12px;
@ -1342,43 +1340,57 @@
.module-message__reactions { .module-message__reactions {
position: absolute; position: absolute;
top: 0; bottom: 0px;
z-index: 2; z-index: 2;
height: 100%; height: 22px;
display: flex;
&--incoming {
right: -28px;
}
&--outgoing {
left: -28px;
}
} }
.module-message__reactions__reaction { .module-message__reactions__reaction {
@include button-reset; @include button-reset;
width: 33px; min-width: 28px;
height: 33px; height: 22px;
border: 1px solid; border: 1px solid;
border-radius: 33px; border-radius: 33px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: absolute; &--with-count {
top: 0; min-width: 40px;
&:first-of-type {
z-index: 2;
} }
&--incoming { &__count {
right: 0; @include font-caption-bold;
}
&--outgoing { margin-left: 4px;
left: 0;
&--no-emoji {
margin-left: 0px;
}
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
&--is-me {
@include light-theme {
color: $color-gray-75;
}
@include dark-theme {
color: $color-gray-15;
}
@include ios-theme {
color: $color-white-alpha-90;
}
}
} }
&:focus { &:focus {
@ -7633,6 +7645,10 @@ button.module-image__border-overlay:focus {
.module-message__container { .module-message__container {
// 2px to allow for 1px border // 2px to allow for 1px border
max-width: 302px; max-width: 302px;
&--with-reactions {
margin-bottom: 12px;
}
} }
/* Spec: container > 438px and container < 593px */ /* Spec: container > 438px and container < 593px */

View file

@ -449,369 +449,253 @@ Note that timestamp and status can be hidden with the `collapseMetadata` boolean
### Reactions ### Reactions
#### One Reaction
```jsx ```jsx
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}> <util.ConversationContext>
<div className="module-message-container"> {[
<Message { reactions: [{ emoji: '👍', from: { id: '+14155552671' } }] },
direction="incoming" {
status="delivered" reactions: [
authorColor="red" { emoji: '👍', from: { id: '+14155552671', name: 'Jack Sparrow' } },
timestamp={Date.now()}
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
i18n={util.i18n}
reactions={[
{ {
emoji: '👍', emoji: '😂',
from: { id: '+14155552671', name: 'Amelia Briggs' }, from: { id: '+14155552672', profileName: 'Davy Jones' },
timestamp: 1,
},
]}
/>
</div>
<div className="module-message-container">
<Message
direction="outgoing"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', name: 'Amelia Briggs' },
timestamp: 1,
},
]}
/>
</div>
</util.ConversationContext>
```
#### One Reaction - Ours
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', isMe: true, name: 'Amelia Briggs' },
timestamp: 1,
},
]}
/>
</div>
<div className="module-message-container">
<Message
direction="outgoing"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', isMe: true, name: 'Amelia Briggs' },
timestamp: 1,
},
]}
/>
</div>
</util.ConversationContext>
```
#### Multiple reactions, ordered by most common then most recent
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', name: 'Amelia Briggs' },
timestamp: 1,
},
{
emoji: '👍',
from: { id: '+14155552671', name: 'Joel Ferrari' },
timestamp: 1,
},
{
emoji: '😡',
from: { id: '+14155552671', name: 'Adam Burrell' },
timestamp: 1,
}, },
],
},
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Jack Sparrow' } },
{ emoji: '😂', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ {
emoji: '😮', emoji: '😮',
from: { id: '+14155552671', name: 'Rick Owens' }, from: { id: '+14155552673', profileName: 'Joel Ferrari' },
timestamp: 2,
}, },
]} ],
/> },
</div> {
<div className="module-message-container"> reactions: [
<Message { emoji: '👍', from: { id: '+14155552671', name: 'Jack Sparrow' } },
direction="outgoing" { emoji: '😂', from: { id: '+14155552672', name: 'Amelia Briggs' } },
status="delivered" { emoji: '😮', from: { id: '+14155552673', name: 'Amelia Briggs' } },
authorColor="red" { emoji: '😡', from: { id: '+14155552674', name: 'Amelia Briggs' } },
timestamp={Date.now()} { emoji: '👎', from: { id: '+14155552675', name: 'Amelia Briggs' } },
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half." { emoji: '❤️', from: { id: '+14155552676', name: 'Amelia Briggs' } },
i18n={util.i18n} ],
reactions={[ },
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
],
},
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😡', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '👎', from: { id: '+14155552676', name: 'Amelia Briggs' } },
{ emoji: '❤️', from: { id: '+14155552678', name: 'Amelia Briggs' } },
],
},
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
],
},
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
],
},
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
{ emoji: '😮', from: { id: '+14155552677', name: 'Amelia Briggs' } },
{ emoji: '😮', from: { id: '+14155552678', name: 'Amelia Briggs' } },
{ emoji: '😮', from: { id: '+14155552679', name: 'Amelia Briggs' } },
],
},
{
short: true,
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😡', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '👎', from: { id: '+14155552676', name: 'Amelia Briggs' } },
{ emoji: '❤️', from: { id: '+14155552677', name: 'Amelia Briggs' } },
],
},
{
short: true,
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
],
},
{
short: true,
reactions: [],
},
{
reactions: [
{ {
emoji: '👍', emoji: '👍',
from: { id: '+14155552671', name: 'Amelia Briggs' }, from: { isMe: true, id: '+14155552671', name: 'Amelia Briggs' },
timestamp: 1,
}, },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
{ emoji: '😡', from: { id: '+14155552677', name: 'Amelia Briggs' } },
{ emoji: '👎', from: { id: '+14155552678', name: 'Amelia Briggs' } },
{ emoji: '❤️', from: { id: '+14155552679', name: 'Amelia Briggs' } },
],
},
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ {
emoji: '👍', emoji: '😂',
from: { id: '+14155552671', name: 'Joel Ferrari' }, from: { isMe: true, id: '+14155552674', name: 'Amelia Briggs' },
timestamp: 1,
}, },
{ emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
{ emoji: '😡', from: { id: '+14155552677', name: 'Amelia Briggs' } },
{ emoji: '👎', from: { id: '+14155552678', name: 'Amelia Briggs' } },
{ emoji: '❤️', from: { id: '+14155552679', name: 'Amelia Briggs' } },
],
},
{
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
{ {
emoji: '😡', emoji: '😡',
from: { id: '+14155552671', name: 'Adam Burrell' }, from: { isMe: true, id: '+14155552677', name: 'Amelia Briggs' },
timestamp: 1,
}, },
{ emoji: '👎', from: { id: '+14155552678', name: 'Amelia Briggs' } },
{ emoji: '❤️', from: { id: '+14155552679', name: 'Amelia Briggs' } },
],
},
{
outgoing: true,
reactions: [
{ emoji: '😂', from: { id: '+14155552671', name: 'Amelia Briggs' } },
],
},
{
outgoing: true,
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
{ emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
{ emoji: '😡', from: { id: '+14155552677', name: 'Amelia Briggs' } },
{ emoji: '👎', from: { id: '+14155552678', name: 'Amelia Briggs' } },
{ emoji: '❤️', from: { id: '+14155552679', name: 'Amelia Briggs' } },
],
},
{
outgoing: true,
reactions: [
{ emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
{ emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ {
emoji: '😮', emoji: '😂',
from: { id: '+14155552671', name: 'Rick Owens' }, from: { isMe: true, id: '+14155552674', name: 'Amelia Briggs' },
timestamp: 2,
}, },
]} { emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
/> { emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
</div> { emoji: '😡', from: { id: '+14155552677', name: 'Amelia Briggs' } },
</util.ConversationContext> { emoji: '👎', from: { id: '+14155552678', name: 'Amelia Briggs' } },
``` { emoji: '❤️', from: { id: '+14155552679', name: 'Amelia Briggs' } },
],
#### Multiple reactions, ours is most recent/common },
{
```jsx outgoing: true,
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}> short: true,
<div className="module-message-container"> reactions: [
<Message { emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
direction="incoming" { emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
status="delivered" { emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
authorColor="red" { emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
timestamp={Date.now()} { emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half." { emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
i18n={util.i18n} { emoji: '😡', from: { id: '+14155552677', name: 'Amelia Briggs' } },
reactions={[ { emoji: '👎', from: { id: '+14155552678', name: 'Amelia Briggs' } },
{ { emoji: '❤️', from: { id: '+14155552679', name: 'Amelia Briggs' } },
emoji: '👍', ],
from: { id: '+14155552671', isMe: true, name: 'Amelia Briggs' }, },
timestamp: 1, {
}, outgoing: true,
{ short: true,
emoji: '👍', reactions: [
from: { id: '+14155552671', name: 'Joel Ferrari' }, { emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
timestamp: 1, { emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
}, { emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
{ { emoji: '😂', from: { id: '+14155552674', name: 'Amelia Briggs' } },
emoji: '😡', { emoji: '😂', from: { id: '+14155552675', name: 'Amelia Briggs' } },
from: { id: '+14155552671', name: 'Adam Burrell' }, { emoji: '😂', from: { id: '+14155552676', name: 'Amelia Briggs' } },
timestamp: 1, ],
}, },
{ {
emoji: '😮', outgoing: true,
from: { id: '+14155552671', name: 'Rick Owens' }, short: true,
timestamp: 2, reactions: [
}, { emoji: '👍', from: { id: '+14155552671', name: 'Amelia Briggs' } },
]} { emoji: '👍', from: { id: '+14155552672', name: 'Amelia Briggs' } },
/> { emoji: '👍', from: { id: '+14155552673', name: 'Amelia Briggs' } },
</div> ],
<div className="module-message-container"> },
<Message ].map((spec, i) => (
direction="outgoing" <div key={i} className="module-message-container">
status="delivered" <Message
authorColor="red" direction={spec.outgoing ? 'outgoing' : 'incoming'}
timestamp={Date.now()} status="delivered"
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half." authorColor="light_green"
i18n={util.i18n} timestamp={Date.now()}
reactions={[ text={
{ spec.short
emoji: '👍', ? 'hahaha'
from: { id: '+14155552671', name: 'Amelia Briggs' }, : "I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
timestamp: 1, }
}, reactions={spec.reactions}
{ i18n={util.i18n}
emoji: '👍', />
from: { id: '+14155552671', isMe: true, name: 'Joel Ferrari' }, </div>
timestamp: 1, ))}
},
{
emoji: '😡',
from: { id: '+14155552671', name: 'Adam Burrell' },
timestamp: 1,
},
{
emoji: '😮',
from: { id: '+14155552671', name: 'Rick Owens' },
timestamp: 2,
},
]}
/>
</div>
</util.ConversationContext>
```
#### Multiple reactions, ours not on top
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', name: 'Amelia Briggs' },
timestamp: 1,
},
{
emoji: '👍',
from: { id: '+14155552671', name: 'Joel Ferrari' },
timestamp: 1,
},
{
emoji: '😡',
from: { id: '+14155552671', name: 'Adam Burrell' },
timestamp: 1,
},
{
emoji: '😮',
from: { id: '+14155552671', isMe: true, name: 'Rick Owens' },
timestamp: 2,
},
]}
/>
</div>
<div className="module-message-container">
<Message
direction="outgoing"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="I'd like to order one large phone with extra phones please. cell phone, no no no rotary... and payphone on half."
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', name: 'Amelia Briggs' },
timestamp: 1,
},
{
emoji: '👍',
from: { id: '+14155552671', name: 'Joel Ferrari' },
timestamp: 1,
},
{
emoji: '😡',
from: { id: '+14155552671', name: 'Adam Burrell' },
timestamp: 1,
},
{
emoji: '😮',
from: { id: '+14155552671', isMe: true, name: 'Rick Owens' },
timestamp: 2,
},
]}
/>
</div>
</util.ConversationContext>
```
#### Small message
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
<div className="module-message-container">
<Message
direction="incoming"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="Burgertime!"
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', name: 'Amelia Briggs' },
timestamp: 1,
},
{
emoji: '👍',
from: { id: '+14155552671', name: 'Joel Ferrari' },
timestamp: 1,
},
{
emoji: '😡',
from: { id: '+14155552671', name: 'Adam Burrell' },
timestamp: 1,
},
{
emoji: '😮',
from: { id: '+14155552671', name: 'Rick Owens' },
timestamp: 2,
},
]}
/>
</div>
<div className="module-message-container">
<Message
direction="outgoing"
status="delivered"
authorColor="red"
timestamp={Date.now()}
text="Burgertime!"
i18n={util.i18n}
reactions={[
{
emoji: '👍',
from: { id: '+14155552671', name: 'Amelia Briggs' },
timestamp: 1,
},
{
emoji: '👍',
from: { id: '+14155552671', name: 'Joel Ferrari' },
timestamp: 1,
},
{
emoji: '😡',
from: { id: '+14155552671', name: 'Adam Burrell' },
timestamp: 1,
},
{
emoji: '😮',
from: { id: '+14155552671', name: 'Rick Owens' },
timestamp: 2,
},
]}
/>
</div>
</util.ConversationContext> </util.ConversationContext>
``` ```

View file

@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM, { createPortal } from 'react-dom'; import ReactDOM, { createPortal } from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import Measure from 'react-measure'; import Measure from 'react-measure';
import { clamp, groupBy, orderBy, take } from 'lodash'; import { drop, groupBy, orderBy, take } from 'lodash';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import { Avatar } from '../Avatar'; import { Avatar } from '../Avatar';
@ -161,11 +161,12 @@ interface State {
isSelected: boolean; isSelected: boolean;
prevSelectedCounter: number; prevSelectedCounter: number;
reactionsHeight: number;
reactionViewerRoot: HTMLDivElement | null; reactionViewerRoot: HTMLDivElement | null;
reactionPickerRoot: HTMLDivElement | null; reactionPickerRoot: HTMLDivElement | null;
isWide: boolean; isWide: boolean;
containerWidth: number;
} }
const EXPIRATION_CHECK_MINIMUM = 2000; const EXPIRATION_CHECK_MINIMUM = 2000;
@ -199,11 +200,12 @@ export class Message extends React.PureComponent<Props, State> {
isSelected: props.isSelected, isSelected: props.isSelected,
prevSelectedCounter: props.isSelectedCounter, prevSelectedCounter: props.isSelectedCounter,
reactionsHeight: 0,
reactionViewerRoot: null, reactionViewerRoot: null,
reactionPickerRoot: null, reactionPickerRoot: null,
isWide: this.wideMl.matches, isWide: this.wideMl.matches,
containerWidth: 0,
}; };
} }
@ -370,6 +372,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
} }
// tslint:disable-next-line cyclomatic-complexity
public renderMetadata() { public renderMetadata() {
const { const {
collapseMetadata, collapseMetadata,
@ -379,6 +382,7 @@ export class Message extends React.PureComponent<Props, State> {
i18n, i18n,
isSticker, isSticker,
isTapToViewExpired, isTapToViewExpired,
reactions,
status, status,
text, text,
textPending, textPending,
@ -391,6 +395,7 @@ export class Message extends React.PureComponent<Props, State> {
const isShowingImage = this.isShowingImage(); const isShowingImage = this.isShowingImage();
const withImageNoCaption = Boolean(!isSticker && !text && isShowingImage); const withImageNoCaption = Boolean(!isSticker && !text && isShowingImage);
const withReactions = reactions && reactions.length > 0;
const showError = status === 'error' && direction === 'outgoing'; const showError = status === 'error' && direction === 'outgoing';
const metadataDirection = isSticker ? undefined : direction; const metadataDirection = isSticker ? undefined : direction;
@ -398,12 +403,13 @@ export class Message extends React.PureComponent<Props, State> {
<div <div
className={classNames( className={classNames(
'module-message__metadata', 'module-message__metadata',
`module-message__metadata--${direction}`,
withReactions ? 'module-message__metadata--with-reactions' : null,
withImageNoCaption withImageNoCaption
? 'module-message__metadata--with-image-no-caption' ? 'module-message__metadata--with-image-no-caption'
: null : null
)} )}
> >
<span className="module-message__metadata__spacer" />
{showError ? ( {showError ? (
<span <span
className={classNames( className={classNames(
@ -991,9 +997,7 @@ export class Message extends React.PureComponent<Props, State> {
return null; return null;
} }
const { reactions } = this.props;
const { reactionPickerRoot, isWide } = this.state; const { reactionPickerRoot, isWide } = this.state;
const hasReactions = reactions && reactions.length > 0;
const multipleAttachments = attachments && attachments.length > 1; const multipleAttachments = attachments && attachments.length > 1;
const firstAttachment = attachments && attachments[0]; const firstAttachment = attachments && attachments[0];
@ -1093,8 +1097,7 @@ export class Message extends React.PureComponent<Props, State> {
<div <div
className={classNames( className={classNames(
'module-message__buttons', 'module-message__buttons',
`module-message__buttons--${direction}`, `module-message__buttons--${direction}`
hasReactions ? 'module-message__buttons--has-reactions' : null
)} )}
> >
{ENABLE_REACTION_SEND ? reactButton : null} {ENABLE_REACTION_SEND ? reactButton : null}
@ -1524,16 +1527,43 @@ export class Message extends React.PureComponent<Props, State> {
['length', ([{ timestamp }]) => timestamp], ['length', ([{ timestamp }]) => timestamp],
['desc', 'desc'] ['desc', 'desc']
); );
// Take the first two groups for rendering // Take the first three groups for rendering
const toRender = take(ordered, 2).map(res => ({ const toRender = take(ordered, 3).map(res => ({
emoji: res[0].emoji, emoji: res[0].emoji,
count: res.length,
isMe: res.some(re => Boolean(re.from.isMe)), isMe: res.some(re => Boolean(re.from.isMe)),
})); }));
const someNotRendered = ordered.length > 3;
// We only drop two here because the third emoji would be replaced by the
// more button
const maybeNotRendered = drop(ordered, 2);
const maybeNotRenderedTotal = maybeNotRendered.reduce(
(sum, res) => sum + res.length,
0
);
const notRenderedIsMe =
someNotRendered &&
maybeNotRendered.some(res => res.some(re => Boolean(re.from.isMe)));
const reactionHeight = 32; const { reactionViewerRoot, containerWidth } = this.state;
const { reactionsHeight: height, reactionViewerRoot } = this.state;
const offset = clamp((height - reactionHeight) / toRender.length, 4, 28); // Calculate the width of the reactions container
const reactionsWidth = toRender.reduce((sum, res, i, arr) => {
if (someNotRendered && i === arr.length - 1) {
return sum + 28;
}
if (res.count > 1) {
return sum + 40;
}
return sum + 28;
}, 0);
const reactionsXAxisOffset = Math.max(
containerWidth - reactionsWidth - 6,
6
);
const popperPlacement = outgoing ? 'bottom-end' : 'bottom-start'; const popperPlacement = outgoing ? 'bottom-end' : 'bottom-start';
@ -1541,58 +1571,83 @@ export class Message extends React.PureComponent<Props, State> {
<Manager> <Manager>
<Reference> <Reference>
{({ ref: popperRef }) => ( {({ ref: popperRef }) => (
<Measure <div
bounds={true} ref={mergeRefs(this.reactionsContainerRef, popperRef)}
onResize={({ bounds = { height: 0 } }) => { className={classNames(
this.setState({ reactionsHeight: bounds.height }); 'module-message__reactions',
outgoing
? 'module-message__reactions--outgoing'
: 'module-message__reactions--incoming'
)}
style={{
[outgoing ? 'right' : 'left']: `${reactionsXAxisOffset}px`,
}} }}
> >
{({ measureRef }) => ( {toRender.map((re, i) => {
<div const isLast = i === toRender.length - 1;
ref={mergeRefs( const isMore = isLast && someNotRendered;
this.reactionsContainerRef, const isMoreWithMe = isMore && notRenderedIsMe;
measureRef,
popperRef return (
)} <button
className={classNames( key={`${re.emoji}-${i}`}
'module-message__reactions', className={classNames(
outgoing 'module-message__reactions__reaction',
? 'module-message__reactions--outgoing' re.count > 1
: 'module-message__reactions--incoming' ? 'module-message__reactions__reaction--with-count'
)} : null,
> outgoing
{toRender.map((re, i) => ( ? 'module-message__reactions__reaction--outgoing'
<button : 'module-message__reactions__reaction--incoming',
key={`${re.emoji}-${i}`} isMoreWithMe || (re.isMe && !isMoreWithMe)
className={classNames( ? 'module-message__reactions__reaction--is-me'
'module-message__reactions__reaction', : null
outgoing )}
? 'module-message__reactions__reaction--outgoing' onClick={e => {
: 'module-message__reactions__reaction--incoming', e.stopPropagation();
re.isMe e.preventDefault();
? 'module-message__reactions__reaction--is-me' this.toggleReactionViewer();
: null }}
)} onKeyDown={e => {
style={{ // Prevent enter key from opening stickers/attachments
top: `${i * offset}px`, if (e.key === 'Enter') {
}}
onClick={e => {
e.stopPropagation(); e.stopPropagation();
this.toggleReactionViewer(); }
}} }}
onKeyDown={e => { >
// Prevent enter key from opening stickers/attachments {isMore ? (
if (e.key === 'Enter') { <span
e.stopPropagation(); className={classNames(
} 'module-message__reactions__reaction__count',
}} 'module-message__reactions__reaction__count--no-emoji',
> isMoreWithMe
<Emoji size={18} emoji={re.emoji} /> ? 'module-message__reactions__reaction__count--is-me'
</button> : null
))} )}
</div> >
)} +{maybeNotRenderedTotal}
</Measure> </span>
) : (
<React.Fragment>
<Emoji size={16} emoji={re.emoji} />
{re.count > 1 ? (
<span
className={classNames(
'module-message__reactions__reaction__count',
re.isMe
? 'module-message__reactions__reaction__count--is-me'
: null
)}
>
{re.count}
</span>
) : null}
</React.Fragment>
)}
</button>
);
})}
</div>
)} )}
</Reference> </Reference>
{reactionViewerRoot && {reactionViewerRoot &&
@ -1604,14 +1659,6 @@ export class Message extends React.PureComponent<Props, State> {
style={{ style={{
...style, ...style,
zIndex: 2, zIndex: 2,
marginTop: -(height - reactionHeight * 0.75),
...(outgoing
? {
marginRight: reactionHeight * -0.375,
}
: {
marginLeft: reactionHeight * -0.375,
}),
}} }}
reactions={reactions} reactions={reactions}
i18n={i18n} i18n={i18n}
@ -1816,6 +1863,7 @@ export class Message extends React.PureComponent<Props, State> {
isTapToView, isTapToView,
isTapToViewExpired, isTapToViewExpired,
isTapToViewError, isTapToViewError,
reactions,
} = this.props; } = this.props;
const { isSelected } = this.state; const { isSelected } = this.state;
@ -1844,6 +1892,9 @@ export class Message extends React.PureComponent<Props, State> {
: null, : null,
isTapToViewError isTapToViewError
? 'module-message__container--with-tap-to-view-error' ? 'module-message__container--with-tap-to-view-error'
: null,
reactions && reactions.length > 0
? 'module-message__container--with-reactions'
: null : null
); );
const containerStyles = { const containerStyles = {
@ -1851,11 +1902,24 @@ export class Message extends React.PureComponent<Props, State> {
}; };
return ( return (
<div className={containerClassnames} style={containerStyles}> <Measure
{this.renderAuthor()} bounds={true}
{this.renderContents()} onResize={({ bounds = { width: 0 } }) => {
{this.renderAvatar()} this.setState({ containerWidth: bounds.width });
</div> }}
>
{({ measureRef }) => (
<div
ref={measureRef}
className={containerClassnames}
style={containerStyles}
>
{this.renderAuthor()}
{this.renderContents()}
{this.renderAvatar()}
</div>
)}
</Measure>
); );
} }

View file

@ -61,6 +61,20 @@ export type MessageType = {
pending: boolean; pending: boolean;
}; };
}; };
unread: boolean;
reactions?: Array<{
emoji: string;
timestamp: number;
from: {
id: string;
color?: string;
avatarPath?: string;
name?: string;
profileName?: string;
isMe?: boolean;
phoneNumber?: string;
};
}>;
// No need to go beyond this; unused at this stage, since this goes into // No need to go beyond this; unused at this stage, since this goes into
// a reducer still in plain JavaScript and comes out well-formed // a reducer still in plain JavaScript and comes out well-formed
@ -573,6 +587,14 @@ function hasMessageHeightChanged(
return true; return true;
} }
const currentReactions = message.reactions || [];
const lastReactions = previous.reactions || [];
const reactionsChanged =
(currentReactions.length === 0) !== (lastReactions.length === 0);
if (reactionsChanged) {
return true;
}
return false; return false;
} }

View file

@ -9250,17 +9250,17 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();", "line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
"lineNumber": 176, "lineNumber": 177,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-01-21T23:01:37.636Z" "updated": "2020-02-03T17:18:39.600Z"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " > = React.createRef();", "line": " > = React.createRef();",
"lineNumber": 180, "lineNumber": 181,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-01-21T23:01:37.636Z" "updated": "2020-02-03T17:18:39.600Z"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",