Skip to content

Commit

Permalink
Improve chat page UX (GladysAssistant#596)
Browse files Browse the repository at this point in the history
Fix GladysAssistant#588 : Messages are re-ordered on receive
  • Loading branch information
Pierre-Gilles authored Oct 30, 2019
1 parent 5a05b22 commit 3c69a29
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 66 deletions.
65 changes: 54 additions & 11 deletions front/src/actions/message.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { RequestStatus } from '../utils/consts';
import update from 'immutability-helper';
import uuid from 'uuid';

const TYPING_MIN_TIME = 300;
const TYPING_MIN_TIME = 400;
const TYPING_MAX_TIME = 600;

const sortMessages = messages =>
messages.sort((a, b) => {
if (a.created_at < b.created_at) {
return -1;
}
if (a.created_at > b.created_at) {
return 1;
}
return 0;
});

function createActions(store) {
const actions = {
scrollToBottom() {
const chatWindow = document.getElementById('chat-window');
setTimeout(() => chatWindow.scrollTo(0, chatWindow.scrollHeight), 10);
setTimeout(() => {
const chatWindow = document.getElementById('chat-window');
if (chatWindow) {
chatWindow.scrollTo(0, chatWindow.scrollHeight);
}
}, 20);
},
async getMessages(state) {
store.setState({
MessageGetStatus: RequestStatus.Getting
});
try {
const messages = await state.httpClient.get('/api/v1/message');
messages.reverse();
let messages = await state.httpClient.get('/api/v1/message');
messages = sortMessages(messages);
store.setState({
messages,
MessageGetStatus: RequestStatus.Success
Expand All @@ -40,9 +56,10 @@ function createActions(store) {
actions.scrollToBottom();
const randomWait = Math.floor(Math.random() * TYPING_MAX_TIME) + TYPING_MIN_TIME;
setTimeout(() => {
const newMessages = update(store.getState().messages, {
let newMessages = update(store.getState().messages, {
$push: [message]
});
newMessages = sortMessages(newMessages);
store.setState({
gladysIsTyping: false,
messages: newMessages
Expand All @@ -56,26 +73,52 @@ function createActions(store) {
}
},
async sendMessage(state) {
if (!state.currentMessageTextInput || state.currentMessageTextInput.length === 0) {
return;
}
store.setState({
MessageSendStatus: RequestStatus.Getting
});
const messageText = state.currentMessageTextInput;
try {
const message = await state.httpClient.post('/api/v1/message', {
text: state.currentMessageTextInput
});
const newMessage = {
text: messageText,
created_at: new Date()
};
const tempId = uuid.v4();
// we first push the message
const newState = update(state, {
messages: {
$push: [message]
$push: [Object.assign({}, newMessage, { tempId })]
},
MessageSendStatus: {
$set: RequestStatus.Success
$set: RequestStatus.Getting
},
currentMessageTextInput: {
$set: ''
}
});
newState.messages = sortMessages(newState.messages);
store.setState(newState);
actions.scrollToBottom();
// then we send the message
const createdMessage = await state.httpClient.post('/api/v1/message', newMessage);
const messagesWithoutTempMessage = store.getState().messages.filter(message => message.tempId !== tempId);
messagesWithoutTempMessage.push(createdMessage);
// then we remove the message loading
const finalState = update(state, {
messages: {
$set: sortMessages(messagesWithoutTempMessage)
},
MessageSendStatus: {
$set: RequestStatus.Success
},
currentMessageTextInput: {
$set: ''
}
});
store.setState(finalState);
actions.scrollToBottom();
} catch (e) {
store.setState({
MessageSendStatus: RequestStatus.Error
Expand Down
1 change: 1 addition & 0 deletions front/src/assets/images/undraw_typing.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,11 @@
"whatCanYouAsk": "What can I ask?",
"whatsTheWeatherLike": "What's the weather like?",
"whatsTheTemperatureKitchen": "What's the temperature in the kitchen?",
"showCameraImage": "Show me camera image in the kitchen"
"showCameraImage": "Show me camera image in the kitchen",
"emptyStateMessage": "Send me a message!",
"messagePlaceholder": "Type your message...",
"sendingInProgress": "Sending...",
"typingInProgress": "Typing..."
},
"header": {
"gladysAssistant": "Gladys Assistant",
Expand Down
17 changes: 13 additions & 4 deletions front/src/routes/chat/ChatItems.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Text } from 'preact-i18n';
import style from './style.css';

import dayjs from 'dayjs';
Expand Down Expand Up @@ -38,9 +39,13 @@ const OutGoingMessage = ({ children, ...props }) => (
<p>{props.message.text}</p>
<span class={style.time_date}>
{' '}
{dayjs(props.message.created_at)
.locale(props.user.language)
.fromNow()}
{props.message.tempId ? (
<Text id="chat.sendingInProgress" />
) : (
dayjs(props.message.created_at)
.locale(props.user.language)
.fromNow()
)}
</span>
</div>
</div>
Expand All @@ -57,7 +62,11 @@ const Messages = ({ children, ...props }) => (
return <OutGoingMessage user={props.user} message={message} />;
})}

{props.gladysIsTyping && <p>Typing...</p>}
{props.gladysIsTyping && (
<p>
<Text id="chat.typingInProgress" />
</p>
)}
</div>
</div>
</div>
Expand Down
130 changes: 82 additions & 48 deletions front/src/routes/chat/ChatPage.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,101 @@
import { Text } from 'preact-i18n';
import { Text, Localizer } from 'preact-i18n';
import cx from 'classnames';
import { connect } from 'unistore/preact';
import actions from '../../actions/message';
import { RequestStatus } from '../../utils/consts';
import ChatItems from './ChatItems';
import EmptyChat from './EmptyChat';

const IntegrationPage = connect(
'user,messages,currentMessageTextInput,gladysIsTyping',
'user,messages,currentMessageTextInput,gladysIsTyping,MessageGetStatus',
actions
)(({ user, messages, currentMessageTextInput, updateMessageTextInput, onKeyPress, sendMessage, gladysIsTyping }) => (
<div class="page">
<div class="page-main">
<div class="my-3 my-md-5">
<div class="container">
<div class="page-header" />
<div class="row">
<div class="col-lg-8">
<div class="card">
<ChatItems user={user} messages={messages} gladysIsTyping={gladysIsTyping} />
<div class="card-footer">
<div class="input-group">
<input
type="text"
class="form-control"
placeholder="Message"
value={currentMessageTextInput}
onInput={updateMessageTextInput}
onKeyPress={onKeyPress}
/>
<div class="input-group-append">
<button type="button" class="btn btn-secondary" onClick={sendMessage}>
<i class="fe fe-send" />
</button>
)(
({
user,
messages,
MessageGetStatus,
currentMessageTextInput,
updateMessageTextInput,
onKeyPress,
sendMessage,
gladysIsTyping
}) => (
<div class="page">
<div class="page-main">
<div class="my-3 my-md-5">
<div class="container">
<div class="page-header" />
<div class="row">
<div class="col-lg-8">
<div class="card">
<div
class={cx('dimmer', {
active: MessageGetStatus === RequestStatus.Getting
})}
>
<div class="loader" />
<div class="dimmer-content">
{messages && messages.length ? (
<ChatItems user={user} messages={messages} gladysIsTyping={gladysIsTyping} />
) : (
<EmptyChat />
)}
<div class="card-footer">
<div class="input-group">
<Localizer>
<input
type="text"
class="form-control"
placeholder={<Text id="chat.messagePlaceholder" />}
value={currentMessageTextInput}
onInput={updateMessageTextInput}
onKeyPress={onKeyPress}
/>
</Localizer>
<div class="input-group-append">
<button
type="button"
class="btn btn-secondary"
onClick={sendMessage}
disabled={!currentMessageTextInput || currentMessageTextInput.length === 0}
>
<i class="fe fe-send" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<Text id="chat.whatCanYouAsk" />
</h3>
</div>
<div class="card-body">
<ul>
<li>
"<Text id="chat.whatsTheWeatherLike" />"
</li>
<li>
"<Text id="chat.showCameraImage" />"
</li>
<li>
"<Text id="chat.whatsTheTemperatureKitchen" />"
</li>
</ul>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<Text id="chat.whatCanYouAsk" />
</h3>
</div>
<div class="card-body">
<ul>
<li>
"<Text id="chat.whatsTheWeatherLike" />"
</li>
<li>
"<Text id="chat.showCameraImage" />"
</li>
<li>
"<Text id="chat.whatsTheTemperatureKitchen" />"
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
));
)
);

export default IntegrationPage;
33 changes: 33 additions & 0 deletions front/src/routes/chat/EmptyChat.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Text } from 'preact-i18n';

const Messages = ({ children, ...props }) => (
<div
style={{
width: '40%',
maxWidth: '200px',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: '60px',
marginBottom: '60px',
textAlign: 'center'
}}
>
<img
src="/assets/images/undraw_typing.svg"
style={{
marginLeft: 'auto',
marginRight: 'auto',
display: 'block'
}}
/>
<p
style={{
marginTop: '20px'
}}
>
<Text id="chat.emptyStateMessage" />
</p>
</div>
);

export default Messages;
4 changes: 2 additions & 2 deletions server/api/controllers/message.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ module.exports = function MessageController(gladys) {
language: req.user.language,
source_user_id: req.user.id,
user: req.user,
created_at: new Date(),
created_at: req.body.created_at || new Date(),
};
gladys.event.emit(EVENTS.MESSAGE.NEW, messageToSend);
res.status(201).json({
text: req.body.text,
source: 'api_client',
language: req.user.language,
source_user_id: req.user.id,
created_at: new Date(),
created_at: messageToSend.created_at,
});
}

Expand Down

0 comments on commit 3c69a29

Please sign in to comment.