Yesterday I created the front-end of the ChatBox for TripTime :)
Here’s what it looks like:

I met with a couple of challenges along the way, but happy to see how it turns out to look.

Positioning of the bubbles

Using the grid display to position the chat bubbles were my first challenge. Here’s what the code looks like for the container of message bubbles, which takes up the 100% width of the chat box and takes care of the positioning of avatar, info and chat content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.chatMessageContainer, .myChatMessageContainer {
width: 100%;
display: grid;
padding: 1rem;
box-sizing: border-box;
}
.chatMessageContainer {
grid-template-columns: 2rem 1rem auto 1fr;
grid-template-rows: 1rem 1fr;
grid-template-areas: 'messageAvatar . messageInfo .' 'messageAvatar . messageContent .';
}

.myChatMessageContainer {
grid-template-columns: 1fr auto 1rem 2rem;
grid-template-rows: 1rem 1fr;
grid-template-areas: '. messageInfo . messageAvatar' '. messageContent . messageAvatar';
}

Here’s the grid structure of the containers:

grid grid right

Note here

For grie-template-columns/grie-template-rows,

  • auto means the width of the child depends on itself.
  • 1fr means the child takes up one fraction of whatever space is left.
    So the empty space takes up 1fr, and the bubble takes up auto.

The little triangle attached to the bubble

triangle To get this tiny triangle, I added `before` pseudo-element for the bubbles:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.messageContent::before, .myMessageContent::before{
content: " ";
height: 0;
position: absolute;
top: 0.2rem;
width: 0;
border: solid;
}
.messageContent::before{
left: -0.7rem;
border-width: 0.3rem 0.7rem 0.3rem 0;
border-color: transparent var(--trip-orange) transparent transparent;
}
.myMessageContent::before{
right: -0.7rem;
border-width: 0.3rem 0 0.3rem 0.7rem;
border-color: transparent transparent transparent var(--trip-gray) ;
}

Managing the width of the bubble

The width of the bubble were adjusted responsively.
Firstly, for any screen size:

1
2
3
.messageContent, .myMessageContent {
min-width: 10rem;
}

Then for big screen:

1
2
3
4
5
@media screen and (min-width: 1000px) {
.messageContent, .myMessageContent {
max-width: 50vw;
}
}

For small screen:

1
2
3
4
5
@media screen and (max-width: 1000px) {
.messageContent, .myMessageContent {
max-width: 70vw;
}
}

When the 70vw is bigger than the space available, it will just take up the whole space instead of flowing out, which is pretty handy.

Keep at the bottom of container

To keep the ChatBox showing the newest message, I used an npm package react-scroll-to-bottom:

React container that will auto scroll to bottom or top if new content is added and viewport is at the bottom, similar to tail -f. Otherwise, a “jump to bottom” button will be shown to allow user to quickly jump to bottom.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import ScrollToBottom from 'react-scroll-to-bottom';

export default class ChatMessageList extends React.Component {
render() {
const messages = this.props.messages;
return (
<ScrollToBottom className={styles.messageListContainer}>
{messages.map((message, index) => (
<ChatMessage
chatMessage={message}
key={index}
isMine={message.author.id === this.props.userID}
/>
))}
</ScrollToBottom>
);
}
}

Handling incoming messages

Only the ChatBox component knows about the chat message list API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// inside ChatBox class:
handleIncomingNewMessages(incomeMessages) {
this.setState(state => ({
newMessageNum: state.newMessageNum + incomeMessages.length,
messages: state.messages.concat(incomeMessages),
}));
} // This is the method to take care of API call for new message

handleMyNewMessage(myNewMessage) {
this.setState(state => ({
newMessageNum: 0,
messages: [...state.messages, myNewMessage],
// Also need to submit it to the backend here
}));
}

The ChatMessageList will know about the current message list, and the user ID (so that it can tell between my messages and others):

1
2
3
4
<ChatMessageList
messages={this.state.messages}
userID={this.getCurrentUser().id}
/>

The ChatInputForm will know about how to handle the user’s new message:

1
2
3
4
<ChatInputForm
newMessageHandler={this.handleMyNewMessage}
me={this.getCurrentUser()}
/>

The code can be find here.
The CSS module is here.

It’s fun to create a cutie like this, let’s see how it works with WebSocket in the future:)