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() {
|
||||
return del(['dist']);
|
||||
};
|
||||
}
|
||||
|
||||
function js(cb) {
|
||||
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 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('default', dev);
|
||||
|
@ -53,6 +53,62 @@ i[class*=' icon-']:before {
|
||||
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 {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -214,9 +270,10 @@ i[class*=' icon-']:before {
|
||||
}
|
||||
|
||||
.connect-form {
|
||||
margin: auto 0;
|
||||
margin: auto 20px;
|
||||
padding-top: 20px;
|
||||
width: 300px;
|
||||
width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.connect-form h1 {
|
||||
@ -235,9 +292,7 @@ i[class*=' icon-']:before {
|
||||
}
|
||||
|
||||
.connect-form input {
|
||||
display: block;
|
||||
margin: 5px 0px;
|
||||
padding: 15px;
|
||||
margin-top: 5px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@ -249,6 +304,7 @@ i[class*=' icon-']:before {
|
||||
|
||||
.connect-form input[type='submit'] {
|
||||
height: 50px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
font-family: Montserrat, sans-serif;
|
||||
background: #6bb758;
|
||||
@ -264,28 +320,47 @@ i[class*=' icon-']:before {
|
||||
background: #6bb758;
|
||||
}
|
||||
|
||||
.connect-form input[type='checkbox'] {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
.connect-form-address {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.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 {
|
||||
float: right;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
padding: 10px 5px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.connect-form i:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.connect-form label {
|
||||
display: inline-block;
|
||||
padding: 10px 0;
|
||||
color: #333;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chat-title-bar {
|
||||
|
@ -1,144 +1,94 @@
|
||||
import React, { Component } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Form, withFormik } from 'formik';
|
||||
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(
|
||||
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 = {
|
||||
showOptionals: false,
|
||||
passwordTouched: false
|
||||
showOptionals: false
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
const { connect, select, join, defaults } = this.props;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const nick = e.target.nick.value.trim();
|
||||
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);
|
||||
}
|
||||
handleSSLChange = e => {
|
||||
const { values, setFieldValue } = this.props;
|
||||
if (e.target.checked && values.port === '6667') {
|
||||
setFieldValue('port', '6697', false);
|
||||
} else if (!e.target.checked && values.port === '6697') {
|
||||
setFieldValue('port', '6667', false);
|
||||
}
|
||||
setFieldValue('ssl', e.target.checked);
|
||||
};
|
||||
|
||||
handleShowClick = () => {
|
||||
this.setState({ showOptionals: !this.state.showOptionals });
|
||||
};
|
||||
|
||||
handlePasswordChange = () => {
|
||||
this.setState({ passwordTouched: true });
|
||||
renderOptionals = () => {
|
||||
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() {
|
||||
const { defaults } = this.props;
|
||||
let optionals = null;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const { defaults, values, errors, touched } = this.props;
|
||||
const { readonly, showDetails } = defaults;
|
||||
let form;
|
||||
|
||||
if (defaults.readonly) {
|
||||
if (readonly) {
|
||||
form = (
|
||||
<form className="connect-form" onSubmit={this.handleSubmit}>
|
||||
<Form className="connect-form">
|
||||
<h1>Connect</h1>
|
||||
{defaults.showDetails && (
|
||||
{showDetails && (
|
||||
<div className="connect-details">
|
||||
<h2>{defaults.address}</h2>
|
||||
{getSortedDefaultChannels(defaults).map(channel => (
|
||||
<h2>
|
||||
{values.host}:{values.port}
|
||||
</h2>
|
||||
{getSortedDefaultChannels(values).map(channel => (
|
||||
<p>{channel}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<input name="nick" type="text" placeholder="Nick" />
|
||||
<TextInput name="nick" placeholder="Nick" />
|
||||
{touched.nick && <Error>{errors.nick}</Error>}
|
||||
<input type="submit" value="Connect" />
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
} else {
|
||||
form = (
|
||||
<form className="connect-form" onSubmit={this.handleSubmit}>
|
||||
<Form className="connect-form">
|
||||
<h1>Connect</h1>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
defaultValue={defaults.name}
|
||||
/>
|
||||
<input
|
||||
name="address"
|
||||
type="text"
|
||||
placeholder="Address"
|
||||
defaultValue={defaults.address}
|
||||
/>
|
||||
<input name="nick" type="text" placeholder="Nick" />
|
||||
<input
|
||||
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>
|
||||
<TextInput name="name" placeholder="Name" />
|
||||
<div className="connect-form-address">
|
||||
<TextInput name="host" placeholder="Host" />
|
||||
<TextInput name="port" placeholder="Port" />
|
||||
<Checkbox name="ssl" label="SSL" onChange={this.handleSSLChange} />
|
||||
</div>
|
||||
{touched.host && <Error>{errors.host}</Error>}
|
||||
{touched.port && <Error>{errors.port}</Error>}
|
||||
<TextInput name="nick" placeholder="Nick" />
|
||||
{touched.nick && <Error>{errors.nick}</Error>}
|
||||
<TextInput name="channels" placeholder="Channels" />
|
||||
{touched.channels && <Error>{errors.channels}</Error>}
|
||||
{this.state.showOptionals && this.renderOptionals()}
|
||||
<i className="icon-ellipsis" onClick={this.handleShowClick} />
|
||||
</p>
|
||||
<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';
|
||||
|
||||
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()', () => {
|
||||
it('passes valid names to the server', () => {
|
||||
const name = 'cake';
|
||||
|
@ -232,7 +232,10 @@ describe('channel reducer', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
'127.0.0.1': {}
|
||||
|
@ -3,7 +3,10 @@ import * as actions from '../actions';
|
||||
|
||||
describe('server reducer', () => {
|
||||
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({
|
||||
'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({
|
||||
'127.0.0.1': {
|
||||
@ -33,9 +36,7 @@ describe('server reducer', () => {
|
||||
|
||||
state = reducer(
|
||||
state,
|
||||
connect('127.0.0.2:1337', 'nick', {
|
||||
name: 'srv'
|
||||
})
|
||||
connect({ host: '127.0.0.2', nick: 'nick', name: 'srv' })
|
||||
);
|
||||
|
||||
expect(state).toEqual({
|
||||
@ -93,7 +94,10 @@ describe('server reducer', () => {
|
||||
});
|
||||
|
||||
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, {
|
||||
type: actions.SET_NICK,
|
||||
server: '127.0.0.1',
|
||||
@ -111,7 +115,10 @@ describe('server reducer', () => {
|
||||
});
|
||||
|
||||
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, {
|
||||
type: actions.SET_NICK,
|
||||
server: '127.0.0.1',
|
||||
@ -134,7 +141,10 @@ describe('server reducer', () => {
|
||||
});
|
||||
|
||||
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, {
|
||||
type: actions.socket.NICK,
|
||||
server: '127.0.0.1',
|
||||
@ -152,7 +162,10 @@ describe('server reducer', () => {
|
||||
});
|
||||
|
||||
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, {
|
||||
type: actions.SET_NICK,
|
||||
server: '127.0.0.1',
|
||||
@ -200,6 +213,7 @@ describe('server reducer', () => {
|
||||
'127.0.0.1': {
|
||||
name: 'stuff',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: true
|
||||
}
|
||||
@ -207,6 +221,7 @@ describe('server reducer', () => {
|
||||
'127.0.0.2': {
|
||||
name: 'stuffz',
|
||||
nick: 'nick2',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false
|
||||
}
|
||||
@ -215,7 +230,10 @@ describe('server reducer', () => {
|
||||
});
|
||||
|
||||
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, {
|
||||
type: actions.socket.CONNECTION_UPDATE,
|
||||
server: '127.0.0.1',
|
||||
|
@ -14,7 +14,7 @@ export const getCurrentNick = createSelector(
|
||||
return;
|
||||
}
|
||||
const { editedNick } = servers[tab.server];
|
||||
if (!editedNick) {
|
||||
if (editedNick === null) {
|
||||
return servers[tab.server].nick;
|
||||
}
|
||||
return editedNick;
|
||||
@ -36,12 +36,12 @@ export const getCurrentServerStatus = createSelector(
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.CONNECT](state, { host, nick, options }) {
|
||||
[actions.CONNECT](state, { host, nick, name }) {
|
||||
if (!state[host]) {
|
||||
state[host] = {
|
||||
nick,
|
||||
editedNick: null,
|
||||
name: options.name || host,
|
||||
name: name || host,
|
||||
status: {
|
||||
connected: false,
|
||||
error: null
|
||||
@ -82,7 +82,7 @@ export default createReducer(
|
||||
[actions.socket.SERVERS](state, { data }) {
|
||||
if (data) {
|
||||
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) {
|
||||
let host = server;
|
||||
let port;
|
||||
const i = server.indexOf(':');
|
||||
if (i > 0) {
|
||||
host = server.slice(0, i);
|
||||
port = server.slice(i + 1);
|
||||
}
|
||||
|
||||
export function connect(config) {
|
||||
return {
|
||||
type: actions.CONNECT,
|
||||
host,
|
||||
nick,
|
||||
options,
|
||||
...config,
|
||||
socket: {
|
||||
type: 'connect',
|
||||
data: {
|
||||
host,
|
||||
port,
|
||||
nick,
|
||||
username: options.username || nick,
|
||||
password: options.password,
|
||||
realname: options.realname || nick,
|
||||
tls: options.tls || false,
|
||||
name: options.name || server
|
||||
}
|
||||
data: config
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { isChannel } from '..';
|
||||
import { isChannel, isValidNick, isValidChannel, isValidUsername } from '..';
|
||||
import linkify from '../linkify';
|
||||
|
||||
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()', () => {
|
||||
const proto = href => (href.indexOf('http') !== 0 ? `http://${href}` : href);
|
||||
const linkTo = href => (
|
||||
|
@ -2,7 +2,7 @@ if (Object.keys) {
|
||||
try {
|
||||
Object.keys('');
|
||||
} catch (e) {
|
||||
Object.keys = function(o, k, r) {
|
||||
Object.keys = function keys(o, k, r) {
|
||||
r = [];
|
||||
// eslint-disable-next-line
|
||||
for (k in o) r.hasOwnProperty.call(o, k) && r.push(k);
|
||||
|
@ -35,6 +35,93 @@ export function stringifyTab(server, name) {
|
||||
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()) {
|
||||
const h = padStart(date.getHours(), 2, '0');
|
||||
const m = padStart(date.getMinutes(), 2, '0');
|
||||
|
@ -4,7 +4,8 @@ verify_certificates = true
|
||||
# Defaults for the client connect form
|
||||
[defaults]
|
||||
name = "Freenode"
|
||||
address = "chat.freenode.net"
|
||||
host = "chat.freenode.net"
|
||||
port = 6697
|
||||
channels = [
|
||||
"#dispatch",
|
||||
"#go-nuts"
|
||||
|
@ -11,9 +11,10 @@ import (
|
||||
)
|
||||
|
||||
type connectDefaults struct {
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Channels []string `json:"channels,omitempty"`
|
||||
Password bool `json:"password,omitempty"`
|
||||
SSL bool `json:"ssl,omitempty"`
|
||||
ReadOnly bool `json:"readonly,omitempty"`
|
||||
@ -81,7 +82,8 @@ func getIndexData(r *http.Request, session *Session) *indexData {
|
||||
|
||||
data.Defaults = connectDefaults{
|
||||
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"),
|
||||
Password: viper.GetString("defaults.password") != "",
|
||||
SSL: viper.GetBool("defaults.ssl"),
|
||||
|
@ -85,7 +85,7 @@ func (h *wsHandler) connect(b []byte) {
|
||||
json.Unmarshal(b, &data)
|
||||
|
||||
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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user