Skip to content

Commit 53007af

Browse files
kaxi1993adsbooker
authored andcommitted
[Contribution: @kaxi1993] Add typing indicator (#127)
* Add typing indicator * Update readme to include typing feature * fix sample code issue in readme * Update demo to include typing example * Remove vendor prefixes from typing indicator style * Update typing indicator prop name * Add missing comma
1 parent 15ff352 commit 53007af

File tree

9 files changed

+130
-13
lines changed

9 files changed

+130
-13
lines changed

README.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class Demo extends Component {
4242
constructor() {
4343
super();
4444
this.state = {
45-
messageList: []
45+
messageList: [],
46+
isTyping: true
4647
};
4748
}
4849

@@ -74,6 +75,7 @@ class Demo extends Component {
7475
onMessageWasSent={this._onMessageWasSent.bind(this)}
7576
messageList={this.state.messageList}
7677
showEmoji
78+
showTypingIndicator={this.state.isTyping}
7779
/>
7880
</div>)
7981
}
@@ -92,15 +94,16 @@ Launcher props:
9294

9395
| prop | type | required | description |
9496
|------------------|--------|----------|-------------|
95-
| agentProfile | [object](#agent-profile-objects) | yes | Represents your product or service's customer service agent. Fields: imageUrl (string), teamName (string). |
96-
| handleClick | function | yes | Intercept the click event on the launcher. No argument sent when function is called. |
97-
| isOpen | boolean | yes | Force the open/close state of the chat window. If this is not set, it will open and close when clicked. |
98-
| messageList | [[message](#message-objects)] | yes | An array of message objects to be rendered as a conversation. |
99-
| mute | boolean | no | Don't play sound for incoming messages. Defaults to `false`. |
100-
| newMessagesCount | number | no | The number of new messages. If greater than 0, this number will be displayed in a badge on the launcher. Defaults to `0`. |
101-
| onFilesSelected | function([fileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList)) | no | Called after file has been selected from dialogue in chat window. |
102-
| onMessageWasSent | function([message](#message-objects)) | yes | Called when a message is sent, with a message object as an argument. |
103-
| showEmoji | boolean | no | Whether or not to show the emoji button in the input bar. Defaults to `true`.
97+
| agentProfile | [object](#agent-profile-objects) | yes | Represents your product or service's customer service agent. Fields: imageUrl (string), teamName (string). |
98+
| handleClick | function | yes | Intercept the click event on the launcher. No argument sent when function is called. |
99+
| isOpen | boolean | yes | Force the open/close state of the chat window. If this is not set, it will open and close when clicked. |
100+
| messageList | [[message](#message-objects)] | yes | An array of message objects to be rendered as a conversation. |
101+
| mute | boolean | no | Don't play sound for incoming messages. Defaults to `false`. |
102+
| newMessagesCount | number | no | The number of new messages. If greater than 0, this number will be displayed in a badge on the launcher. Defaults to `0`. |
103+
| onFilesSelected | function([fileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList)) | no | Called after file has been selected from dialogue in chat window. |
104+
| onMessageWasSent | function([message](#message-objects)) | yes | Called when a message is sent, with a message object as an argument. |
105+
| showEmoji | boolean | no | Whether or not to show the emoji button in the input bar. Defaults to `true`.
106+
| showTypingIndicator | boolean | no | Whether or not to show typing indicator in the message box. Defaults to `false`.
104107

105108

106109
### Message Objects

demo/src/TestArea.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
import React, { Component } from 'react';
22

33
class TestArea extends Component {
4+
constructor (props) {
5+
super(props);
6+
7+
this.timeout = null;
8+
this._onKeyUp = this._onKeyUp.bind(this);
9+
}
10+
11+
_onKeyUp() {
12+
if (this.timeout) {
13+
clearTimeout(this.timeout);
14+
}
15+
16+
this.props.startTyping();
17+
18+
// stop typing after 1 second of inactivity
19+
this.timeout = setTimeout(() => {
20+
this.props.stopTyping();
21+
}, 1000);
22+
}
23+
424
render () {
525
return (
626
<div className="demo-test-area--wrapper">
@@ -13,13 +33,15 @@ class TestArea extends Component {
1333
<form className="demo-test-area" onSubmit={(e)=> {
1434
e.preventDefault();
1535
this.props.onMessage(this.textArea.value);
36+
this.props.stopTyping();
1637
this.textArea.value = '';
1738
}}>
1839
<div className="demo-test-area--preamble">Test the chat window by sending a message:</div>
1940
<textarea
2041
ref={(e) => { this.textArea = e; }}
2142
className="demo-test-area--text"
2243
placeholder="Write a test message...."
44+
onKeyUp={this._onKeyUp}
2345
/>
2446
<button className="demo-test-area--button"> Send Message! </button>
2547
</form>

demo/src/index.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class Demo extends Component {
1717
this.state = {
1818
messageList: messageHistory,
1919
newMessagesCount: 0,
20-
isOpen: false
20+
isOpen: false,
21+
isTyping: false
2122
};
2223
}
2324

@@ -54,6 +55,18 @@ class Demo extends Component {
5455
}
5556
}
5657

58+
_startTyping() {
59+
this.setState({
60+
isTyping: true
61+
});
62+
}
63+
64+
_stopTyping() {
65+
this.setState({
66+
isTyping: false
67+
});
68+
}
69+
5770
_handleClick() {
5871
this.setState({
5972
isOpen: !this.state.isOpen,
@@ -66,6 +79,8 @@ class Demo extends Component {
6679
<Header />
6780
<TestArea
6881
onMessage={this._sendMessage.bind(this)}
82+
startTyping={this._startTyping.bind(this)}
83+
stopTyping={this._stopTyping.bind(this)}
6984
/>
7085
<Launcher
7186
agentProfile={{
@@ -79,6 +94,7 @@ class Demo extends Component {
7994
handleClick={this._handleClick.bind(this)}
8095
isOpen={this.state.isOpen}
8196
showEmoji
97+
showTypingIndicator={this.state.isTyping}
8298
/>
8399
<img className="demo-monster-img" src={monsterImgUrl} />
84100
<Footer />

src/components/ChatWindow.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class ChatWindow extends Component {
3434
<MessageList
3535
messages={messageList}
3636
imageUrl={this.props.agentProfile.imageUrl}
37+
showTypingIndicator={this.props.showTypingIndicator}
3738
/>
3839
<UserInput
3940
onSubmit={this.onUserInputSubmit.bind(this)}
@@ -51,7 +52,8 @@ ChatWindow.propTypes = {
5152
onClose: PropTypes.func.isRequired,
5253
onFilesSelected: PropTypes.func,
5354
onUserInputSubmit: PropTypes.func.isRequired,
54-
showEmoji: PropTypes.bool
55+
showEmoji: PropTypes.bool,
56+
showTypingIndicator: PropTypes.bool
5557
};
5658

5759
export default ChatWindow;

src/components/Launcher.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class Launcher extends Component {
6060
isOpen={isOpen}
6161
onClose={this.handleClick.bind(this)}
6262
showEmoji={this.props.showEmoji}
63+
showTypingIndicator={this.props.showTypingIndicator}
6364
/>
6465
</div>
6566
);
@@ -84,11 +85,13 @@ Launcher.propTypes = {
8485
messageList: PropTypes.arrayOf(PropTypes.object),
8586
mute: PropTypes.bool,
8687
showEmoji: PropTypes.bool,
88+
showTypingIndicator: PropTypes.bool
8789
};
8890

8991
Launcher.defaultProps = {
9092
newMessagesCount: 0,
91-
showEmoji: true
93+
showEmoji: true,
94+
showTypingIndicator: false
9295
};
9396

9497
export default Launcher;

src/components/MessageList.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { Component } from 'react';
22
import Message from './Messages';
3+
import TypingIndicator from './TypingIndicator';
34

45
class MessageList extends Component {
56

@@ -13,6 +14,7 @@ class MessageList extends Component {
1314
{this.props.messages.map((message, i) => {
1415
return <Message message={message} key={i} />;
1516
})}
17+
{this.props.showTypingIndicator && <TypingIndicator />}
1618
</div>);
1719
}
1820
}

src/components/TypingIndicator.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react'
2+
3+
function TypingIndicator () {
4+
return (
5+
<div className="sc-typing-indicator">
6+
<div className="sc-typing-indicator--message">
7+
<div className="sc-typing-indicator--dot"></div>
8+
<div className="sc-typing-indicator--dot"></div>
9+
<div className="sc-typing-indicator--dot"></div>
10+
</div>
11+
</div>
12+
)
13+
}
14+
15+
16+
export default TypingIndicator

src/styles/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ import './header.css';
55
import './message.css';
66
import './user-input.css';
77
import './popup-window.css';
8+
import './typing-indicator.css';

src/styles/typing-indicator.css

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.sc-typing-indicator {
2+
padding-top: 20px;
3+
width: 300px;
4+
margin: auto;
5+
}
6+
7+
.sc-typing-indicator--circle {
8+
width: 28px;
9+
height: 22px;
10+
padding: 5px 8px;
11+
border-radius: 15px;
12+
background-color: #f4f7f9;
13+
display: flex;
14+
justify-content: center;
15+
align-items: center;
16+
}
17+
18+
.sc-typing-indicator--dot {
19+
background-color: #90949c;
20+
animation: typing-indicator 1.5s infinite ease-in-out;
21+
display: inline-block;
22+
border-radius: 2px;
23+
margin-right: 2px;
24+
height: 4px;
25+
width: 4px;
26+
}
27+
28+
@keyframes typing-indicator {
29+
0% {
30+
transform: translateY(0px)
31+
}
32+
33+
28% {
34+
transform: translateY(-5px)
35+
}
36+
37+
44% {
38+
transform: translateY(0px)
39+
}
40+
}
41+
42+
.sc-typing-indicator--dot:nth-child(1) {
43+
animation-delay: 200ms;
44+
}
45+
46+
.sc-typing-indicator--dot:nth-child(2) {
47+
animation-delay: 300ms;
48+
}
49+
50+
.sc-typing-indicator--dot:nth-child(3) {
51+
animation-delay: 400ms;
52+
}

0 commit comments

Comments
 (0)