Commit 81d8d4b8 authored by Tasso Evangelista's avatar Tasso Evangelista

Add Screen to Chat route

parent 830fff08
import { Component } from 'preact';
import styles from './styles';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
import Avatar from '../../components/Avatar';
import DropFiles from '../../components/DropFiles';
import Composer, { Action, Actions } from '../../components/Composer';
import Typing from '../../components/TypingIndicator';
import Messages from '../../components/Message';
import { throttle, createClassName } from '../../components/helpers';
import DropFiles from '../../components/DropFiles';
import Messages from '../../components/Messages';
import Screen from '../../components/Screen';
import Sound from '../../components/Sound';
import TypingIndicator from '../../components/TypingIndicator';
import { debounce, createClassName } from '../../components/helpers';
import styles from './styles';
import EmojiIcon from '../../icons/smile.svg';
import PlusIcon from '../../icons/plus.svg';
import SendIcon from '../../icons/send.svg';
import Smile from 'icons/smile.svg';
import Plus from 'icons/plus.svg';
import Bell from 'icons/bell.svg';
import BellOff from 'icons/bellOff.svg';
import Arrow from 'icons/arrowDown.svg';
import NewWindow from 'icons/newWindow.svg';
export const isBottom = (el) => el.scrollHeight - el.scrollTop === el.clientHeight;
export const isTop = (el) => el.scrollTop === 0;
const toBottom = (el) => el.scrollTop = el.scrollHeight;
export default class Chat extends Component {
isBottom() {
return isBottom(this.el);
handleSoundRef = (sound) => {
this.sound = sound;
}
bind(el) {
this.el = el;
handleMessagesContainerRef = (messagesContainer) => {
this.messagesContainer = messagesContainer;
}
handleScroll() {
handleScroll = debounce(() => {
const { clientHeight, scrollTop, scrollHeight } = this.messagesContainer;
const atTop = scrollTop === 0;
const atBottom = scrollHeight - scrollTop === clientHeight;
const atBottom = isBottom(this.el);
if (this.state.atBottom !== atBottom) {
this.setState({ atBottom });
}
if (isTop(this.el)) {
return this.props.onTop && this.props.onTop();
}
const { onTop, onBottom } = this.props;
if (atBottom) {
return this.props.onBottom && this.props.onBottom();
if (atTop && onTop) {
return onTop();
}
}
if (atBottom && onBottom) {
return onBottom();
}
}, 100)
constructor(props) {
super(props);
this.state = {
atBottom: true,
};
this.handleScroll = this.handleScroll.bind(this);
this.bind = throttle(this.bind, 100).bind(this);
state = {
atBottom: true,
}
componentDidMount() {
toBottom(this.el);
toBottom(this.messagesContainer);
}
componentDidUpdate() {
if (this.state.atBottom) {
toBottom(this.el);
toBottom(this.messagesContainer);
}
}
renderNotification() {
// if (this.props.sound.enabled) {
// return <Bell width={20} />;
// }
return <BellOff width={20} />;
}
render = ({ onUpload, onPlaySound, typingUsers, onSubmit, color, messages, user, src, title, subtitle, uploads, emoji = true, notification, minimize, fullScreen, sound }) => (
<div class={styles.container}>
{/* <Sound onPlay={onPlaySound} src={sound.src} play={sound.play} /> */}
<Header color={color}>
{src && <Header.Picture><Avatar src={src} /></Header.Picture>}
<Header.Content>
<Header.Title>{title}</Header.Title>
<Header.SubTitle>{subtitle}</Header.SubTitle>
</Header.Content>
<Header.Actions>
<Header.Action onClick={notification}>{this.renderNotification()}</Header.Action>
<Header.Action onClick={minimize}><Arrow width={20} /></Header.Action>
<Header.Action onClick={fullScreen}><NewWindow width={20} /></Header.Action>
</Header.Actions>
</Header>
render = ({
color,
title,
agent,
sound,
user,
loading,
onUpload,
onPlaySound,
typingUsers,
onSubmit,
messages,
uploads = false,
emoji = false,
...props
}, {
atBottom = true,
}) => (
<Screen
color={color}
title={title || I18n.t('Need help?')}
agent={agent}
nopadding
footer={(
<Composer onUpload={onUpload}
onSubmit={onSubmit}
pre={emoji && (
<Actions>
<Action>
<EmojiIcon width={20} />
</Action>
</Actions>
)}
post={(
<Actions>
{uploads && (
<Action>
<PlusIcon width={20} />
</Action>
)}
<Action>
<SendIcon width={20} />
</Action>
</Actions>
)}
/>
)}
className={createClassName(styles, 'chat')}
{...props}
>
<Sound ref={this.handleSoundRef} onPlay={onPlaySound} src={sound.src} play={sound.play} />
<DropFiles onUpload={onUpload}>
<div className={createClassName(styles, 'main', { atBottom: this.state.atBottom, loading: this.props.loading })}>
<div ref={this.bind} onScroll={this.handleScroll} className={createClassName(styles, 'main__wrapper')}>
<div className={createClassName(styles, 'chat__messages', { atBottom, loading })}>
<div ref={this.handleMessagesContainerRef} onScroll={this.handleScroll} className={createClassName(styles, 'chat__wrapper')}>
<Messages messages={messages} user={user} />
{typingUsers && !!typingUsers.length && <Typing users={typingUsers} />}
{typingUsers && !!typingUsers.length && <TypingIndicator users={typingUsers} />}
</div>
</div>
</DropFiles>
<Footer>
<Footer.Content>
<Composer onUpload={onUpload}
onSubmit={onSubmit}
pre={
emoji && <Actions>
<Action>
<Smile width="20" />
</Action>
</Actions>
}
post={
uploads && <Actions>
<Action>
<Plus width="20" />
</Action>
</Actions>
}
placeholder="insert your text here"
/>
</Footer.Content>
<Footer.Content>
<Footer.PoweredBy />
</Footer.Content>
</Footer>
</div>
</Screen>
)
}
......@@ -4,36 +4,41 @@ import { Consumer, store } from '../../store';
import Chat from './component';
let rid = '';
class Wrapped extends Component {
async getUser() {
class ChatContainer extends Component {
rid = ''
getUser = async() => {
const { dispatch } = this.props;
const { state } = store;
let { user } = state;
if (user && user.token) {
return user;
}
this.setState({ loading: true });
dispatch({ loading: true });
const { defaultToken } = state;
user = await SDK.grantVisitor({ visitor: { token: defaultToken } });
this.setState({ loading: false });
this.actions({ user });
dispatch({ loading: false });
dispatch({ user });
return user;
}
async getRoomId(token) {
if (!rid) {
getRoomId = async(token) => {
const { dispatch } = this.props;
if (!this.rid) {
try {
const room = await SDK.room({ token });
rid = room._id;
this.actions({ room });
this.rid = room._id;
dispatch({ room });
} catch (error) {
throw error;
}
}
return rid;
return this.rid;
}
async sendMessage(msg) {
sendMessage = async(msg) => {
if (msg.trim() === '') {
return;
}
......@@ -47,19 +52,21 @@ class Wrapped extends Component {
}
async onTop() {
onTop = async() => {
const { dispatch } = this.props;
if (this.state.ended) {
return;
}
const { state } = store;
const { messages } = state;
this.setState({ loading: true });
const moreMessages = await SDK.loadMessages(rid, { limit: messages.length + 10 });
const moreMessages = await SDK.loadMessages(this.rid, { limit: messages.length + 10 });
this.setState({ loading: false, ended: messages.length + 10 >= moreMessages.length });
this.actions({ messages: (moreMessages || []).reverse() });
dispatch({ messages: (moreMessages || []).reverse() });
}
onUpload(files) {
onUpload = (files) => {
const { state } = store;
const { user: { token } } = state;
......@@ -67,7 +74,7 @@ class Wrapped extends Component {
files.forEach(async(file) => {
const formData = new FormData();
formData.append('file', file);
await fetch(`http://localhost:3000/api/v1/livechat/upload/${ rid }`, {
await fetch(`http://localhost:3000/api/v1/livechat/upload/${ this.rid }`, {
body: formData,
method: 'POST',
headers: { 'x-visitor-token': token },
......@@ -80,48 +87,27 @@ class Wrapped extends Component {
});
}
onPlaySound() {
onPlaySound = () => {
const { dispatch } = this.props;
const { state } = store;
const sound = Object.assign(state.sound, { play: false });
this.actions({ sound });
dispatch({ sound });
}
notification() {
notification = () => {
const { dispatch } = this.props;
const { state } = store;
const enabled = !state.sound.enabled;
const sound = Object.assign(state.sound, { enabled });
this.actions({ sound });
}
title(agent, theme) {
if (agent) {
return agent.name;
}
return (theme && theme.title) || I18n.t('Need help?');
}
subTitle(agent) {
if (!agent) {
return;
}
const { username, emails } = agent;
if (emails && emails[0]) {
return emails[0].address;
}
return username;
dispatch({ sound });
}
constructor() {
super();
this.state = {
loading: false,
};
constructor(props) {
super(props);
const { state } = store;
const { config: { settings: { fileUpload } } } = state;
rid = state.room && state.room._id;
this.rid = state.room && state.room._id;
this.sendMessage = this.sendMessage.bind(this);
this.onTop = this.onTop.bind(this);
this.onUpload = fileUpload && this.onUpload.bind(this);
......@@ -130,46 +116,73 @@ class Wrapped extends Component {
}
async componentDidMount() {
const { dispatch } = this.props;
const { state } = store;
const { token } = state.user;
if (rid) {
if (this.rid) {
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({ loading: true });
const messages = await SDK.loadMessages(rid, { token });
const messages = await SDK.loadMessages(this.rid, { token });
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({ loading: false });
this.actions({ messages: (messages || []).reverse() });
dispatch({ messages: (messages || []).reverse() });
}
}
render(props) {
return (
<Consumer>
{
({ typing, user, dispatch, sound, config: { theme, settings }, agent, messages }) => {
this.actions = dispatch;
return (
<Chat
{...props}
onTop={this.onTop}
user={user}
typingUsers={typing}
loading={this.state.loading}
onSubmit={this.sendMessage}
color={theme.color}
onUpload={this.onUpload}
messages={messages}
uploads={settings.fileUpload}
title={this.title(agent, theme)}
subtitle={this.subTitle(agent)}
src={agent && `http://localhost:3000/avatar/${ agent.username }`}
sound={sound}
onPlaySound={this.onPlaySound}
notification={this.notification}
/>);
}}
</Consumer>);
}
render = (props) => (
<Chat
{...props}
onTop={this.onTop}
onSubmit={this.sendMessage}
onUpload={this.onUpload}
onPlaySound={this.onPlaySound}
notification={this.notification}
/>
)
}
export default Wrapped;
export const ChatConnector = ({ ref, ...props }) => (
<Consumer>
{({
theme: {
color,
title,
} = {},
settings: {
fileUpload: uploads,
} = {},
agent,
sound,
user,
messages,
loading,
typing: typingUsers,
dispatch,
}) => (
<ChatContainer
ref={ref}
{...props}
color={color}
title={title || I18n.t('Need help?')}
agent={agent && {
name: agent.name,
status: agent.status,
email: agent.emails && agent.emails[0] && agent.emails[0].address,
username: agent.username,
avatarSrc: `http://localhost:3000/avatar/${ agent.username }`,
}}
sound={sound}
user={user}
messages={messages}
emoji={false}
uploads={uploads}
typingUsers={typingUsers}
loading={loading}
dispatch={dispatch}
/>
)}
</Consumer>
);
export default ChatConnector;
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import centered from '@storybook/addon-centered';
import { withKnobs, boolean, text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import { withKnobs, boolean, color, object, text } from '@storybook/addon-knobs';
import Chat from './component';
import soundSrc from '../../components/Sound/chime.mp3';
const data = [
{ u: { _id: 1 }, ts: new Date(), msg: 'Hello dido' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Welcome to my channel' },
{ u: { _id: 2 }, ts: new Date(), msg: '???' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Welcome to my channel' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Welcome to my channel' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Welcome to my channel' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Welcome to my channel' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Welcome to my channel' },
{ u: { _id: 1 }, ts: new Date(), msg: 'LARGE MESSAGE AAaasdkaskdlaskdl;kas;ldk;aslkd;aslkd;alsdk;alskd;al ;laskd;laskd;lask ;laskd;laskd;laskd;alk;sldk;alskd;aslkd;alskda;lskd;alskd;laskd;laskd;laks;dl' },
{ u: { _id: 1 }, ts: new Date(), msg: 'Lorem ipsum dolor sit amet, ea usu quod eirmod lucilius, mea veri viris concludaturque id, vel eripuit fabulas ea' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Putent appareat te sea, dico recusabo pri te' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Iudico utinam volutpat eos eu, sadipscing repudiandae pro te' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Movet doming ad ius, mel id adversarium disputationi' },
{ u: { _id: 1 }, ts: new Date(), msg: 'Adhuc latine et nec' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Vis at verterem adversarium concludaturque' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Sea no congue scripta persecuti, sed amet fabulas voluptaria ex' },
{ u: { _id: 2 }, ts: new Date(), msg: 'Invidunt repudiandae has eu' },
{ u: { _id: 1 }, ts: new Date(), msg: 'Veri soluta suscipit mel no' },
];
const screenCentered = (storyFn) => centered(() => (
<div style={{ display: 'flex', width: '365px', background: 'white' }}>
<div style={{ display: 'flex', width: '365px', height: '500px' }}>
{storyFn()}
</div>
));
......@@ -26,15 +27,109 @@ const screenCentered = (storyFn) => centered(() => (
storiesOf('Screen|Chat', module)
.addDecorator(screenCentered)
.addDecorator(withKnobs)
.add('loading', () => (
<Chat
color={color('color', '#175CC4')}
title={text('title', '')}
agent={object('agent', {
name: 'Guilherme Gazzo',
status: 'online',
email: 'guilherme.gazzo@rocket.chat',
username: '@guilherme.gazzo',
avatarSrc: '//gravatar.com/avatar/7ba3fcdd590033117b1e6587e0d20478?s=32',
})}
sound={{
src: soundSrc,
play: false,
}}
user={object('user', { _id: 1 })}
messages={object('messages', [])}
emoji={boolean('emoji', false)}
uploads={boolean('uploads', false)}
typingUsers={object('typingUsers', [])}
loading={boolean('loading', true)}
notificationsEnabled={boolean('notificationsEnabled', true)}
minimized={boolean('minimized', false)}
windowed={boolean('windowed', false)}
onEnableNotifications={action('enableNotifications')}
onDisableNotifications={action('disableNotifications')}
onMinimize={action('minimize')}
onRestore={action('restore')}
onOpenWindow={action('openWindow')}
onTop={action('top')}
onBottom={action('bottom')}
onUpload={action('upload')}
onSubmit={action('submit')}
/>
))
.add('normal', () => (
<Chat
color={color('color', '#175CC4')}
title={text('title', '')}
agent={object('agent', {
name: 'Guilherme Gazzo',
status: 'online',
email: 'guilherme.gazzo@rocket.chat',
username: '@guilherme.gazzo',
avatarSrc: '//gravatar.com/avatar/7ba3fcdd590033117b1e6587e0d20478?s=32',
})}
sound={{
src: soundSrc,
play: false,
}}
user={object('user', { _id: 1 })}
messages={object('messages', data)}
emoji={boolean('emoji', false)}
uploads={boolean('uploads', false)}
typingUsers={object('typingUsers', [])}
loading={boolean('loading', false)}
notificationsEnabled={boolean('notificationsEnabled', true)}
minimized={boolean('minimized', false)}
windowed={boolean('windowed', false)}
onEnableNotifications={action('enableNotifications')}
onDisableNotifications={action('disableNotifications')}
onMinimize={action('minimize')}
onRestore={action('restore')}
onOpenWindow={action('openWindow')}
onTop={action('top')}
onBottom={action('bottom')}
onUpload={action('upload')}
onSubmit={action('submit')}
/>
))
.add('with typing user', () => (
<Chat
color={color('color', '#175CC4')}
title={text('title', '')}
agent={object('agent', {
name: 'Guilherme Gazzo',
status: 'online',
email: 'guilherme.gazzo@rocket.chat',
username: '@guilherme.gazzo',
avatarSrc: '//gravatar.com/avatar/7ba3fcdd590033117b1e6587e0d20478?s=32',
})}
sound={{
src: soundSrc,
play: false,
}}
user={{ _id: 1 }}
messages={data}
loading={boolean('loading', true)}
uploads={boolean('uploads', true)}
emoji={boolean('emoji', true)}
title={text('text', 'guilherme.gazzo')}
emoji={boolean('emoji', false)}
uploads={boolean('uploads', false)}
typingUsers={object('typingUsers', ['user'])}
loading={boolean('loading', false)}
notificationsEnabled={boolean('notificationsEnabled', true)}
minimized={boolean('minimized', false)}
windowed={boolean('windowed', false)}
onEnableNotifications={action('enableNotifications')}
onDisableNotifications={action('disableNotifications')}
onMinimize={action('minimize')}
onRestore={action('restore')}
onOpenWindow={action('openWindow')}
onTop={action('top')}
onBottom={action('bottom')}
onUpload={action('upload')}
onSubmit={action('submit')}
/>
));
))
;
@import '~styles/colors';
@import '~styles/variables';
.main {
.chat {
&__messages {
display: flex;
position: relative;
overflow: hidden;
width: 100%;
&::before {
content:"";
transition: all .3s;
width: 100%;
position: absolute;
bottom: -4px;
box-shadow: 0 0 1px 1px #ccc;
border-radius: 50%;
height: 4px;
z-index: 1;
}
&--atBottom::before {
box-shadow: 0 0 1px 1px transparent;
}
&--loading::after {
position: absolute;
left: 0;
right: 0;
content: "";
height: 20px;
width: 20px;
border-radius: 50%;
margin: $default-padding auto;
border: 4px solid $color-dark-blue;
border-color: $color-dark-blue transparent transparent transparent;
display: block;
animation: loader-rotate 1s linear infinite;
}
}
&__wrapper {
flex: 1 1 auto;
padding: 0 16px;
overflow-y: auto;
}
&::before {
content:"";
transition: all .3s;
width: 100%;
position: absolute;
bottom: -4px;
box-shadow: 0 0 1px 1px #ccc;
border-radius: 50%;
height: 4px;
z-index: 1;
}
&--atBottom::before {
box-shadow: 0 0 1px 1px transparent;
}
&--loading::after {
position: absolute;
left: 0;
right: 0;
content: "";
height: 20px;
width: 20px;
border-radius: 50%;
margin: $default-padding auto;
border: 4px solid $color-dark-blue;
border-color: $color-dark-blue transparent transparent transparent;
display: block;
animation: loader-rotate 1s linear infinite;
}
}
.container {
flex: 1 1;
display: flex;
flex-direction: column;