Skip to content

Commit

Permalink
feat: smart typing delay and typing indication
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthieuJnon committed Nov 15, 2019
1 parent 6faa8cb commit 8738c63
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 44 deletions.
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"eslint.autoFixOnSave": true,
"prettier.singleQuote": true,
"prettier.jsxBracketSameLine": true,
"prettier.jsxSingleQuote": true,
"eslint.enable": true
}
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ In your `<body/>`:
WebChat.default.init({
selector: "#webchat",
initPayload: "/get_started",
interval: 1000, // 1000 ms between each message
customData: {"userId": "123"}, // arbitrary custom data. Stay minimal as this will be added to the socket
socketUrl: "http://localhost:5500",
socketPath: "/socket.io/",
Expand Down Expand Up @@ -74,7 +73,6 @@ import { Widget } from 'rasa-webchat';
function CustomWidget = () => {
return (
<Widget
interval={2000}
initPayload={"/get_started"}
socketUrl={"http://localhost:5500"}
socketPath={"/socket.io/"}
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const plugin = {
socketUrl={args.socketUrl}
socketPath={args.socketPath}
protocolOptions={args.protocolOptions}
interval={args.interval}
initPayload={args.initPayload}
title={args.title}
subtitle={args.subtitle}
Expand All @@ -33,6 +32,7 @@ const plugin = {
docViewer={args.docViewer}
displayUnreadCount={args.displayUnreadCount}
showMessageDate={args.showMessageDate}
customMessageDelay={args.customMessageDelay}
/>, document.querySelector(args.selector)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,37 +38,40 @@ class Messages extends Component {
getComponentToRender = (message, index, isLast) => {
const { params } = this.props;
const ComponentToRender = (() => {
switch(message.get('type')){
switch (message.get('type')) {
case MESSAGES_TYPES.TEXT: {
return Message
return Message;
}
case MESSAGES_TYPES.SNIPPET.LINK: {
return Snippet
return Snippet;
}
case MESSAGES_TYPES.VIDREPLY.VIDEO: {
return Video
return Video;
}
case MESSAGES_TYPES.IMGREPLY.IMAGE: {
return Image
return Image;
}
case MESSAGES_TYPES.QUICK_REPLY: {
return QuickReply
return QuickReply;
}
case MESSAGES_TYPES.CUSTOM_COMPONENT:
return connect(
store => ({ store }),
dispatch => ({ dispatch })
)(this.props.customComponent);
default:
return null;
}
return null
})()
})();
if (message.get('type') === 'component') {
return <ComponentToRender id={index} {...message.get('props')} isLast={isLast}/>;
return <ComponentToRender id={index} {...message.get('props')} isLast={isLast} />;
}
return <ComponentToRender id={index} params={params} message={message} isLast={isLast} />;
}

render() {
const { displayTypingIndication } = this.props;

const renderMessages = () => {
const {
messages,
Expand Down Expand Up @@ -131,6 +134,17 @@ class Messages extends Component {
return (
<div id="messages" className="messages-container">
{ renderMessages() }
{displayTypingIndication && (
<div className="message">
<div className="response">
<div id="wave">
<span className="dot" />
<span className="dot" />
<span className="dot" />
</div>
</div>
</div>
)}
</div>
);
}
Expand All @@ -140,9 +154,11 @@ Messages.propTypes = {
messages: ImmutablePropTypes.listOf(ImmutablePropTypes.map),
profileAvatar: PropTypes.string,
customComponent: PropTypes.func,
showMessageDate: PropTypes.oneOfType([PropTypes.bool, PropTypes.func])
showMessageDate: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
displayTypingIndication: PropTypes.bool.isRequired
};

export default connect(store => ({
messages: store.messages
messages: store.messages,
displayTypingIndication: store.behavior.get('messageDelayed')
}))(Messages);
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,39 @@
@include messages-container-fs;
}
}

div#wave {
position:relative;
text-align:center;
width:25px;
height:13px;
margin-left: auto;
margin-right: auto;
.dot {
display:inline-block;
width:5px;
height:5px;
border-radius:50%;
margin-right:3px;
background:#939393;
animation: wave 1.6s linear infinite;

&:nth-child(2) {
animation-delay: -1.4s;
}

&:nth-child(3) {
animation-delay: -1.2s;
}
}
}

@keyframes wave {
0%, 60%, 100% {
transform: initial;
}

30% {
transform: translateY(-5px);
}
}
61 changes: 42 additions & 19 deletions src/components/Widget/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
connectServer,
disconnectServer,
pullSession,
newUnreadMessage
newUnreadMessage,
triggerMessageDelayed
} from 'actions';

import { SESSION_NAME, NEXT_MESSAGE } from 'constants';
Expand All @@ -30,21 +31,7 @@ class Widget extends Component {
constructor(props) {
super(props);
this.messages = [];

const {
interval,
isChatOpen,
dispatch
} = this.props;

setInterval(() => {
if (this.messages.length > 0) {
this.dispatchMessage(this.messages.shift());
if (!isChatOpen) {
dispatch(newUnreadMessage());
}
}
}, interval);
this.onGoingMessageDelay = false;
}

componentDidMount() {
Expand Down Expand Up @@ -119,6 +106,39 @@ class Widget extends Component {
return localId;
}

handleMessageReceived(message) {
const { dispatch } = this.props;
if (!this.onGoingMessageDelay) {
this.onGoingMessageDelay = true;
dispatch(triggerMessageDelayed(true));
this.newMessageTimeout(message);
} else {
this.messages.push(message);
}
}

popLastMessage() {
const { dispatch } = this.props;
if (this.messages.length) {
this.onGoingMessageDelay = true;
dispatch(triggerMessageDelayed(true));
this.newMessageTimeout(this.messages.shift());
}
}

newMessageTimeout(message) {
const { dispatch, isChatOpen, customMessageDelay } = this.props;
setTimeout(() => {
this.dispatchMessage(message);
if (!isChatOpen) {
dispatch(newUnreadMessage());
}
dispatch(triggerMessageDelayed(false));
this.onGoingMessageDelay = false;
this.popLastMessage();
}, customMessageDelay(message.text || ''));
}

initializeWidget() {
const {
storage,
Expand All @@ -133,7 +153,7 @@ class Widget extends Component {

socket.on('bot_uttered', (botUttered) => {
const newMessage = { ...botUttered, text: String(botUttered.text) };
this.messages.push(newMessage);
this.handleMessageReceived(newMessage);
});

dispatch(pullSession());
Expand All @@ -146,6 +166,7 @@ class Widget extends Component {

// When session_confirm is received from the server:
socket.on('session_confirm', (remoteId) => {
// eslint-disable-next-line no-console
console.log(`session_confirm:${socket.socket.id} session_id:${remoteId}`);

// Store the initial state to both the redux store and the storage, set connected to true
Expand Down Expand Up @@ -181,6 +202,7 @@ class Widget extends Component {
});

socket.on('disconnect', (reason) => {
// eslint-disable-next-line no-console
console.log(reason);
if (reason !== 'io client disconnect') {
dispatch(disconnectServer());
Expand Down Expand Up @@ -220,6 +242,7 @@ class Widget extends Component {
// check that session_id is confirmed
if (!sessionId) return;

// eslint-disable-next-line no-console
console.log('sending init payload', sessionId);
socket.emit('user_uttered', { message: initPayload, customData, session_id: sessionId });
dispatch(initialize());
Expand Down Expand Up @@ -320,7 +343,6 @@ const mapStateToProps = state => ({
});

Widget.propTypes = {
interval: PropTypes.number,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
customData: PropTypes.shape({}),
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
Expand All @@ -344,7 +366,8 @@ Widget.propTypes = {
closeImage: PropTypes.string,
customComponent: PropTypes.func,
displayUnreadCount: PropTypes.bool,
showMessageDate: PropTypes.oneOfType([PropTypes.bool, PropTypes.func])
showMessageDate: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
customMessageDelay: PropTypes.func.isRequired
};

Widget.defaultProps = {
Expand Down
15 changes: 10 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ const ConnectedWidget = (props) => {
return (
<Provider store={store}>
<Widget
interval={props.interval}
initPayload={props.initPayload}
title={props.title}
subtitle={props.subtitle}
Expand All @@ -114,14 +113,14 @@ const ConnectedWidget = (props) => {
displayUnreadCount={props.displayUnreadCount}
socket={sock}
showMessageDate={props.showMessageDate}
customMessageDelay={props.customMessageDelay}
/>
</Provider>
);
};

ConnectedWidget.propTypes = {
initPayload: PropTypes.string,
interval: PropTypes.number,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
protocol: PropTypes.string,
Expand Down Expand Up @@ -149,13 +148,13 @@ ConnectedWidget.propTypes = {
docViewer: PropTypes.bool,
customComponent: PropTypes.func,
displayUnreadCount: PropTypes.bool,
showMessageDate: PropTypes.oneOfType([PropTypes.bool, PropTypes.func])
showMessageDate: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
customMessageDelay: PropTypes.func
};

ConnectedWidget.defaultProps = {
title: 'Welcome',
customData: {},
interval: 2000,
inputTextFieldHint: 'Type a message...',
connectingText: 'Waiting for server...',
fullScreenMode: false,
Expand All @@ -175,7 +174,13 @@ ConnectedWidget.defaultProps = {
showCloseButton: true,
showFullScreenButton: false,
displayUnreadCount: false,
showMessageDate: false
showMessageDate: false,
customMessageDelay: (message) => {
let delay = message.length * 30;
if (delay > 2 * 1000) delay = 3 * 1000;
if (delay < 400) delay = 1000;
return delay;
}
};

export default ConnectedWidget;
4 changes: 2 additions & 2 deletions src/scss/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ $roboto: 'Roboto', serif;
@mixin message-bubble($color, $textColor) {
background-color: $color;
color: $textColor;
border-radius: 10px;
padding: 15px;
border-radius: 15px;
padding: 11px 15px;
max-width: 215px;
text-align: left;
font-family: $roboto;
Expand Down
2 changes: 1 addition & 1 deletion src/store/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ export const INSERT_NEW_USER_MESSAGE = 'INSERT_NEW_USER_MESSAGE';
export const DROP_MESSAGES = 'DROP_MESSAGES';
export const PULL_SESSION = 'PULL_SESSION';
export const NEW_UNREAD_MESSAGE = 'NEW_UNREAD_MESSAGE';

export const TRIGGER_MESSAGE_DELAY = 'TRIGGER_MESSAGE_DELAY';
7 changes: 7 additions & 0 deletions src/store/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,10 @@ export function newUnreadMessage() {
};
}

export function triggerMessageDelayed(messageDelayed) {
return {
type: actions.TRIGGER_MESSAGE_DELAY,
messageDelayed
};
}

Loading

0 comments on commit 8738c63

Please sign in to comment.