Commit e6c1a79c authored by Guilherme Gazzo's avatar Guilherme Gazzo Committed by GitHub

[NEW] SDK and Streams (#45)

parent 39f1c7e6
const webpack = require('webpack');
const webpackOverride = require('../webpackOverride.config');
module.exports = (baseConfig, env, defaultConfig) => {
// we are extending the base alias config here, adding preact as an alias
defaultConfig = webpackOverride(defaultConfig, env);
......@@ -10,9 +9,8 @@ module.exports = (baseConfig, env, defaultConfig) => {
defaultConfig.resolve.extensions.push('.css');
defaultConfig.resolve.extensions.push('.svg');
defaultConfig.resolve.extensions.push('.scss');
defaultConfig.resolve.extensions.push('.svg');
// defaultConfig.resolve.extensions.push('.sass');
// defaultConfig.resolve.extensions.push('.sass');
// adding new plugins to the default config.
defaultConfig.plugins.push(
new webpack.ProvidePlugin({
......@@ -21,8 +19,7 @@ module.exports = (baseConfig, env, defaultConfig) => {
I18n: ['autoI18n', 'default']
})
);
defaultConfig.module.rules[2] = ({
defaultConfig.module.rules.push({
test: /\.(s?css|sass)$/,
use: [
{
......@@ -45,6 +42,7 @@ module.exports = (baseConfig, env, defaultConfig) => {
})
const { include, exclude, test, ...loader } = defaultConfig.module.rules[0];
defaultConfig.module.rules[0] = {
test,
include,
......@@ -63,11 +61,8 @@ module.exports = (baseConfig, env, defaultConfig) => {
enforce: 'pre',
});
defaultConfig.module.rules[4].test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/;
defaultConfig.module.rules.push({
test: /\.svg$/,
loader: 'desvg-loader/preact!svg-loader'
});
// defaultConfig.module.rules[4].test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/;
return defaultConfig;
};
......@@ -3,9 +3,13 @@ const webpackOverride = require('./webpackOverride.config');
const path = require('path');
export default (config, env, helpers) => {
// config.mode = 'production';
// Use Preact CLI's helpers object to get the babel-loader
const babel = helpers.getLoadersByName(config, 'babel-loader')[0].rule;
// Update the loader config to include preact-i18nline
// babel.options.presets[0][1].exclude.push('transform-async-to-generator');
// // Add fast-async
// babel.options.plugins.push([require.resolve('fast-async'), { spec: true }]);
babel.loader = [
{ // create an entry for the old loader
loader: babel.loader,
......@@ -18,26 +22,44 @@ export default (config, env, helpers) => {
// remove the old loader options
delete babel.options;
config.module.loaders[8].test = /\.(woff2?|ttf|eot|jpe?g|png|gif|mp4|mov|ogg|webm)(\?.*)?$/i;
config.module.loaders.push({
test: /\.svg$/,
loader: 'desvg-loader/preact!svg-loader',
});
config.plugins.push(
new webpack.ProvidePlugin({
I18n: ['autoI18n', 'default'],
})
);
config = webpackOverride(config);
config.resolve.alias = Object.assign({}, config.resolve.alias, {
react: 'preact-compat',
'react-dom': 'preact-compat',
styles: path.join(__dirname, './src/styles'),
icons: path.join(__dirname, './src/icons'),
components: path.join(__dirname, './src/components'),
autoI18n: path.resolve(__dirname, './src/i18n'),
});
// const { index } = helpers.getPluginsByName(config, 'UglifyJsPlugin')[0];
// config.plugins.splice(index, 1);
return config;
config.plugins[1].definitions['process.env'] = {};
config.optimization = {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendor: {
// name of the chunk
name: 'vendor',
// async + async chunks
chunks: 'all',
// import file path containing node_modules
test: /node_modules/,
// priority
priority: 20,
},
common: {
name: 'common',
minChunks: 2,
chunks: 'async',
priority: 10,
reuseExistingChunk: true,
enforce: true,
},
},
},
};
return webpackOverride(config);
};
......@@ -77,7 +77,6 @@ $padding: 20px;
width: 100%;
transition: all $default-time-animation;
}
// COLORS
&--danger {
@include state($color-dark-red);
......@@ -107,7 +106,6 @@ $padding: 20px;
& .button__inner {
padding-right: 24px;
}
&.button--stack .button__inner {
padding-left: 24px;
}
......
......@@ -32,6 +32,21 @@ export default class Composer extends Component {
event.preventDefault();
}
}
onPaste(e) {
if (e.clipboardData == null || !e.clipboardData.items) {
return;
}
const items = Array.from(e.clipboardData.items);
const files = items
.filter((item) => (item.kind === 'file' && item.type.indexOf('image/') !== -1))
.map((item) => ({
file: item.getAsFile(),
name: 'Clipboard',
}));
this.props.onUpload(files);
e.preventDefault();
}
input(event) {
// const { inputType, data } = event;
// if (inputType === 'insertParagraph' || (inputType === 'insertText' && data === null)) {
......@@ -44,12 +59,15 @@ export default class Composer extends Component {
this.bind = this.bind.bind(this);
this.input = this.input.bind(this);
this.onKeypress = this.onKeypress.bind(this);
this.onPaste = this.onPaste.bind(this);
}
render({ pre, post, placeholder, ...args }) {
return (
<div {...args} className={createClassName(styles, 'composer', {})}>
{pre}
<div ref={this.bind} onKeypress={this.onKeypress} onInput={this.input} placeholder={placeholder} className={createClassName(styles, 'composer__input', {})} contenteditable />
<div ref={this.bind} onPaste={this.onPaste} onKeypress={this.onKeypress} onInput={this.input} placeholder={placeholder} className={createClassName(styles, 'composer__input', {})}
contenteditable
/>
{post}
</div>);
}
......
.container,
.footer {
width: 100%;
color: #9EA2A8;
padding: 4px 8px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
flex: 0 0 auto;
flex-direction: column;
}
.logo {
margin: 0 5px;
vertical-align: middle;
}
.powered {
font-size: 10px;
font-weight: 500;
flex: 1 1 100%;
text-align: end;
width: 100%;
margin: 0;
align-self: flex-end;
}
.container nav {
float: right;
font-size: 100%;
}
.container nav a {
display: inline-block;
height: 56px;
line-height: 56px;
padding: 0 15px;
min-width: 50px;
text-align: center;
background: rgba(255,255,255,0);
text-decoration: none;
color: #FFF;
will-change: background-color;
}
.container nav a:hover,
.container nav a:active {
background: rgba(0,0,0,0.2);
}
.container nav a.active {
background: rgba(0,0,0,0.4);
}
@import '~styles/variables';
.container,
.footer {
width: 100%;
color: #9EA2A8;
padding: 4px 8px;
display: flex;
justify-content: space-between;
align-items: stretch;
justify-content: space-between;
align-items: center;
font-size: 10px;
flex: 0 0 auto;
flex-direction: column;
}
&__content {
padding: 4px 8px;
display: flex;
}
&__options {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: #9EA2A8;
font-size: 10px;
font-weight: bold;
text-align: left;
letter-spacing: 0.2px;
line-height: 16px;
transition: trasform $default-time-animation;
user-select: none;
&:active {
opacity: .9;
transform: translateY(2px);
}
}
.logo {
margin: 0 5px;
vertical-align: middle;
}
.powered-by {
.powered {
font-size: 10px;
font-weight: 500;
flex: 1 1 100%;
text-align: end;
width: 100%;
margin: 0;
align-self: flex-end;
user-select: none;
}
.container nav {
float: right;
font-size: 100%;
}
.container nav a {
display: inline-block;
height: 56px;
line-height: 56px;
padding: 0 15px;
min-width: 50px;
text-align: center;
background: rgba(255,255,255,0);
text-decoration: none;
color: #FFF;
will-change: background-color;
}
.container nav a:hover,
.container nav a:active {
background: rgba(0,0,0,0.2);
}
&__logo {
margin: 0 5px;
vertical-align: middle;
}
.container nav a.active {
background: rgba(0,0,0,0.4);
}
......@@ -9,16 +9,28 @@ import Avatar from 'components/Avatar';
const parseDate = (ts) => format(ts, isToday(ts) ? 'HH:mm' : 'dddd HH:mm');
const Message = ({ _id, Element = 'div', msg, ts, me, ...args }) => (
<Element id={_id} className={createClassName(styles, 'message', { me })}
{...args}
>
<div className={createClassName(styles, 'message__container', {})}>
export const Container = ({ children, ...args }) => (<div {...args} className={createClassName(styles, 'message__container', {})}>{children}</div>);
export const Text = ({ children, me, ...args }) => (<div {...args} className={createClassName(styles, 'message__text', { me })}>{children}</div>);
export const Content = ({ children, me, ...args }) => (<div {...args} className={createClassName(styles, 'message__content', { me })} >{children}</div>);
export const Body = ({ me, children, Element = 'div', ...args }) => (<Element className={createClassName(styles, 'message', { me })} {...args}>
{children}
</Element>);
const Message = ({ _id, el, msg, ts, me, ...args }) => (
<Body id={_id} me={me} Element={el} {...args}>
<Container>
{!me && <Avatar />}
<div className={createClassName(styles, 'message__content', { me })} ><div className={createClassName(styles, 'message__text', { me })} dangerouslySetInnerHTML={{ __html: md.render(msg) }} /> <div className={createClassName(styles, 'message__time', {})}>{parseDate(ts)}</div></div>
<Content me={me}>
<Text me={me} dangerouslySetInnerHTML={{ __html: md.render(msg) }} />
<div className={createClassName(styles, 'message__time', {})}>{parseDate(ts)}</div>
</Content>
{me && <Avatar />}
</div>
</Element>
</Container>
</Body>
);
export default Message;
......@@ -2,14 +2,15 @@ import { h } from 'preact';
import styles from './styles';
import { createClassName } from '../helpers';
import Avatar from '../Avatar';
import * as Message from '../Message';
export const TypingAvatar = ({ avatars }) =>
(<div className={createClassName(styles, 'avatar-container')}>{
avatars.map((src) => <Avatar src={src} className={[styles.avatar]} />)
}
</div>);
const TypingIndicator = ({ children }) => (
export const TypingAvatar = ({ users = [] }) => (<div className={createClassName(styles, 'avatar-container')}>{
users.map((src) => <Avatar src={`http://localhost:3000/avatar/${ src }`} className={[styles.avatar]} />)
}
</div>);
export const TypingIndicator = ({ children }) => (
<div aria-label={children} class={createClassName(styles, 'typing-indicator')}>
<span class={createClassName(styles, 'typing-indicator__dot')} />
<span class={createClassName(styles, 'typing-indicator__dot')} />
......@@ -17,4 +18,11 @@ const TypingIndicator = ({ children }) => (
</div>
);
export default TypingIndicator;
const Typing = ({ users, description }) => (<Message.Body>
<Message.Container>
<TypingAvatar users={users} />
<Message.Content><Message.Text><TypingIndicator>{description}</TypingIndicator></Message.Text></Message.Content>
</Message.Container>
</Message.Body>);
export default Typing;
......@@ -2,8 +2,7 @@ import { h } from 'preact';
import centered from '@storybook/addon-centered';
import { storiesOf } from '@storybook/react';
import TypingIndicator, { TypingAvatar } from '.';
import Typing, { TypingAvatar, TypingIndicator } from '.';
import bertieBartonAvatar from '../Avatar/bertieBarton.png';
import avatar1 from './avatar1.png';
......@@ -13,6 +12,14 @@ import avatar3 from './avatar3.png';
storiesOf('Components|TypingIndicator', module)
.addDecorator(centered)
.add('three dots', () => <TypingIndicator>The attendant is typing</TypingIndicator>)
.add('multiple Avatars', () => (<TypingAvatar avatars={[
bertieBartonAvatar, avatar1, avatar2, avatar3]}
/>));
.add('multiple Avatars', () => (
<TypingAvatar avatars={[
bertieBartonAvatar,
avatar1,
avatar2,
avatar3,
]}
/>))
.add('as message', () => (
<Typing users={[bertieBartonAvatar, avatar1, avatar2, avatar3]} description={I18n.t('The attendant is typing')} />
));
......@@ -4,11 +4,12 @@
.avatar-container {
margin: 0 5px;
display: flex;
}
.avatar {
margin: auto -5px;
margin: 0 -5px;
box-shadow: -2px 0 1px 0px #00000082;
&:first-child {
box-shadow: none;
......
import { h, Component } from 'preact';
import { Router } from 'preact-router';
import { api } from '@rocket.chat/sdk/dist/bundle.js';
<<<<<<< HEAD
// Code-splitting is automated for routes
=======
import Header from './Header';
import Footer from './Footer';
>>>>>>> master
import Store, { Consumer } from '../store';
import Home from '../containers/home';
import LeaveMessage from '../containers/leaveamessage';
......@@ -21,6 +25,22 @@ export default class App extends Component {
async componentDidMount() {
// console.log(await api.livechat.config());
}
<<<<<<< HEAD
renderScreen({ user, config, messages }) {
const { settings = {}, online } = config;
if (online) {
if (user.token) {
return <Home {...config} messages={messages} default path="/home" />;
}
return <Register {...config} default path="/register" />;
}
if (settings.displayOfflineForm) {
return <LeaveMessage {...config} default path="/LeaveMessage" />;
}
return <LeaveMessage {...config} default path="/LeaveMessage" />;
=======
renderScreen({ user, config, messages }) {
const { settings = {}, online } = config;
......@@ -35,6 +55,7 @@ export default class App extends Component {
}
return <LeaveMessage {...config} default path="/LeaveMessage" />;
>>>>>>> master
}
render() {
return (
......
import { h } from 'preact';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
<<<<<<< HEAD
import { color, withKnobs } from '@storybook/addon-knobs';
import Header from '.';
=======
import { withKnobs, color, select, text } from '@storybook/addon-knobs';
import Header, { Picture, Content, SubTitle, Title, Actions, Action } from '.';
>>>>>>> master
import Avatar from '../Avatar';
import StatusIndicator, { statuses } from '../StatusIndicator';
import Bell from 'icons/bell.svg';
import Arrow from 'icons/arrow.svg';
import NewWindow from 'icons/newWindow.svg';
import bertieBartonAvatar from '../Avatar/bertieBarton.png';
import centered from '@storybook/addon-centered';
storiesOf('Components|Header', module)
......
......@@ -33,3 +33,40 @@ export async function asyncEvery(array, callback) {
}
return true;
}
export const debounce = (func, delay) => {
let inDebounce;
return function(...args) {
const context = this;
clearTimeout(inDebounce);
inDebounce = setTimeout(() => func.apply(context, args), delay);
};
};
export const throttle = (func, limit) => {
let inThrottle;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
};
export function sort(array, value) {
let min = 0;
let max = array.length - 1;
while (min <= max) {
const guess = Math.floor((min + max) / 2);
const { ts } = array[guess];
if (ts < value) { min = guess + 1; } else
if (ts > array[guess + 1]) { return guess; } else { max = guess - 1; }
}
return array.length > 0 ? array.length : 0;
}
export const insert = (array, el) => (array.splice(sort(array, el.ts), 0, el), array);
import { h, Component } from 'preact';
import { api } from '/Users/guilhermegazzo/Rocket.Chat.js.SDK/dist/';
const { livechat } = api;
import { Consumer, getState } from '../store';
import Home from '../routes/home';
let rid = '';
class Wrapped extends Component {
async sendMessage(msg) {
const state = getState();
const { token } = state.user;
const { user: { token } } = state;
if (!rid) {
const { room } = await fetch(`http://localhost:3000/api/v1/livechat/room?token=${ token }`).then((res) => res.json());
const { room } = await livechat.room({ token });
rid = room._id;
this.actions({ room });
}
await livechat.sendMessage({ msg, token, rid });
}
const { messages } = state;
const { message } = await fetch(`http://localhost:3000/api/v1/livechat/message?token=${ token }&rid=${ rid }`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ msg, token, rid }),
}).then((res) => res.json());
this.actions({ messages: [...messages, message].filter((e) => e) });
async onTop() {
if (this.state.ended) {
return;
}
const state = getState();
const { user: { token }, messages } = state;
this.setState({ loading: true });
const { messages: moreMessages } = await livechat.loadMessages(rid, { token, limit: messages.length + 10 });
this.setState({ loading: false, ended: messages.length + 10 >= moreMessages.length });
this.actions({ messages: (moreMessages || []).reverse() });
}
constructor() {
super();
this.state = {
loading: true,
loading: false,
};
const state = getState();
rid = state.room && state.room._id;
this.sendMessage = this.sendMessage.bind(this);
this.onTop = this.onTop.bind(this);
}
async componentDidMount() {
const state = getState();
const { token } = state.user;
if (rid) {
const { messages } = await fetch(`http://localhost:3000/api/v1/livechat/messages.history/${ rid }?token=${ token }`).then((res) => res.json());
this.actions({ messages: messages.reverse() });
this.setState({ loading: true });
const { messages } = await livechat.loadMessages(rid, { token });
this.setState({ loading: false });
this.actions({ messages: (messages || []).reverse() });
}
}
render(props, { loading }) {
onUpload(files) {
console.log(files);
}
render(props) {
return (
<Consumer>
{
({ user, dispatch, config: { theme, settings, agent }, messages }) => {
({ typing, user, dispatch, config: { theme, settings, agent }, messages }) => {
this.actions = dispatch;
return (
<Home
{...props}
onTop={this.onTop}
user={user}
loading={loading}
typingUsers={typing}
loading={this.state.loading}
onSubmit={this.sendMessage}
color={theme.color}
onUpload={this.onUpload}
messages={messages}
uploads={settings.fileUpload}
title={agent.username}
title={agent && agent.username}
/>);
}}
</Consumer>);
......
import { h } from 'preact';
import { h, Component } from 'preact';
import Leaveamessage from '../routes/leaveamessage';
import { api } from '@rocket.chat/sdk/dist/bundle';
const { livechat } = api;