Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve chat page UX #596

Merged
merged 1 commit into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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