Add new connect form, closes #7
This commit is contained in:
parent
f502fea5c1
commit
6fd5235ec9
File diff suppressed because one or more lines are too long
@ -32,7 +32,7 @@ function brotli(opts) {
|
|||||||
|
|
||||||
function clean() {
|
function clean() {
|
||||||
return del(['dist']);
|
return del(['dist']);
|
||||||
};
|
}
|
||||||
|
|
||||||
function js(cb) {
|
function js(cb) {
|
||||||
var config = require('./webpack.config.prod.js');
|
var config = require('./webpack.config.prod.js');
|
||||||
@ -131,7 +131,10 @@ const assets = gulp.parallel(js, config, fonts, compressTTF);
|
|||||||
|
|
||||||
const build = gulp.series(clean, assets, compress, cleanup, bindata);
|
const build = gulp.series(clean, assets, compress, cleanup, bindata);
|
||||||
|
|
||||||
const dev = gulp.series(clean, gulp.parallel(serve, fonts, gulp.series(config, bindata)));
|
const dev = gulp.series(
|
||||||
|
clean,
|
||||||
|
gulp.parallel(serve, fonts, gulp.series(config, bindata))
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task('build', build);
|
gulp.task('build', build);
|
||||||
gulp.task('default', dev);
|
gulp.task('default', dev);
|
||||||
|
@ -53,6 +53,62 @@ i[class*=' icon-']:before {
|
|||||||
color: #f6546a !important;
|
color: #f6546a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textinput {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textinput input {
|
||||||
|
padding: 17.5px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textinput input:focus,
|
||||||
|
.textinput input.value {
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textinput span {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateZ(0);
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textinput-1 {
|
||||||
|
font: 12px 'Montserrat', sans-serif;
|
||||||
|
margin: 15px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textinput input:focus + .textinput-1,
|
||||||
|
.textinput-1.value {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textinput-2 {
|
||||||
|
margin: 22.5px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textinput input:focus + .textinput-1 + .textinput-2,
|
||||||
|
.textinput-2.value {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
background: #f6546a;
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -214,9 +270,10 @@ i[class*=' icon-']:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connect-form {
|
.connect-form {
|
||||||
margin: auto 0;
|
margin: auto 20px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
width: 300px;
|
width: 350px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-form h1 {
|
.connect-form h1 {
|
||||||
@ -235,9 +292,7 @@ i[class*=' icon-']:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connect-form input {
|
.connect-form input {
|
||||||
display: block;
|
margin-top: 5px;
|
||||||
margin: 5px 0px;
|
|
||||||
padding: 15px;
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,6 +304,7 @@ i[class*=' icon-']:before {
|
|||||||
|
|
||||||
.connect-form input[type='submit'] {
|
.connect-form input[type='submit'] {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
margin-top: 0;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-family: Montserrat, sans-serif;
|
font-family: Montserrat, sans-serif;
|
||||||
background: #6bb758;
|
background: #6bb758;
|
||||||
@ -264,28 +320,47 @@ i[class*=' icon-']:before {
|
|||||||
background: #6bb758;
|
background: #6bb758;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-form input[type='checkbox'] {
|
.connect-form-address {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
margin-right: 5px;
|
}
|
||||||
vertical-align: middle;
|
|
||||||
|
.connect-form-address .textinput:nth-child(1) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-form-address .textinput:nth-child(2) {
|
||||||
|
width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-form-address input {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-form-address label {
|
||||||
|
margin-top: 5px;
|
||||||
|
font: 12px 'Montserrat', sans-serif;
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-form-address input[type='checkbox'] {
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-form i {
|
.connect-form i {
|
||||||
float: right;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #999;
|
color: #999;
|
||||||
padding: 10px 5px;
|
text-align: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-form i:hover {
|
.connect-form i:hover {
|
||||||
color: #000;
|
color: #666;
|
||||||
}
|
|
||||||
|
|
||||||
.connect-form label {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 0;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-title-bar {
|
.chat-title-bar {
|
||||||
|
@ -1,144 +1,94 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { Form, withFormik } from 'formik';
|
||||||
import Navicon from 'containers/Navicon';
|
import Navicon from 'containers/Navicon';
|
||||||
|
import Checkbox from 'components/ui/Checkbox';
|
||||||
|
import TextInput from 'components/ui/TextInput';
|
||||||
|
import { isValidNick, isValidChannel, isValidUsername } from 'utils';
|
||||||
|
|
||||||
const getSortedDefaultChannels = createSelector(
|
const getSortedDefaultChannels = createSelector(
|
||||||
defaults => defaults.channels,
|
defaults => defaults.channels,
|
||||||
channels => channels.concat().sort()
|
channels => channels.split(',').sort()
|
||||||
);
|
);
|
||||||
|
|
||||||
export default class Connect extends Component {
|
const Error = ({ children }) =>
|
||||||
|
children ? <div className="form-error">{children}</div> : null;
|
||||||
|
|
||||||
|
class Connect extends Component {
|
||||||
state = {
|
state = {
|
||||||
showOptionals: false,
|
showOptionals: false
|
||||||
passwordTouched: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = e => {
|
handleSSLChange = e => {
|
||||||
const { connect, select, join, defaults } = this.props;
|
const { values, setFieldValue } = this.props;
|
||||||
|
if (e.target.checked && values.port === '6667') {
|
||||||
e.preventDefault();
|
setFieldValue('port', '6697', false);
|
||||||
|
} else if (!e.target.checked && values.port === '6697') {
|
||||||
const nick = e.target.nick.value.trim();
|
setFieldValue('port', '6667', false);
|
||||||
let { address, channels } = defaults;
|
|
||||||
const opts = {
|
|
||||||
name: defaults.name
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!defaults.readonly) {
|
|
||||||
address = e.target.address.value.trim();
|
|
||||||
channels = e.target.channels.value
|
|
||||||
.split(',')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(s => s);
|
|
||||||
opts.name = e.target.name.value.trim();
|
|
||||||
opts.tls = e.target.ssl.checked;
|
|
||||||
|
|
||||||
if (this.state.showOptionals) {
|
|
||||||
opts.realname = e.target.realname.value.trim();
|
|
||||||
opts.username = e.target.username.value.trim();
|
|
||||||
|
|
||||||
if (this.state.passwordTouched) {
|
|
||||||
opts.password = e.target.password.value.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address.indexOf('.') > 0 && nick) {
|
|
||||||
connect(address, nick, opts);
|
|
||||||
|
|
||||||
const i = address.indexOf(':');
|
|
||||||
if (i > 0) {
|
|
||||||
address = address.slice(0, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
select(address);
|
|
||||||
|
|
||||||
if (channels.length > 0) {
|
|
||||||
join(channels, address);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setFieldValue('ssl', e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleShowClick = () => {
|
handleShowClick = () => {
|
||||||
this.setState({ showOptionals: !this.state.showOptionals });
|
this.setState({ showOptionals: !this.state.showOptionals });
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePasswordChange = () => {
|
renderOptionals = () => {
|
||||||
this.setState({ passwordTouched: true });
|
const { errors, touched } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TextInput name="username" placeholder="Username" />
|
||||||
|
{touched.username && <Error>{errors.username}</Error>}
|
||||||
|
<TextInput type="password" name="password" placeholder="Password" />
|
||||||
|
<TextInput name="realname" placeholder="Realname" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { defaults } = this.props;
|
const { defaults, values, errors, touched } = this.props;
|
||||||
let optionals = null;
|
const { readonly, showDetails } = defaults;
|
||||||
|
|
||||||
if (this.state.showOptionals) {
|
|
||||||
optionals = (
|
|
||||||
<div>
|
|
||||||
<input name="username" type="text" placeholder="Username" />
|
|
||||||
<input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
defaultValue={defaults.password ? ' ' : null}
|
|
||||||
onChange={this.handlePasswordChange}
|
|
||||||
/>
|
|
||||||
<input name="realname" type="text" placeholder="Realname" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let form;
|
let form;
|
||||||
|
|
||||||
if (defaults.readonly) {
|
if (readonly) {
|
||||||
form = (
|
form = (
|
||||||
<form className="connect-form" onSubmit={this.handleSubmit}>
|
<Form className="connect-form">
|
||||||
<h1>Connect</h1>
|
<h1>Connect</h1>
|
||||||
{defaults.showDetails && (
|
{showDetails && (
|
||||||
<div className="connect-details">
|
<div className="connect-details">
|
||||||
<h2>{defaults.address}</h2>
|
<h2>
|
||||||
{getSortedDefaultChannels(defaults).map(channel => (
|
{values.host}:{values.port}
|
||||||
|
</h2>
|
||||||
|
{getSortedDefaultChannels(values).map(channel => (
|
||||||
<p>{channel}</p>
|
<p>{channel}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<input name="nick" type="text" placeholder="Nick" />
|
<TextInput name="nick" placeholder="Nick" />
|
||||||
|
{touched.nick && <Error>{errors.nick}</Error>}
|
||||||
<input type="submit" value="Connect" />
|
<input type="submit" value="Connect" />
|
||||||
</form>
|
</Form>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
form = (
|
form = (
|
||||||
<form className="connect-form" onSubmit={this.handleSubmit}>
|
<Form className="connect-form">
|
||||||
<h1>Connect</h1>
|
<h1>Connect</h1>
|
||||||
<input
|
<TextInput name="name" placeholder="Name" />
|
||||||
name="name"
|
<div className="connect-form-address">
|
||||||
type="text"
|
<TextInput name="host" placeholder="Host" />
|
||||||
placeholder="Name"
|
<TextInput name="port" placeholder="Port" />
|
||||||
defaultValue={defaults.name}
|
<Checkbox name="ssl" label="SSL" onChange={this.handleSSLChange} />
|
||||||
/>
|
</div>
|
||||||
<input
|
{touched.host && <Error>{errors.host}</Error>}
|
||||||
name="address"
|
{touched.port && <Error>{errors.port}</Error>}
|
||||||
type="text"
|
<TextInput name="nick" placeholder="Nick" />
|
||||||
placeholder="Address"
|
{touched.nick && <Error>{errors.nick}</Error>}
|
||||||
defaultValue={defaults.address}
|
<TextInput name="channels" placeholder="Channels" />
|
||||||
/>
|
{touched.channels && <Error>{errors.channels}</Error>}
|
||||||
<input name="nick" type="text" placeholder="Nick" />
|
{this.state.showOptionals && this.renderOptionals()}
|
||||||
<input
|
<i className="icon-ellipsis" onClick={this.handleShowClick} />
|
||||||
name="channels"
|
|
||||||
type="text"
|
|
||||||
placeholder="Channels"
|
|
||||||
defaultValue={
|
|
||||||
defaults.channels ? defaults.channels.join(',') : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{optionals}
|
|
||||||
<p>
|
|
||||||
<label htmlFor="ssl">
|
|
||||||
<input name="ssl" type="checkbox" defaultChecked={defaults.ssl} />SSL
|
|
||||||
</label>
|
|
||||||
<i className="icon-ellipsis" onClick={this.handleShowClick} />
|
|
||||||
</p>
|
|
||||||
<input type="submit" value="Connect" />
|
<input type="submit" value="Connect" />
|
||||||
</form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,3 +100,80 @@ export default class Connect extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withFormik({
|
||||||
|
mapPropsToValues: ({ defaults }) => ({
|
||||||
|
name: defaults.name,
|
||||||
|
host: defaults.host,
|
||||||
|
port: defaults.port || defaults.ssl ? '6697' : '6667',
|
||||||
|
nick: '',
|
||||||
|
channels: defaults.channels.join(','),
|
||||||
|
username: '',
|
||||||
|
password: defaults.password ? ' ' : '',
|
||||||
|
realname: '',
|
||||||
|
ssl: defaults.ssl
|
||||||
|
}),
|
||||||
|
validate: values => {
|
||||||
|
Object.keys(values).forEach(k => {
|
||||||
|
if (typeof values[k] === 'string') {
|
||||||
|
values[k] = values[k].trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
|
if (!values.host) {
|
||||||
|
errors.host = 'Host is required';
|
||||||
|
} else if (values.host.indexOf('.') < 1) {
|
||||||
|
errors.host = 'Invalid host';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.port) {
|
||||||
|
values.port = values.ssl ? '6697' : '6667';
|
||||||
|
} else if (values.port < 1 || values.port > 65535) {
|
||||||
|
errors.port = 'Invalid port';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.nick) {
|
||||||
|
errors.nick = 'Nick is required';
|
||||||
|
} else if (!isValidNick(values.nick)) {
|
||||||
|
errors.nick = 'Invalid nick';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.username && !isValidUsername(values.username)) {
|
||||||
|
errors.username = 'Invalid username';
|
||||||
|
}
|
||||||
|
|
||||||
|
values.channels = values.channels
|
||||||
|
.split(',')
|
||||||
|
.map(channel => {
|
||||||
|
channel = channel.trim();
|
||||||
|
if (channel) {
|
||||||
|
if (isValidChannel(channel, false)) {
|
||||||
|
if (channel[0] !== '#') {
|
||||||
|
channel = `#${channel}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.channels = 'Invalid channel(s)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channel;
|
||||||
|
})
|
||||||
|
.filter(s => s)
|
||||||
|
.join(',');
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
handleSubmit: (values, { props }) => {
|
||||||
|
const { connect, select, join } = props;
|
||||||
|
const channels = values.channels.split(',');
|
||||||
|
delete values.channels;
|
||||||
|
|
||||||
|
connect(values);
|
||||||
|
select(values.host);
|
||||||
|
|
||||||
|
if (channels.length > 0) {
|
||||||
|
join(channels, values.host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(Connect);
|
||||||
|
29
client/src/js/components/ui/Checkbox.js
Normal file
29
client/src/js/components/ui/Checkbox.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Field } from 'formik';
|
||||||
|
|
||||||
|
const Checkbox = ({ name, label, onChange, ...props }) => (
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
render={({ field, form }) => (
|
||||||
|
<label htmlFor={name}>
|
||||||
|
{label && <div>{label}</div>}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
checked={field.value}
|
||||||
|
onChange={e => {
|
||||||
|
form.setFieldTouched(name, true);
|
||||||
|
field.onChange(e);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Checkbox;
|
27
client/src/js/components/ui/TextInput.js
Normal file
27
client/src/js/components/ui/TextInput.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Field } from 'formik';
|
||||||
|
|
||||||
|
const TextInput = ({ name, placeholder, ...props }) => (
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="textinput">
|
||||||
|
<input
|
||||||
|
className={field.value ? 'value' : null}
|
||||||
|
type="text"
|
||||||
|
name={name}
|
||||||
|
{...field}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<span className={field.value ? 'textinput-1 value' : 'textinput-1'}>
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
<span className={field.value ? 'textinput-2 value' : 'textinput-2'}>
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TextInput;
|
@ -1,27 +1,5 @@
|
|||||||
import { connect, setServerName } from '../servers';
|
import { connect, setServerName } from '../servers';
|
||||||
|
|
||||||
describe('connect()', () => {
|
|
||||||
it('sets host and port correctly', () => {
|
|
||||||
expect(connect('cake.com:1881', '', {})).toMatchObject({
|
|
||||||
socket: {
|
|
||||||
data: {
|
|
||||||
host: 'cake.com',
|
|
||||||
port: '1881'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(connect('cake.com', '', {})).toMatchObject({
|
|
||||||
socket: {
|
|
||||||
data: {
|
|
||||||
host: 'cake.com',
|
|
||||||
port: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setServerName()', () => {
|
describe('setServerName()', () => {
|
||||||
it('passes valid names to the server', () => {
|
it('passes valid names to the server', () => {
|
||||||
const name = 'cake';
|
const name = 'cake';
|
||||||
|
@ -232,7 +232,10 @@ describe('channel reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('optimistically adds the server on CONNECT', () => {
|
it('optimistically adds the server on CONNECT', () => {
|
||||||
const state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
const state = reducer(
|
||||||
|
undefined,
|
||||||
|
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||||
|
);
|
||||||
|
|
||||||
expect(state).toEqual({
|
expect(state).toEqual({
|
||||||
'127.0.0.1': {}
|
'127.0.0.1': {}
|
||||||
|
@ -3,7 +3,10 @@ import * as actions from '../actions';
|
|||||||
|
|
||||||
describe('server reducer', () => {
|
describe('server reducer', () => {
|
||||||
it('adds the server on CONNECT', () => {
|
it('adds the server on CONNECT', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(
|
||||||
|
undefined,
|
||||||
|
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||||
|
);
|
||||||
|
|
||||||
expect(state).toEqual({
|
expect(state).toEqual({
|
||||||
'127.0.0.1': {
|
'127.0.0.1': {
|
||||||
@ -17,7 +20,7 @@ describe('server reducer', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
state = reducer(state, connect('127.0.0.1:1337', 'nick', {}));
|
state = reducer(state, connect({ host: '127.0.0.1', nick: 'nick' }));
|
||||||
|
|
||||||
expect(state).toEqual({
|
expect(state).toEqual({
|
||||||
'127.0.0.1': {
|
'127.0.0.1': {
|
||||||
@ -33,9 +36,7 @@ describe('server reducer', () => {
|
|||||||
|
|
||||||
state = reducer(
|
state = reducer(
|
||||||
state,
|
state,
|
||||||
connect('127.0.0.2:1337', 'nick', {
|
connect({ host: '127.0.0.2', nick: 'nick', name: 'srv' })
|
||||||
name: 'srv'
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(state).toEqual({
|
expect(state).toEqual({
|
||||||
@ -93,7 +94,10 @@ describe('server reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets editedNick when editing the nick', () => {
|
it('sets editedNick when editing the nick', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(
|
||||||
|
undefined,
|
||||||
|
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||||
|
);
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SET_NICK,
|
type: actions.SET_NICK,
|
||||||
server: '127.0.0.1',
|
server: '127.0.0.1',
|
||||||
@ -111,7 +115,10 @@ describe('server reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('clears editedNick when receiving an empty nick after editing finishes', () => {
|
it('clears editedNick when receiving an empty nick after editing finishes', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(
|
||||||
|
undefined,
|
||||||
|
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||||
|
);
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SET_NICK,
|
type: actions.SET_NICK,
|
||||||
server: '127.0.0.1',
|
server: '127.0.0.1',
|
||||||
@ -134,7 +141,10 @@ describe('server reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates the nick on SOCKET_NICK', () => {
|
it('updates the nick on SOCKET_NICK', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(
|
||||||
|
undefined,
|
||||||
|
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||||
|
);
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.socket.NICK,
|
type: actions.socket.NICK,
|
||||||
server: '127.0.0.1',
|
server: '127.0.0.1',
|
||||||
@ -152,7 +162,10 @@ describe('server reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('clears editedNick on SOCKET_NICK_FAIL', () => {
|
it('clears editedNick on SOCKET_NICK_FAIL', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(
|
||||||
|
undefined,
|
||||||
|
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||||
|
);
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SET_NICK,
|
type: actions.SET_NICK,
|
||||||
server: '127.0.0.1',
|
server: '127.0.0.1',
|
||||||
@ -200,6 +213,7 @@ describe('server reducer', () => {
|
|||||||
'127.0.0.1': {
|
'127.0.0.1': {
|
||||||
name: 'stuff',
|
name: 'stuff',
|
||||||
nick: 'nick',
|
nick: 'nick',
|
||||||
|
editedNick: null,
|
||||||
status: {
|
status: {
|
||||||
connected: true
|
connected: true
|
||||||
}
|
}
|
||||||
@ -207,6 +221,7 @@ describe('server reducer', () => {
|
|||||||
'127.0.0.2': {
|
'127.0.0.2': {
|
||||||
name: 'stuffz',
|
name: 'stuffz',
|
||||||
nick: 'nick2',
|
nick: 'nick2',
|
||||||
|
editedNick: null,
|
||||||
status: {
|
status: {
|
||||||
connected: false
|
connected: false
|
||||||
}
|
}
|
||||||
@ -215,7 +230,10 @@ describe('server reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates connection status on SOCKET_CONNECTION_UPDATE', () => {
|
it('updates connection status on SOCKET_CONNECTION_UPDATE', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(
|
||||||
|
undefined,
|
||||||
|
connect({ host: '127.0.0.1', nick: 'nick' })
|
||||||
|
);
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.socket.CONNECTION_UPDATE,
|
type: actions.socket.CONNECTION_UPDATE,
|
||||||
server: '127.0.0.1',
|
server: '127.0.0.1',
|
||||||
|
@ -14,7 +14,7 @@ export const getCurrentNick = createSelector(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { editedNick } = servers[tab.server];
|
const { editedNick } = servers[tab.server];
|
||||||
if (!editedNick) {
|
if (editedNick === null) {
|
||||||
return servers[tab.server].nick;
|
return servers[tab.server].nick;
|
||||||
}
|
}
|
||||||
return editedNick;
|
return editedNick;
|
||||||
@ -36,12 +36,12 @@ export const getCurrentServerStatus = createSelector(
|
|||||||
export default createReducer(
|
export default createReducer(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
[actions.CONNECT](state, { host, nick, options }) {
|
[actions.CONNECT](state, { host, nick, name }) {
|
||||||
if (!state[host]) {
|
if (!state[host]) {
|
||||||
state[host] = {
|
state[host] = {
|
||||||
nick,
|
nick,
|
||||||
editedNick: null,
|
editedNick: null,
|
||||||
name: options.name || host,
|
name: name || host,
|
||||||
status: {
|
status: {
|
||||||
connected: false,
|
connected: false,
|
||||||
error: null
|
error: null
|
||||||
@ -82,7 +82,7 @@ export default createReducer(
|
|||||||
[actions.socket.SERVERS](state, { data }) {
|
[actions.socket.SERVERS](state, { data }) {
|
||||||
if (data) {
|
if (data) {
|
||||||
data.forEach(({ host, name, nick, status }) => {
|
data.forEach(({ host, name, nick, status }) => {
|
||||||
state[host] = { name, nick, status };
|
state[host] = { name, nick, status, editedNick: null };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -96,32 +96,13 @@ export default createReducer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export function connect(server, nick, options) {
|
export function connect(config) {
|
||||||
let host = server;
|
|
||||||
let port;
|
|
||||||
const i = server.indexOf(':');
|
|
||||||
if (i > 0) {
|
|
||||||
host = server.slice(0, i);
|
|
||||||
port = server.slice(i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: actions.CONNECT,
|
type: actions.CONNECT,
|
||||||
host,
|
...config,
|
||||||
nick,
|
|
||||||
options,
|
|
||||||
socket: {
|
socket: {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
data: {
|
data: config
|
||||||
host,
|
|
||||||
port,
|
|
||||||
nick,
|
|
||||||
username: options.username || nick,
|
|
||||||
password: options.password,
|
|
||||||
realname: options.realname || nick,
|
|
||||||
tls: options.tls || false,
|
|
||||||
name: options.name || server
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { isChannel } from '..';
|
import { isChannel, isValidNick, isValidChannel, isValidUsername } from '..';
|
||||||
import linkify from '../linkify';
|
import linkify from '../linkify';
|
||||||
|
|
||||||
describe('isChannel()', () => {
|
describe('isChannel()', () => {
|
||||||
@ -14,6 +14,68 @@ describe('isChannel()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isValidNick()', () => {
|
||||||
|
it('validates nicks', () =>
|
||||||
|
Object.entries({
|
||||||
|
bob: true,
|
||||||
|
'bob likes cake': false,
|
||||||
|
'-bob': false,
|
||||||
|
'bob.': false,
|
||||||
|
'bob-': true,
|
||||||
|
'1bob': false,
|
||||||
|
'[bob}': true,
|
||||||
|
'': false,
|
||||||
|
' ': false
|
||||||
|
}).forEach(([input, expected]) =>
|
||||||
|
expect(isValidNick(input)).toBe(expected)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isValidChannel()', () => {
|
||||||
|
it('validates channels', () =>
|
||||||
|
Object.entries({
|
||||||
|
'#chan': true,
|
||||||
|
'#cak e': false,
|
||||||
|
'#cake:': false,
|
||||||
|
'#[cake]': true,
|
||||||
|
'#ca,ke': false,
|
||||||
|
'': false,
|
||||||
|
' ': false,
|
||||||
|
cake: false
|
||||||
|
}).forEach(([input, expected]) =>
|
||||||
|
expect(isValidChannel(input)).toBe(expected)
|
||||||
|
));
|
||||||
|
|
||||||
|
it('handles requirePrefix', () =>
|
||||||
|
Object.entries({
|
||||||
|
chan: true,
|
||||||
|
'cak e': false,
|
||||||
|
'#cake:': false,
|
||||||
|
'#[cake]': true,
|
||||||
|
'#ca,ke': false
|
||||||
|
}).forEach(([input, expected]) =>
|
||||||
|
expect(isValidChannel(input, false)).toBe(expected)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isValidUsername()', () => {
|
||||||
|
it('validates usernames', () =>
|
||||||
|
Object.entries({
|
||||||
|
bob: true,
|
||||||
|
'bob likes cake': false,
|
||||||
|
'-bob': true,
|
||||||
|
'bob.': true,
|
||||||
|
'bob-': true,
|
||||||
|
'1bob': true,
|
||||||
|
'[bob}': true,
|
||||||
|
'': false,
|
||||||
|
' ': false,
|
||||||
|
'b@b': false
|
||||||
|
}).forEach(([input, expected]) =>
|
||||||
|
expect(isValidUsername(input)).toBe(expected)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
describe('linkify()', () => {
|
describe('linkify()', () => {
|
||||||
const proto = href => (href.indexOf('http') !== 0 ? `http://${href}` : href);
|
const proto = href => (href.indexOf('http') !== 0 ? `http://${href}` : href);
|
||||||
const linkTo = href => (
|
const linkTo = href => (
|
||||||
|
@ -2,7 +2,7 @@ if (Object.keys) {
|
|||||||
try {
|
try {
|
||||||
Object.keys('');
|
Object.keys('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Object.keys = function(o, k, r) {
|
Object.keys = function keys(o, k, r) {
|
||||||
r = [];
|
r = [];
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
for (k in o) r.hasOwnProperty.call(o, k) && r.push(k);
|
for (k in o) r.hasOwnProperty.call(o, k) && r.push(k);
|
||||||
|
@ -35,6 +35,93 @@ export function stringifyTab(server, name) {
|
|||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isString(s, maxLength) {
|
||||||
|
if (!s || typeof s !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (maxLength && s.length > maxLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 2812
|
||||||
|
// nickname = ( letter / special ) *( letter / digit / special / "-" )
|
||||||
|
// letter = A-Z / a-z
|
||||||
|
// digit = 0-9
|
||||||
|
// special = "[", "]", "\", "`", "_", "^", "{", "|", "}"
|
||||||
|
export function isValidNick(nick, maxLength = 30) {
|
||||||
|
if (!isString(nick, maxLength)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < nick.length; i++) {
|
||||||
|
const char = nick.charCodeAt(i);
|
||||||
|
if (
|
||||||
|
(i > 0 && char < 45) ||
|
||||||
|
(char > 45 && char < 48) ||
|
||||||
|
(char > 57 && char < 65) ||
|
||||||
|
char > 125
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
} else if ((i === 0 && char < 65) || char > 125) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// chanstring = any octet except NUL, BELL, CR, LF, " ", "," and ":"
|
||||||
|
export function isValidChannel(channel, requirePrefix = true) {
|
||||||
|
if (!isString(channel)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requirePrefix && channel[0] !== '#') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < channel.length; i++) {
|
||||||
|
const char = channel.charCodeAt(i);
|
||||||
|
if (
|
||||||
|
char === 0 ||
|
||||||
|
char === 7 ||
|
||||||
|
char === 10 ||
|
||||||
|
char === 13 ||
|
||||||
|
char === 32 ||
|
||||||
|
char === 44 ||
|
||||||
|
char === 58
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// user = any octet except NUL, CR, LF, " " and "@"
|
||||||
|
export function isValidUsername(username) {
|
||||||
|
if (!isString(username)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < username.length; i++) {
|
||||||
|
const char = username.charCodeAt(i);
|
||||||
|
if (
|
||||||
|
char === 0 ||
|
||||||
|
char === 10 ||
|
||||||
|
char === 13 ||
|
||||||
|
char === 32 ||
|
||||||
|
char === 64
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function timestamp(date = new Date()) {
|
export function timestamp(date = new Date()) {
|
||||||
const h = padStart(date.getHours(), 2, '0');
|
const h = padStart(date.getHours(), 2, '0');
|
||||||
const m = padStart(date.getMinutes(), 2, '0');
|
const m = padStart(date.getMinutes(), 2, '0');
|
||||||
|
@ -4,7 +4,8 @@ verify_certificates = true
|
|||||||
# Defaults for the client connect form
|
# Defaults for the client connect form
|
||||||
[defaults]
|
[defaults]
|
||||||
name = "Freenode"
|
name = "Freenode"
|
||||||
address = "chat.freenode.net"
|
host = "chat.freenode.net"
|
||||||
|
port = 6697
|
||||||
channels = [
|
channels = [
|
||||||
"#dispatch",
|
"#dispatch",
|
||||||
"#go-nuts"
|
"#go-nuts"
|
||||||
|
@ -11,9 +11,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type connectDefaults struct {
|
type connectDefaults struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name,omitempty"`
|
||||||
Address string `json:"address"`
|
Host string `json:"host,omitempty"`
|
||||||
Channels []string `json:"channels"`
|
Port int `json:"port,omitempty"`
|
||||||
|
Channels []string `json:"channels,omitempty"`
|
||||||
Password bool `json:"password,omitempty"`
|
Password bool `json:"password,omitempty"`
|
||||||
SSL bool `json:"ssl,omitempty"`
|
SSL bool `json:"ssl,omitempty"`
|
||||||
ReadOnly bool `json:"readonly,omitempty"`
|
ReadOnly bool `json:"readonly,omitempty"`
|
||||||
@ -81,7 +82,8 @@ func getIndexData(r *http.Request, session *Session) *indexData {
|
|||||||
|
|
||||||
data.Defaults = connectDefaults{
|
data.Defaults = connectDefaults{
|
||||||
Name: viper.GetString("defaults.name"),
|
Name: viper.GetString("defaults.name"),
|
||||||
Address: viper.GetString("defaults.address"),
|
Host: viper.GetString("defaults.host"),
|
||||||
|
Port: viper.GetInt("defaults.port"),
|
||||||
Channels: viper.GetStringSlice("defaults.channels"),
|
Channels: viper.GetStringSlice("defaults.channels"),
|
||||||
Password: viper.GetString("defaults.password") != "",
|
Password: viper.GetString("defaults.password") != "",
|
||||||
SSL: viper.GetBool("defaults.ssl"),
|
SSL: viper.GetBool("defaults.ssl"),
|
||||||
|
@ -85,7 +85,7 @@ func (h *wsHandler) connect(b []byte) {
|
|||||||
json.Unmarshal(b, &data)
|
json.Unmarshal(b, &data)
|
||||||
|
|
||||||
if _, ok := h.session.getIRC(data.Host); !ok {
|
if _, ok := h.session.getIRC(data.Host); !ok {
|
||||||
log.Println(h.addr, "[IRC] Add server", data.Server)
|
log.Println(h.addr, "[IRC] Add server", data.Host)
|
||||||
|
|
||||||
connectIRC(data.Server, h.session)
|
connectIRC(data.Server, h.session)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user