Add prettier

This commit is contained in:
Ken-Håvard Lieng 2018-04-06 01:46:22 +02:00
parent 0cbbc1b8ff
commit b176b79144
46 changed files with 832 additions and 544 deletions

View File

@ -1,12 +1,15 @@
{
"presets": [
["@babel/preset-env", {
"modules": false,
"loose": true,
"targets": {
"browsers": ["ie 11"]
[
"@babel/preset-env",
{
"modules": false,
"loose": true,
"targets": {
"browsers": ["ie 11"]
}
}
}],
],
"@babel/preset-react",
"@babel/preset-stage-0"
],

View File

@ -1,5 +1,5 @@
{
"extends": "airbnb",
"extends": ["airbnb", "plugin:prettier/recommended"],
"parser": "babel-eslint",
"env": {
"browser": true

3
client/.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@ -20,7 +20,10 @@ function brotli(opts) {
}
if (file.isStream()) {
this.emit('error', new gutil.PluginError('brotli', 'Streams not supported'));
this.emit(
'error',
new gutil.PluginError('brotli', 'Streams not supported')
);
} else if (file.isBuffer()) {
file.path += '.br';
file.contents = new Buffer(br.compress(file.contents, opts).buffer);
@ -30,11 +33,14 @@ function brotli(opts) {
}
gulp.task('css', function() {
return gulp.src(['src/css/fonts.css', 'src/css/fontello.css', 'src/css/style.css'])
return gulp
.src(['src/css/fonts.css', 'src/css/fontello.css', 'src/css/style.css'])
.pipe(concat('bundle.css'))
.pipe(autoprefixer({
browsers: ['ie 11']
}))
.pipe(
autoprefixer({
browsers: ['ie 11']
})
)
.pipe(nano())
.pipe(gulp.dest('dist'));
});
@ -48,9 +54,12 @@ gulp.task('js', function(cb) {
compiler.run(function(err, stats) {
if (err) throw new gutil.PluginError('webpack', err);
gutil.log('[webpack]', stats.toString({
colors: true
}));
gutil.log(
'[webpack]',
stats.toString({
colors: true
})
);
if (stats.hasErrors()) process.exit(1);
@ -59,22 +68,20 @@ gulp.task('js', function(cb) {
});
gulp.task('fonts', function() {
return gulp.src('src/font/*')
.pipe(gulp.dest('dist/font'));
return gulp.src('src/font/*').pipe(gulp.dest('dist/font'));
});
gulp.task('fonts:woff', function() {
return gulp.src('src/font/*(*.woff|*.woff2)')
.pipe(gulp.dest('dist/br/font'));
return gulp.src('src/font/*(*.woff|*.woff2)').pipe(gulp.dest('dist/br/font'));
});
gulp.task('config', function() {
return gulp.src('../config.default.toml')
.pipe(gulp.dest('dist/br'));
return gulp.src('../config.default.toml').pipe(gulp.dest('dist/br'));
});
function compress() {
return gulp.src(['dist/**/!(*.br|*.woff|*.woff2)', '!dist/{br,br/**}'])
return gulp
.src(['dist/**/!(*.br|*.woff|*.woff2)', '!dist/{br,br/**}'])
.pipe(brotli({ quality: 11 }))
.pipe(gulp.dest('dist/br'));
}
@ -83,48 +90,71 @@ gulp.task('compress', ['css', 'js', 'fonts'], compress);
gulp.task('compress:dev', ['css', 'fonts'], compress);
gulp.task('bindata', ['compress', 'config'], function(cb) {
exec('go-bindata -nomemcopy -nocompress -pkg assets -o ../assets/bindata.go -prefix "dist/br" dist/br/...', cb);
exec(
'go-bindata -nomemcopy -nocompress -pkg assets -o ../assets/bindata.go -prefix "dist/br" dist/br/...',
cb
);
});
gulp.task('bindata:dev', ['compress:dev', 'config'], function(cb) {
exec('go-bindata -debug -pkg assets -o ../assets/bindata.go -prefix "dist/br" dist/br/...', cb);
exec(
'go-bindata -debug -pkg assets -o ../assets/bindata.go -prefix "dist/br" dist/br/...',
cb
);
});
gulp.task('dev', ['css', 'fonts', 'fonts:woff', 'config', 'compress:dev', 'bindata:dev'], function() {
gulp.watch('src/css/*.css', ['css']);
gulp.task(
'dev',
['css', 'fonts', 'fonts:woff', 'config', 'compress:dev', 'bindata:dev'],
function() {
gulp.watch('src/css/*.css', ['css']);
var config = require('./webpack.config.dev.js');
var compiler = webpack(config);
var app = express();
var config = require('./webpack.config.dev.js');
var compiler = webpack(config);
var app = express();
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
headers: {
'Access-Control-Allow-Origin': '*'
}
}));
app.use(
require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
headers: {
'Access-Control-Allow-Origin': '*'
}
})
);
app.use(require('webpack-hot-middleware')(compiler));
app.use(require('webpack-hot-middleware')(compiler));
app.use('/', express.static('dist'));
app.use('/', express.static('dist'));
app.use('*', proxy('localhost:1337', {
proxyReqPathResolver: function(req) {
return req.originalUrl;
}
}));
app.use(
'*',
proxy('localhost:1337', {
proxyReqPathResolver: function(req) {
return req.originalUrl;
}
})
);
app.listen(3000, function (err) {
if (err) {
console.log(err);
return;
}
app.listen(3000, function(err) {
if (err) {
console.log(err);
return;
}
console.log('Listening at http://localhost:3000');
});
});
console.log('Listening at http://localhost:3000');
});
}
);
gulp.task('build', ['css', 'js', 'fonts', 'fonts:woff', 'config', 'compress', 'bindata']);
gulp.task('build', [
'css',
'js',
'fonts',
'fonts:woff',
'config',
'compress',
'bindata'
]);
gulp.task('default', ['dev']);

View File

@ -19,10 +19,12 @@
"css-loader": "^0.28.0",
"eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-import-resolver-webpack": "^0.9.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "^7.7.0",
"express": "^4.14.1",
"express-http-proxy": "^1.0.1",
@ -32,6 +34,7 @@
"gulp-cssnano": "^2.1.2",
"gulp-util": "^3.0.8",
"jest": "^22.4.3",
"prettier": "1.11.1",
"style-loader": "^0.20.3",
"through2": "^2.0.3",
"webpack": "^4.1.1",
@ -58,6 +61,8 @@
"url-pattern": "^1.0.3"
},
"scripts": {
"prettier": "prettier --write {.*,*.js,src/css/*.css}",
"prettier:all": "prettier --write {.*,*.js,src/**/*.js,src/css/*.css}",
"test": "jest",
"test:verbose": "jest --verbose",
"test:watch": "jest --watch"

View File

@ -1,14 +1,15 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.woff2?48901973') format('woff2'),
url('../font/fontello.woff?48901973') format('woff'),
url('../font/fontello.ttf?48901973') format('truetype');
url('../font/fontello.woff?48901973') format('woff'),
url('../font/fontello.ttf?48901973') format('truetype');
font-weight: normal;
font-style: normal;
}
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
[class^='icon-']:before,
[class*=' icon-']:before {
font-family: 'fontello';
font-style: normal;
font-weight: normal;
speak: none;
@ -16,7 +17,7 @@
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
margin-right: 0.2em;
text-align: center;
/* opacity: .8; */
@ -29,7 +30,7 @@
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
margin-left: 0.2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
@ -42,9 +43,21 @@
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-cancel:before { content: '\e800'; } /* '' */
.icon-menu:before { content: '\e801'; } /* '' */
.icon-cog:before { content: '\e802'; } /* '' */
.icon-search:before { content: '\e803'; } /* '' */
.icon-user:before { content: '\f061'; } /* '' */
.icon-ellipsis:before { content: '\f141'; } /* '' */
.icon-cancel:before {
content: '\e800';
} /* '' */
.icon-menu:before {
content: '\e801';
} /* '' */
.icon-cog:before {
content: '\e802';
} /* '' */
.icon-search:before {
content: '\e803';
} /* '' */
.icon-user:before {
content: '\f061';
} /* '' */
.icon-ellipsis:before {
content: '\f141';
} /* '' */

View File

@ -3,9 +3,9 @@
font-style: normal;
font-weight: 400;
src: local('Montserrat-Regular'),
url(/font/Montserrat-Regular.woff2) format('woff2'),
url(/font/Montserrat-Regular.woff) format('woff'),
url(/font/Montserrat-Regular.ttf) format('truetype');
url(/font/Montserrat-Regular.woff2) format('woff2'),
url(/font/Montserrat-Regular.woff) format('woff'),
url(/font/Montserrat-Regular.ttf) format('truetype');
}
@font-face {
@ -13,29 +13,27 @@
font-style: normal;
font-weight: 700;
src: local('Montserrat-Bold'),
url(/font/Montserrat-Bold.woff2) format('woff2'),
url(/font/Montserrat-Bold.woff) format('woff'),
url(/font/Montserrat-Bold.ttf) format('truetype');
url(/font/Montserrat-Bold.woff2) format('woff2'),
url(/font/Montserrat-Bold.woff) format('woff'),
url(/font/Montserrat-Bold.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: local('Roboto Mono'),
local('RobotoMono-Regular'),
url(/font/RobotoMono-Regular.woff2) format('woff2'),
url(/font/RobotoMono-Regular.woff) format('woff'),
url(/font/RobotoMono-Regular.ttf) format('truetype');
src: local('Roboto Mono'), local('RobotoMono-Regular'),
url(/font/RobotoMono-Regular.woff2) format('woff2'),
url(/font/RobotoMono-Regular.woff) format('woff'),
url(/font/RobotoMono-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: local('Roboto Mono Bold'),
local('RobotoMono-Bold'),
url(/font/RobotoMono-Bold.woff2) format('woff2'),
url(/font/RobotoMono-Bold.woff) format('woff'),
url(/font/RobotoMono-Bold.ttf) format('truetype');
src: local('Roboto Mono Bold'), local('RobotoMono-Bold'),
url(/font/RobotoMono-Bold.woff2) format('woff2'),
url(/font/RobotoMono-Bold.woff) format('woff'),
url(/font/RobotoMono-Bold.ttf) format('truetype');
}

View File

@ -9,7 +9,12 @@ body {
background: #f0f0f0;
}
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: Montserrat, sans-serif;
font-weight: 400;
}
@ -35,16 +40,17 @@ p {
line-height: 1.5;
}
i[class^="icon-"]:before, i[class*=" icon-"]:before {
i[class^='icon-']:before,
i[class*=' icon-']:before {
margin: 0;
}
.success {
color: #6BB758 !important;
color: #6bb758 !important;
}
.error {
color: #F6546A !important;
color: #f6546a !important;
}
.wrap {
@ -60,8 +66,8 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.app-info {
width: 100%;
font-family: Montserrat, sans-serif;
background: #F6546A;
color: #FFF;
background: #f6546a;
color: #fff;
height: 50px;
line-height: 50px;
text-align: center;
@ -79,9 +85,9 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
bottom: 0;
width: 200px;
background: #222;
color: #FFF;
color: #fff;
font-family: Montserrat, sans-serif;
transition: transform .2s;
transition: transform 0.2s;
}
.tab-container {
@ -109,7 +115,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.tablist p.selected {
padding-left: 10px;
border-left: 5px solid #6BB758;
border-left: 5px solid #6bb758;
}
.tab-content {
@ -141,16 +147,16 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.button-connect {
width: 100%;
height: 50px;
background: #6BB758;
color: #FFF;
background: #6bb758;
color: #fff;
}
.button-connect:hover {
background: #7BBF6A;
background: #7bbf6a;
}
.button-connect:active {
background: #6BB758;
background: #6bb758;
}
.side-buttons {
@ -168,16 +174,16 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
line-height: 50px;
cursor: pointer;
font-size: 20px;
border-top: 1px solid #1D1D1D;
border-top: 1px solid #1d1d1d;
}
.side-buttons i:not(:first-child) {
border-left: 1px solid #1D1D1D;
border-left: 1px solid #1d1d1d;
}
.side-buttons i:hover {
color: #CCC;
background: #1D1D1D;
color: #ccc;
background: #1d1d1d;
}
.main-container {
@ -186,7 +192,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
top: 0;
bottom: 0;
right: 0;
transition: left .2s, transform .2s;
transition: left 0.2s, transform 0.2s;
}
.connect {
@ -199,7 +205,8 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
overflow: auto;
}
.connect .navicon, .settings .navicon {
.connect .navicon,
.settings .navicon {
position: fixed;
top: 0;
left: 0;
@ -223,30 +230,30 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
border: none;
}
.connect-form input[type="submit"],
.connect-form input[type="text"],
.connect-form input[type="password"] {
.connect-form input[type='submit'],
.connect-form input[type='text'],
.connect-form input[type='password'] {
width: 100%;
}
.connect-form input[type="submit"] {
.connect-form input[type='submit'] {
height: 50px;
margin-bottom: 20px;
font-family: Montserrat, sans-serif;
background: #6BB758;
color: #FFF;
background: #6bb758;
color: #fff;
cursor: pointer;
}
.connect-form input[type="submit"]:hover {
background: #7BBF6A;
.connect-form input[type='submit']:hover {
background: #7bbf6a;
}
.connect-form input[type="submit"]:active {
background: #6BB758;
.connect-form input[type='submit']:active {
background: #6bb758;
}
.connect-form input[type="checkbox"] {
.connect-form input[type='checkbox'] {
display: inline-block;
margin-right: 5px;
vertical-align: middle;
@ -278,7 +285,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
right: 0;
height: 50px;
line-height: 50px;
border-bottom: 1px solid #DDD;
border-bottom: 1px solid #ddd;
display: flex;
font-size: 20px;
}
@ -304,28 +311,31 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
display: none;
}
.chat-server .userlist, .chat-private .userlist {
.chat-server .userlist,
.chat-private .userlist {
display: none;
}
.chat-server .userlist-bar, .chat-private .userlist-bar {
.chat-server .userlist-bar,
.chat-private .userlist-bar {
display: none;
}
.button-leave {
border-left: 1px solid #DDD;
border-left: 1px solid #ddd;
}
.button-leave:hover {
background: #DDD;
background: #ddd;
}
.button-userlist {
display: none;
border-left: 1px solid #DDD;
border-left: 1px solid #ddd;
}
.chat-server .button-userlist, .chat-private .button-userlist {
.chat-server .button-userlist,
.chat-private .button-userlist {
display: none;
}
@ -369,8 +379,8 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
right: 0;
width: 200px;
height: 50px;
border-left: 1px solid #DDD;
border-bottom: 1px solid #DDD;
border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd;
line-height: 50px;
text-align: center;
padding: 0 15px;
@ -403,13 +413,13 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.search-input-wrap {
display: flex;
width: 100%;
background: #FFF;
border-bottom: 1px solid #DDD;
background: #fff;
border-bottom: 1px solid #ddd;
}
.search i {
padding: 15px;
color: #DDD;
color: #ddd;
}
.search-input {
@ -465,17 +475,17 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
}
.message-error {
color: #F6546A;
color: #f6546a;
}
.message-prompt {
font-weight: 700;
font-style: italic;
color: #6BB758;
color: #6bb758;
}
.message-action {
color: #FF6698;
color: #ff6698;
}
.message-time {
@ -486,13 +496,13 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.message-sender {
font-weight: 700;
color: #6BB758;
color: #6bb758;
cursor: pointer;
}
.message a {
text-decoration: none;
color: #0066FF;
color: #0066ff;
}
.message a:hover {
@ -507,8 +517,8 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
height: 50px;
z-index: 1;
display: flex;
border-top: 1px solid #DDD;
background: #FFF;
border-top: 1px solid #ddd;
background: #fff;
}
.message-input-nick {
@ -517,8 +527,8 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
line-height: 30px;
height: 30px;
padding: 0 10px;
background: #6BB758 !important;
color: #FFF;
background: #6bb758 !important;
color: #fff;
font-family: Montserrat, sans-serif !important;
margin-right: 0;
}
@ -536,10 +546,10 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
bottom: 50px;
right: 0;
width: 200px;
border-left: 1px solid #DDD;
border-left: 1px solid #ddd;
background: #f0f0f0;
z-index: 2;
transition: transform .2s;
transition: transform 0.2s;
}
.userlist p {
@ -548,7 +558,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
}
.userlist p:hover {
background: #DDD;
background: #ddd;
}
.settings {
@ -569,18 +579,18 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.settings button {
margin: 5px;
color: #FFF;
background: #6BB758;
color: #fff;
background: #6bb758;
padding: 10px 20px;
width: 200px;
}
.settings button:hover {
background: #7BBF6A;
background: #7bbf6a;
}
.settings button:active {
background: #6BB758;
background: #6bb758;
}
.settings div {
@ -589,11 +599,11 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
.settings .error {
margin: 10px;
color: #F6546A;
color: #f6546a;
}
.input-file {
color: #FFF;
color: #fff;
background: #222 !important;
padding: 10px;
margin: 5px;
@ -664,7 +674,8 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
transform: translateX(0);
}
.chat-channel .chat-title-bar, .chat-channel .messagebox {
.chat-channel .chat-title-bar,
.chat-channel .messagebox {
right: 0;
}

View File

@ -4,7 +4,10 @@ import { sendMessage, raw } from 'state/messages';
import { setNick, disconnect, whois, away } from 'state/servers';
import { select } from 'state/tab';
import { find } from 'utils';
import createCommandMiddleware, { beforeHandler, notFoundHandler } from './middleware/command';
import createCommandMiddleware, {
beforeHandler,
notFoundHandler
} from './middleware/command';
const help = [
'/join <channel> - Join a channel',
@ -26,7 +29,8 @@ const help = [
const text = content => ({ content });
const error = content => ({ content, type: 'error' });
const prompt = content => ({ content, type: 'prompt' });
const findHelp = cmd => find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
const findHelp = cmd =>
find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
export default createCommandMiddleware(COMMAND, {
join({ dispatch, server }, channel) {
@ -160,10 +164,7 @@ export default createCommandMiddleware(COMMAND, {
dispatch(raw(cmd, server));
return prompt(`=> ${cmd}`);
}
return [
prompt('=> /raw'),
error('Missing message')
];
return [prompt('=> /raw'), error('Missing message')];
},
help(_, ...commands) {

View File

@ -14,14 +14,26 @@ export default class App extends Component {
};
render() {
const { connected, tab, channels, servers,
privateChats, showTabList, select, push } = this.props;
const mainClass = showTabList ? 'main-container off-canvas' : 'main-container';
const {
connected,
tab,
channels,
servers,
privateChats,
showTabList,
select,
push
} = this.props;
const mainClass = showTabList
? 'main-container off-canvas'
: 'main-container';
return (
<div className="wrap">
{!connected &&
<div className="app-info">Connection lost, attempting to reconnect...</div>
}
{!connected && (
<div className="app-info">
Connection lost, attempting to reconnect...
</div>
)}
<div className="app-container" onClick={this.handleClick}>
<TabList
tab={tab}
@ -33,9 +45,15 @@ export default class App extends Component {
push={push}
/>
<div className={mainClass}>
<Route name="chat"><Chat /></Route>
<Route name="connect"><Connect /></Route>
<Route name="settings"><Settings /></Route>
<Route name="chat">
<Chat />
</Route>
<Route name="connect">
<Connect />
</Route>
<Route name="settings">
<Settings />
</Route>
</div>
</div>
</div>

View File

@ -24,36 +24,48 @@ export default class TabList extends PureComponent {
/>
);
server.forEach((channel, name) => tabs.push(
<TabListItem
key={address + name}
server={address}
target={name}
content={name}
selected={tab.server === address && tab.name === name}
onClick={this.handleTabClick}
/>
));
if (privateChats.has(address) && privateChats.get(address).size > 0) {
tabs.push(<div key={`${address}-pm}`} className="tab-label">Private messages</div>);
privateChats.get(address).forEach(nick => tabs.push(
server.forEach((channel, name) =>
tabs.push(
<TabListItem
key={address + nick}
key={address + name}
server={address}
target={nick}
content={nick}
selected={tab.server === address && tab.name === nick}
target={name}
content={name}
selected={tab.server === address && tab.name === name}
onClick={this.handleTabClick}
/>
));
)
);
if (privateChats.has(address) && privateChats.get(address).size > 0) {
tabs.push(
<div key={`${address}-pm}`} className="tab-label">
Private messages
</div>
);
privateChats
.get(address)
.forEach(nick =>
tabs.push(
<TabListItem
key={address + nick}
server={address}
target={nick}
content={nick}
selected={tab.server === address && tab.name === nick}
onClick={this.handleTabClick}
/>
)
);
}
});
return (
<div className={className}>
<button className="button-connect" onClick={this.handleConnectClick}>Connect</button>
<button className="button-connect" onClick={this.handleConnectClick}>
Connect
</button>
<div className="tab-container">{tabs}</div>
<div className="side-buttons">
<i className="icon-user" />

View File

@ -90,10 +90,7 @@ export default class Chat extends Component {
onToggleSearch={toggleSearch}
onToggleUserList={toggleUserList}
/>
<Search
search={search}
onSearch={this.handleSearch}
/>
<Search search={search} onSearch={this.handleSearch} />
<MessageBox
hasMoreMessages={hasMoreMessages}
messages={messages}

View File

@ -7,8 +7,16 @@ import { linkify } from 'utils';
export default class ChatTitle extends PureComponent {
render() {
const { status, title, tab, channel, onTitleChange,
onToggleSearch, onToggleUserList, onCloseClick } = this.props;
const {
status,
title,
tab,
channel,
onTitleChange,
onToggleSearch,
onToggleUserList,
onCloseClick
} = this.props;
let closeTitle;
if (tab.isChannel()) {
@ -21,7 +29,9 @@ export default class ChatTitle extends PureComponent {
let serverError = null;
if (!tab.name && status.error) {
serverError = <span className="chat-topic error">Error: {status.error}</span>;
serverError = (
<span className="chat-topic error">Error: {status.error}</span>
);
}
return (
@ -38,7 +48,9 @@ export default class ChatTitle extends PureComponent {
<span className="chat-title">{title}</span>
</Editable>
<div className="chat-topic-wrap">
<span className="chat-topic">{linkify(channel.get('topic')) || null}</span>
<span className="chat-topic">
{linkify(channel.get('topic')) || null}
</span>
{serverError}
</div>
<i className="icon-search" title="Search" onClick={onToggleSearch} />
@ -51,7 +63,9 @@ export default class ChatTitle extends PureComponent {
</div>
<div className="userlist-bar">
<i className="icon-user" />
<span className="chat-usercount">{channel.get('users', List()).size}</span>
<span className="chat-usercount">
{channel.get('users', List()).size}
</span>
</div>
</div>
);

View File

@ -5,7 +5,9 @@ export default class Message extends PureComponent {
render() {
const { message } = this.props;
const className = message.type ? `message message-${message.type}` : 'message';
const className = message.type
? `message message-${message.type}`
: 'message';
const style = {
paddingLeft: `${window.messageIndent + 15}px`,
textIndent: `-${window.messageIndent}px`,
@ -15,11 +17,13 @@ export default class Message extends PureComponent {
return (
<p className={className} style={style}>
<span className="message-time">{message.time}</span>
{message.from &&
{message.from && (
<span className="message-sender" onClick={this.handleNickClick}>
{' '}{message.from}
{' '}
{message.from}
</span>
}{' '}{message.content}
)}{' '}
{message.content}
</p>
);
}

View File

@ -33,7 +33,8 @@ export default class MessageBox extends PureComponent {
if (nextProps.messages.get(0) !== this.props.messages.get(0)) {
if (nextProps.tab === this.props.tab) {
const addedMessages = nextProps.messages.size - this.props.messages.size;
const addedMessages =
nextProps.messages.size - this.props.messages.size;
let addedHeight = 0;
for (let i = 0; i < addedMessages; i++) {
addedHeight += nextProps.messages.get(i).height;
@ -126,10 +127,12 @@ export default class MessageBox extends PureComponent {
handleScroll = ({ scrollTop, clientHeight, scrollHeight }) => {
if (this.mounted) {
if (!this.loading &&
if (
!this.loading &&
this.props.hasMoreMessages &&
scrollTop <= fetchThreshold &&
scrollTop < this.prevScrollTop) {
scrollTop < this.prevScrollTop
) {
this.fetchMore();
}
@ -165,11 +168,7 @@ export default class MessageBox extends PureComponent {
if (index === 0) {
if (this.props.hasMoreMessages) {
return (
<div
key="top"
className="messagebox-top-indicator"
style={style}
>
<div key="top" className="messagebox-top-indicator" style={style}>
Loading messages...
</div>
);

View File

@ -7,8 +7,16 @@ export default class MessageInput extends PureComponent {
};
handleKey = e => {
const { tab, onCommand, onMessage,
add, reset, increment, decrement, currentHistoryEntry } = this.props;
const {
tab,
onCommand,
onMessage,
add,
reset,
increment,
decrement,
currentHistoryEntry
} = this.props;
if (e.key === 'Enter' && e.target.value) {
if (e.target.value[0] === '/') {
@ -36,7 +44,12 @@ export default class MessageInput extends PureComponent {
};
render() {
const { nick, currentHistoryEntry, onNickChange, onNickEditDone } = this.props;
const {
nick,
currentHistoryEntry,
onNickChange,
onNickEditDone
} = this.props;
return (
<div className="message-input-wrap">
<Editable

View File

@ -8,7 +8,9 @@ export default class Search extends PureComponent {
}
}
inputRef = el => { this.input = el; };
inputRef = el => {
this.input = el;
};
handleSearch = e => this.props.onSearch(e.target.value);

View File

@ -11,12 +11,14 @@ export default class SearchResult extends PureComponent {
return (
<p className="search-result" style={style}>
<span className="message-time">{timestamp(new Date(result.time * 1000))}</span>
<span className="message-time">
{timestamp(new Date(result.time * 1000))}
</span>
<span>
{' '}
<span className="message-sender">{result.from}</span>
</span>
<span>{' '}{linkify(result.content)}</span>
<span> {linkify(result.content)}</span>
</p>
);
}

View File

@ -10,7 +10,9 @@ export default class UserList extends PureComponent {
}
}
listRef = el => { this.list = el; };
listRef = el => {
this.list = el;
};
renderUser = ({ index, style, key }) => {
const { users, onNickClick } = this.props;

View File

@ -14,7 +14,10 @@ export default class Connect extends Component {
let address = e.target.address.value.trim();
const nick = e.target.nick.value.trim();
const channels = e.target.channels.value.split(',').map(s => s.trim()).filter(s => s);
const channels = e.target.channels.value
.split(',')
.map(s => s.trim())
.filter(s => s);
const opts = {
name: e.target.name.value.trim(),
tls: e.target.ssl.checked
@ -78,18 +81,32 @@ export default class Connect extends Component {
<Navicon />
<form className="connect-form" onSubmit={this.handleSubmit}>
<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="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}
defaultValue={
defaults.channels ? defaults.channels.join(',') : null
}
/>
{optionals}
<p>
<label htmlFor="ssl"><input name="ssl" type="checkbox" defaultChecked={defaults.ssl} />SSL</label>
<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" />

View File

@ -26,7 +26,7 @@ const Settings = ({ settings, onCertChange, onKeyChange, uploadCert }) => {
/>
</div>
<button onClick={uploadCert}>{status}</button>
{ error ? <p className="error">{error}</p> : null }
{error ? <p className="error">{error}</p> : null}
</div>
);
};

View File

@ -30,7 +30,8 @@ export default class Editable extends PureComponent {
getInputWidth(value) {
if (this.input) {
const style = window.getComputedStyle(this.input);
const padding = parseInt(style.paddingLeft, 10) + parseInt(style.paddingRight, 10);
const padding =
parseInt(style.paddingLeft, 10) + parseInt(style.paddingRight, 10);
// Make sure the width is atleast 1px so the caret always shows
const width = stringWidth(value, style.font) || 1;
return padding + width;
@ -68,7 +69,9 @@ export default class Editable extends PureComponent {
}
};
inputRef = el => { this.input = el; }
inputRef = el => {
this.input = el;
};
render() {
const { children, className, value } = this.props;
@ -77,21 +80,21 @@ export default class Editable extends PureComponent {
width: this.state.width
};
return (
this.state.editing ?
<input
autoFocus
ref={this.inputRef}
className={className}
type="text"
value={value}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleKey}
style={style}
spellCheck={false}
/> :
<div onClick={this.startEditing}>{children}</div>
return this.state.editing ? (
<input
autoFocus
ref={this.inputRef}
className={className}
type="text"
value={value}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleKey}
style={style}
spellCheck={false}
/>
) : (
<div onClick={this.startEditing}>{children}</div>
);
}
}

View File

@ -21,7 +21,9 @@ export default class FileInput extends PureComponent {
render() {
return (
<button className="input-file" onClick={this.handleClick}>{this.props.name}</button>
<button className="input-file" onClick={this.handleClick}>
{this.props.name}
</button>
);
}
}

View File

@ -3,14 +3,35 @@ import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import Chat from 'components/pages/Chat';
import { getSelectedTabTitle } from 'state';
import { getSelectedChannel, getSelectedChannelUsers, part } from 'state/channels';
import { getCurrentInputHistoryEntry, addInputHistory, resetInputHistory,
incrementInputHistory, decrementInputHistory } from 'state/input';
import { getSelectedMessages, getHasMoreMessages,
runCommand, sendMessage, fetchMessages, addFetchedMessages } from 'state/messages';
import {
getSelectedChannel,
getSelectedChannelUsers,
part
} from 'state/channels';
import {
getCurrentInputHistoryEntry,
addInputHistory,
resetInputHistory,
incrementInputHistory,
decrementInputHistory
} from 'state/input';
import {
getSelectedMessages,
getHasMoreMessages,
runCommand,
sendMessage,
fetchMessages,
addFetchedMessages
} from 'state/messages';
import { openPrivateChat, closePrivateChat } from 'state/privateChats';
import { getSearch, searchMessages, toggleSearch } from 'state/search';
import { getCurrentNick, getCurrentServerStatus, disconnect, setNick, setServerName } from 'state/servers';
import {
getCurrentNick,
getCurrentServerStatus,
disconnect,
setNick,
setServerName
} from 'state/servers';
import { getSelectedTab, select } from 'state/tab';
import { getShowUserList, toggleUserList } from 'state/ui';
@ -29,29 +50,35 @@ const mapState = createStructuredSelector({
});
const mapDispatch = dispatch => ({
...bindActionCreators({
addFetchedMessages,
closePrivateChat,
disconnect,
fetchMessages,
openPrivateChat,
part,
runCommand,
searchMessages,
select,
sendMessage,
setNick,
setServerName,
toggleSearch,
toggleUserList
}, dispatch),
...bindActionCreators(
{
addFetchedMessages,
closePrivateChat,
disconnect,
fetchMessages,
openPrivateChat,
part,
runCommand,
searchMessages,
select,
sendMessage,
setNick,
setServerName,
toggleSearch,
toggleUserList
},
dispatch
),
inputActions: bindActionCreators({
add: addInputHistory,
reset: resetInputHistory,
increment: incrementInputHistory,
decrement: decrementInputHistory
}, dispatch)
inputActions: bindActionCreators(
{
add: addInputHistory,
reset: resetInputHistory,
increment: incrementInputHistory,
decrement: decrementInputHistory
},
dispatch
)
});
export default connect(mapState, mapDispatch)(Chat);

View File

@ -10,7 +10,9 @@ import routes from './routes';
import runModules from './modules';
const production = process.env.NODE_ENV === 'production';
const host = production ? window.location.host : `${window.location.hostname}:1337`;
const host = production
? window.location.host
: `${window.location.hostname}:1337`;
const socket = new Socket(host);
const store = configureStore(socket);

View File

@ -1,6 +1,12 @@
import { socketAction } from 'state/actions';
import { setConnected } from 'state/app';
import { broadcast, inform, print, addMessage, addMessages } from 'state/messages';
import {
broadcast,
inform,
print,
addMessage,
addMessages
} from 'state/messages';
import { reconnect } from 'state/servers';
import { select } from 'state/tab';
import { normalizeChannel } from 'utils';
@ -21,7 +27,10 @@ function findChannels(state, server, user) {
return channels;
}
export default function handleSocket({ socket, store: { dispatch, getState } }) {
export default function handleSocket({
socket,
store: { dispatch, getState }
}) {
const handlers = {
message(message) {
dispatch(addMessage(message, message.server, message.to));
@ -41,10 +50,12 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
const [joinedChannel] = channels;
if (tab.server && tab.name) {
const { nick } = state.servers.get(tab.server);
if (tab.server === server &&
if (
tab.server === server &&
nick === user &&
tab.name !== joinedChannel &&
normalizeChannel(tab.name) === normalizeChannel(joinedChannel)) {
normalizeChannel(tab.name) === normalizeChannel(joinedChannel)
) {
dispatch(select(server, joinedChannel));
}
}
@ -53,7 +64,9 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
},
part({ user, server, channel, reason }) {
dispatch(inform(withReason(`${user} left the channel`, reason), server, channel));
dispatch(
inform(withReason(`${user} left the channel`, reason), server, channel)
);
},
quit({ user, server, reason }) {
@ -63,7 +76,9 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
nick({ server, oldNick, newNick }) {
const channels = findChannels(getState(), server, oldNick);
dispatch(broadcast(`${oldNick} changed nick to ${newNick}`, server, channels));
dispatch(
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
);
},
topic({ server, channel, topic, nick }) {
@ -84,14 +99,20 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
whois(data) {
const tab = getState().tab.selected;
dispatch(print([
`Nick: ${data.nick}`,
`Username: ${data.username}`,
`Realname: ${data.realname}`,
`Host: ${data.host}`,
`Server: ${data.server}`,
`Channels: ${data.channels}`
], tab.server, tab.name));
dispatch(
print(
[
`Nick: ${data.nick}`,
`Username: ${data.username}`,
`Realname: ${data.realname}`,
`Host: ${data.host}`,
`Server: ${data.server}`,
`Channels: ${data.channels}`
],
tab.server,
tab.name
)
);
},
print(message) {
@ -100,11 +121,17 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
},
connection_update({ server, errorType }) {
if (errorType === 'verify' &&
confirm('The server is using a self-signed certificate, continue anyway?')) {
dispatch(reconnect(server, {
skipVerify: true
}));
if (
errorType === 'verify' &&
confirm(
'The server is using a self-signed certificate, continue anyway?'
)
) {
dispatch(
reconnect(server, {
skipVerify: true
})
);
}
},

View File

@ -1,4 +1,4 @@
import { connect, setServerName } from '../servers';
import { connect, setServerName } from '../servers';
describe('connect()', () => {
it('sets host and port correctly', () => {

View File

@ -1,13 +1,15 @@
import Immutable from 'immutable';
import reducer, { compareUsers } from '../channels';
import { connect } from '../servers';
import { connect } from '../servers';
import * as actions from '../actions';
describe('channel reducer', () => {
it('removes channels on PART', () => {
let state = Immutable.fromJS({
srv1: {
chan1: {}, chan2: {}, chan3: {}
chan1: {},
chan2: {},
chan3: {}
},
srv2: {
chan1: {}
@ -45,14 +47,10 @@ describe('channel reducer', () => {
expect(state.toJS()).toEqual({
srv: {
chan1: {
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' },
]
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
@ -64,9 +62,7 @@ describe('channel reducer', () => {
expect(state.toJS()).toEqual({
srv: {
chan1: {
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' }
]
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
}
}
});
@ -86,9 +82,7 @@ describe('channel reducer', () => {
expect(state.toJS()).toEqual({
srv: {
chan1: {
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' }
]
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
},
chan2: {
users: []
@ -118,9 +112,7 @@ describe('channel reducer', () => {
]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
@ -131,13 +123,7 @@ describe('channel reducer', () => {
type: actions.socket.USERS,
server: 'srv',
channel: 'chan1',
users: [
'user3',
'user2',
'@user4',
'user1',
'+user5'
]
users: ['user3', 'user2', '@user4', 'user1', '+user5']
});
expect(state.toJS()).toEqual({
@ -152,7 +138,7 @@ describe('channel reducer', () => {
]
}
}
})
});
});
it('handles SOCKET_TOPIC', () => {
@ -188,14 +174,12 @@ describe('channel reducer', () => {
]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
state = reducer(state, socket_mode('srv', 'chan1', 'nick1' ,'v', 'o'));
state = reducer(state, socket_mode('srv', 'chan1', 'nick1', 'v', 'o'));
state = reducer(state, socket_mode('srv', 'chan1', 'nick2', 'o', ''));
state = reducer(state, socket_mode('srv', 'chan2', 'not_there', 'x', ''));
@ -208,9 +192,7 @@ describe('channel reducer', () => {
]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
@ -240,10 +222,7 @@ describe('channel reducer', () => {
it('handles SOCKET_SERVERS', () => {
const state = reducer(undefined, {
type: actions.socket.SERVERS,
data: [
{ host: '127.0.0.1' },
{ host: 'thehost' }
]
data: [{ host: '127.0.0.1' }, { host: 'thehost' }]
});
expect(state.toJS()).toEqual({
@ -280,7 +259,8 @@ describe('channel reducer', () => {
function socket_join(server, channel, user) {
return {
type: actions.socket.JOIN,
server, user,
server,
user,
channels: [channel]
};
}
@ -288,29 +268,35 @@ function socket_join(server, channel, user) {
function socket_mode(server, channel, user, add, remove) {
return {
type: actions.socket.MODE,
server, channel, user, add, remove
server,
channel,
user,
add,
remove
};
}
describe('compareUsers()', () => {
it('compares users correctly', () => {
expect([
{ renderName: 'user5' },
{ renderName: '@user2' },
{ renderName: 'user3' },
{ renderName: 'user2' },
{ renderName: '+user1' },
{ renderName: '~bob' },
{ renderName: '%apples' },
{ renderName: '&cake' }
].sort(compareUsers)).toEqual([
expect(
[
{ renderName: 'user5' },
{ renderName: '@user2' },
{ renderName: 'user3' },
{ renderName: 'user2' },
{ renderName: '+user1' },
{ renderName: '~bob' },
{ renderName: '%apples' },
{ renderName: '&cake' }
].sort(compareUsers)
).toEqual([
{ renderName: '~bob' },
{ renderName: '&cake' },
{ renderName: '@user2' },
{ renderName: '%apples' },
{ renderName: '+user1' },
{ renderName: '+user1' },
{ renderName: 'user2' },
{ renderName: 'user3' },
{ renderName: 'user3' },
{ renderName: 'user5' }
]);
});

View File

@ -17,10 +17,12 @@ describe('message reducer', () => {
expect(state.toJS()).toMatchObject({
srv: {
'#chan1': [{
from: 'foo',
content: 'msg'
}]
'#chan1': [
{
from: 'foo',
content: 'msg'
}
]
}
});
});
@ -34,10 +36,12 @@ describe('message reducer', () => {
{
from: 'foo',
content: 'msg'
}, {
},
{
from: 'bar',
content: 'msg'
}, {
},
{
tab: '#chan2',
from: 'foo',
content: 'msg'
@ -51,15 +55,18 @@ describe('message reducer', () => {
{
from: 'foo',
content: 'msg'
}, {
},
{
from: 'bar',
content: 'msg'
}
],
'#chan2': [{
from: 'foo',
content: 'msg'
}]
'#chan2': [
{
from: 'foo',
content: 'msg'
}
]
}
});
});
@ -92,10 +99,9 @@ describe('message reducer', () => {
};
const thunk = broadcast('test', 'srv', ['#chan1', '#chan3']);
thunk(
action => { state.messages = reducer(undefined, action); },
() => state
);
thunk(action => {
state.messages = reducer(undefined, action);
}, () => state);
const messages = state.messages.toJS();
@ -109,18 +115,11 @@ describe('message reducer', () => {
it('deletes all messages related to server when disconnecting', () => {
let state = fromJS({
srv: {
'#chan1': [
{ content: 'msg1' },
{ content: 'msg2' }
],
'#chan2': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
'#chan2': [{ content: 'msg' }]
},
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
@ -131,9 +130,7 @@ describe('message reducer', () => {
expect(state.toJS()).toEqual({
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
});
@ -141,18 +138,11 @@ describe('message reducer', () => {
it('deletes all messages related to channel when parting', () => {
let state = fromJS({
srv: {
'#chan1': [
{ content: 'msg1' },
{ content: 'msg2' }
],
'#chan2': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
'#chan2': [{ content: 'msg' }]
},
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
@ -164,14 +154,10 @@ describe('message reducer', () => {
expect(state.toJS()).toEqual({
srv: {
'#chan2': [
{ content: 'msg' }
]
'#chan2': [{ content: 'msg' }]
},
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
});

View File

@ -32,9 +32,12 @@ describe('server reducer', () => {
}
});
state = reducer(state, connect('127.0.0.2:1337', 'nick', {
name: 'srv'
}));
state = reducer(
state,
connect('127.0.0.2:1337', 'nick', {
name: 'srv'
})
);
expect(state.toJS()).toEqual({
'127.0.0.1': {
@ -190,7 +193,7 @@ describe('server reducer', () => {
status: {
connected: false
}
},
}
]
});

View File

@ -8,9 +8,7 @@ describe('tab reducer', () => {
expect(state.toJS()).toEqual({
selected: { server: 'srv', name: '#chan' },
history: [
{ server: 'srv', name: '#chan' }
]
history: [{ server: 'srv', name: '#chan' }]
});
state = reducer(state, setSelectedTab('srv', 'user1'));
@ -62,7 +60,7 @@ describe('tab reducer', () => {
history: [
{ server: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan3' }
{ server: 'srv', name: '#chan3' }
]
});
});
@ -75,14 +73,12 @@ describe('tab reducer', () => {
state = reducer(state, {
type: actions.DISCONNECT,
server: 'srv',
server: 'srv'
});
expect(state.toJS()).toEqual({
selected: { server: 'srv', name: '#chan3' },
history: [
{ server: 'srv1', name: 'bob' },
]
history: [{ server: 'srv1', name: 'bob' }]
});
});
@ -93,14 +89,13 @@ describe('tab reducer', () => {
expect(state.toJS()).toEqual({
selected: { server: null, name: null },
history: [
{ server: 'srv', name: '#chan' }
]
history: [{ server: 'srv', name: '#chan' }]
});
});
it('selects the tab and adds it to history when navigating to a tab', () => {
const state = reducer(undefined,
const state = reducer(
undefined,
locationChanged('chat', {
server: 'srv',
name: '#chan'
@ -109,9 +104,7 @@ describe('tab reducer', () => {
expect(state.toJS()).toEqual({
selected: { server: 'srv', name: '#chan' },
history: [
{ server: 'srv', name: '#chan' }
]
history: [{ server: 'srv', name: '#chan' }]
});
});
});

View File

@ -29,11 +29,13 @@ function updateRenderName(user) {
}
function createUser(nick, mode) {
return updateRenderName(new User({
nick,
renderName: nick,
mode: mode || ''
}));
return updateRenderName(
new User({
nick,
renderName: nick,
mode: mode || ''
})
);
}
function loadUser(nick) {
@ -80,13 +82,14 @@ export const getChannels = state => state.channels;
const key = (v, k) => k.toLowerCase();
export const getSortedChannels = createSelector(
getChannels,
channels => channels.withMutations(c =>
c.forEach((server, address) =>
c.update(address, chans => chans.sortBy(key))
export const getSortedChannels = createSelector(getChannels, channels =>
channels
.withMutations(c =>
c.forEach((server, address) =>
c.update(address, chans => chans.sortBy(key))
)
)
).sortBy(key)
.sortBy(key)
);
export const getSelectedChannel = createSelector(
@ -125,7 +128,9 @@ export default createReducer(Map(), {
[actions.socket.QUIT](state, { server, user }) {
return state.withMutations(s => {
s.get(server).forEach((v, channel) => {
s.updateIn([server, channel, 'users'], users => users.filter(u => u.nick !== user));
s.updateIn([server, channel, 'users'], users =>
users.filter(u => u.nick !== user)
);
});
});
},
@ -139,8 +144,8 @@ export default createReducer(Map(), {
return users;
}
return users.update(i,
user => updateRenderName(user.set('nick', newNick))
return users.update(i, user =>
updateRenderName(user.set('nick', newNick))
);
});
});
@ -148,7 +153,8 @@ export default createReducer(Map(), {
},
[actions.socket.USERS](state, { server, channel, users }) {
return state.setIn([server, channel, 'users'],
return state.setIn(
[server, channel, 'users'],
List(users.map(user => loadUser(user)))
);
},
@ -183,10 +189,13 @@ export default createReducer(Map(), {
return state.withMutations(s => {
data.forEach(channel => {
s.setIn([channel.server, channel.name], Map({
users: List(),
topic: channel.topic
}));
s.setIn(
[channel.server, channel.name],
Map({
users: List(),
topic: channel.topic
})
);
});
});
},

View File

@ -1,6 +1,12 @@
import { List, Map, Record } from 'immutable';
import { createSelector } from 'reselect';
import { findBreakpoints, messageHeight, linkify, timestamp, isChannel } from 'utils';
import {
findBreakpoints,
messageHeight,
linkify,
timestamp,
isChannel
} from 'utils';
import createReducer from 'utils/createReducer';
import { getApp } from './app';
import { getSelectedTab } from './tab';
@ -24,7 +30,8 @@ export const getMessages = state => state.messages;
export const getSelectedMessages = createSelector(
getSelectedTab,
getMessages,
(tab, messages) => messages.getIn([tab.server, tab.name || tab.server], List())
(tab, messages) =>
messages.getIn([tab.server, tab.name || tab.server], List())
);
export const getHasMoreMessages = createSelector(
@ -37,18 +44,24 @@ export const getHasMoreMessages = createSelector(
export default createReducer(Map(), {
[actions.ADD_MESSAGE](state, { server, tab, message }) {
return state.updateIn([server, tab], List(), list => list.push(new Message(message)));
return state.updateIn([server, tab], List(), list =>
list.push(new Message(message))
);
},
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) {
return state.withMutations(s => {
if (prepend) {
for (let i = messages.length - 1; i >= 0; i--) {
s.updateIn([server, tab], List(), list => list.unshift(new Message(messages[i])));
s.updateIn([server, tab], List(), list =>
list.unshift(new Message(messages[i]))
);
}
} else {
messages.forEach(message =>
s.updateIn([server, message.tab || tab], List(), list => list.push(new Message(message)))
s.updateIn([server, message.tab || tab], List(), list =>
list.push(new Message(message))
)
);
}
});
@ -60,18 +73,28 @@ export default createReducer(Map(), {
[actions.PART](state, { server, channels }) {
return state.withMutations(s =>
channels.forEach(channel =>
s.deleteIn([server, channel])
)
channels.forEach(channel => s.deleteIn([server, channel]))
);
},
[actions.UPDATE_MESSAGE_HEIGHT](state, { wrapWidth, charWidth, windowWidth }) {
[actions.UPDATE_MESSAGE_HEIGHT](
state,
{ wrapWidth, charWidth, windowWidth }
) {
return state.withMutations(s =>
s.forEach((server, serverKey) =>
server.forEach((target, targetKey) =>
target.forEach((message, index) => s.setIn([serverKey, targetKey, index, 'height'],
messageHeight(message, wrapWidth, charWidth, 6 * charWidth, windowWidth))
target.forEach((message, index) =>
s.setIn(
[serverKey, targetKey, index, 'height'],
messageHeight(
message,
wrapWidth,
charWidth,
6 * charWidth,
windowWidth
)
)
)
)
)
@ -111,7 +134,13 @@ function initMessage(message, tab, state) {
message.length = message.content.length;
message.breakpoints = findBreakpoints(message.content);
message.height = messageHeight(message, wrapWidth, charWidth, 6 * charWidth, windowWidth);
message.height = messageHeight(
message,
wrapWidth,
charWidth,
6 * charWidth,
windowWidth
);
message.content = linkify(message.content);
return message;
@ -175,10 +204,14 @@ export function sendMessage(content, to, server) {
type: actions.ADD_MESSAGE,
server,
tab: to,
message: initMessage({
from: state.servers.getIn([server, 'nick']),
content
}, to, state),
message: initMessage(
{
from: state.servers.getIn([server, 'nick']),
content
},
to,
state
),
socket: {
type: 'message',
data: { content, to, server }
@ -190,12 +223,13 @@ export function sendMessage(content, to, server) {
export function addMessage(message, server, to) {
const tab = getMessageTab(server, to);
return (dispatch, getState) => dispatch({
type: actions.ADD_MESSAGE,
server,
tab,
message: initMessage(message, tab, getState())
});
return (dispatch, getState) =>
dispatch({
type: actions.ADD_MESSAGE,
server,
tab,
message: initMessage(message, tab, getState())
});
}
export function addMessages(messages, server, to, prepend, next) {
@ -209,7 +243,9 @@ export function addMessages(messages, server, to, prepend, next) {
messages[0].next = true;
}
messages.forEach(message => initMessage(message, message.tab || tab, state));
messages.forEach(message =>
initMessage(message, message.tab || tab, state)
);
dispatch({
type: actions.ADD_MESSAGES,
@ -222,25 +258,36 @@ export function addMessages(messages, server, to, prepend, next) {
}
export function broadcast(message, server, channels) {
return addMessages(channels.map(channel => ({
tab: channel,
content: message,
type: 'info'
})), server);
return addMessages(
channels.map(channel => ({
tab: channel,
content: message,
type: 'info'
})),
server
);
}
export function print(message, server, channel, type) {
if (Array.isArray(message)) {
return addMessages(message.map(line => ({
content: line,
type
})), server, channel);
return addMessages(
message.map(line => ({
content: line,
type
})),
server,
channel
);
}
return addMessage({
content: message,
type
}, server, channel);
return addMessage(
{
content: message,
type
},
server,
channel
);
}
export function inform(message, server, channel) {

View File

@ -10,11 +10,12 @@ const lowerCaseValue = v => v.toLowerCase();
export const getSortedPrivateChats = createSelector(
getPrivateChats,
privateChats => privateChats.withMutations(p =>
p.forEach((server, address) =>
p.update(address, chats => chats.sortBy(lowerCaseValue))
privateChats =>
privateChats.withMutations(p =>
p.forEach((server, address) =>
p.update(address, chats => chats.sortBy(lowerCaseValue))
)
)
)
);
function open(state, server, nick) {

View File

@ -45,10 +45,13 @@ export const getCurrentServerStatus = createSelector(
export default createReducer(Map(), {
[actions.CONNECT](state, { host, nick, options }) {
if (!state.has(host)) {
return state.set(host, new Server({
nick,
name: options.name || host
}));
return state.set(
host,
new Server({
nick,
name: options.name || host
})
);
}
return state;
@ -73,9 +76,8 @@ export default createReducer(Map(), {
[actions.socket.NICK](state, { server, oldNick, newNick }) {
if (!oldNick || oldNick === state.get(server).nick) {
return state.update(server, s => s
.set('nick', newNick)
.set('editedNick', null)
return state.update(server, s =>
s.set('nick', newNick).set('editedNick', null)
);
}
return state;

View File

@ -40,19 +40,29 @@ export default createReducer(new State(), {
[actions.SELECT_TAB]: selectTab,
[actions.PART](state, action) {
return state.set('history', state.history.filter(tab =>
!(tab.server === action.server && tab.name === action.channels[0])
));
return state.set(
'history',
state.history.filter(
tab =>
!(tab.server === action.server && tab.name === action.channels[0])
)
);
},
[actions.CLOSE_PRIVATE_CHAT](state, action) {
return state.set('history', state.history.filter(tab =>
!(tab.server === action.server && tab.name === action.nick)
));
return state.set(
'history',
state.history.filter(
tab => !(tab.server === action.server && tab.name === action.nick)
)
);
},
[actions.DISCONNECT](state, action) {
return state.set('history', state.history.filter(tab => tab.server !== action.server));
return state.set(
'history',
state.history.filter(tab => tab.server !== action.server)
);
},
[LOCATION_CHANGED](state, action) {

View File

@ -7,20 +7,24 @@ import createSocketMiddleware from './middleware/socket';
import commands from './commands';
export default function configureStore(socket) {
// eslint-disable-next-line no-underscore-dangle
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
/* eslint-disable no-underscore-dangle */
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const reducer = createReducer(routeReducer);
const store = createStore(reducer, composeEnhancers(
applyMiddleware(
thunk,
routeMiddleware,
createSocketMiddleware(socket),
message,
commands
const store = createStore(
reducer,
composeEnhancers(
applyMiddleware(
thunk,
routeMiddleware,
createSocketMiddleware(socket),
message,
commands
)
)
));
);
return store;
}

View File

@ -50,7 +50,7 @@ export default class Socket {
this.retry();
};
this.ws.onmessage = (e) => {
this.ws.onmessage = e => {
this.setTimeoutPing();
const msg = JSON.parse(e.data);

View File

@ -2,55 +2,40 @@ import React from 'react';
import linkify from '../linkify';
describe('linkify()', () => {
const proto = href => href.indexOf('http') !== 0 ? `http://${href}` : href;
const linkTo = href => <a href={proto(href)} rel="noopener noreferrer" target="_blank">{href}</a>;
const proto = href => (href.indexOf('http') !== 0 ? `http://${href}` : href);
const linkTo = href => (
<a href={proto(href)} rel="noopener noreferrer" target="_blank">
{href}
</a>
);
it('returns the arg when no matches are found', () => [
null,
undefined,
10,
false,
true,
'just some text',
''
].forEach(input => expect(linkify(input)).toBe(input)));
it('returns the arg when no matches are found', () =>
[null, undefined, 10, false, true, 'just some text', ''].forEach(input =>
expect(linkify(input)).toBe(input)
));
it('linkifies text', () => Object.entries({
'google.com': linkTo('google.com'),
'google.com stuff': [
linkTo('google.com'),
' stuff'
],
'cake google.com stuff': [
'cake ',
linkTo('google.com'),
' stuff'
],
'cake google.com stuff https://google.com': [
'cake ',
linkTo('google.com'),
' stuff ',
linkTo('https://google.com')
],
'cake google.com stuff pie https://google.com ': [
'cake ',
linkTo('google.com'),
' stuff pie ',
linkTo('https://google.com'),
' '
],
' google.com': [
' ',
linkTo('google.com')
],
'google.com ': [
linkTo('google.com'),
' '
],
'/google.com?': [
'/',
linkTo('google.com'),
'?'
]
}).forEach(([ input, expected ]) => expect(linkify(input)).toEqual(expected)));
it('linkifies text', () =>
Object.entries({
'google.com': linkTo('google.com'),
'google.com stuff': [linkTo('google.com'), ' stuff'],
'cake google.com stuff': ['cake ', linkTo('google.com'), ' stuff'],
'cake google.com stuff https://google.com': [
'cake ',
linkTo('google.com'),
' stuff ',
linkTo('https://google.com')
],
'cake google.com stuff pie https://google.com ': [
'cake ',
linkTo('google.com'),
' stuff pie ',
linkTo('https://google.com'),
' '
],
' google.com': [' ', linkTo('google.com')],
'google.com ': [linkTo('google.com'), ' '],
'/google.com?': ['/', linkTo('google.com'), '?']
}).forEach(([input, expected]) =>
expect(linkify(input)).toEqual(expected)
));
});

View File

@ -8,7 +8,10 @@ export function normalizeChannel(channel) {
return channel;
}
return channel.split('#').join('').toLowerCase();
return channel
.split('#')
.join('')
.toLowerCase();
}
export function isChannel(name) {

View File

@ -30,12 +30,19 @@ export default function linkify(text) {
}
result.push(
<a target="_blank" rel="noopener noreferrer" href={match.getAnchorHref()}>
<a
target="_blank"
rel="noopener noreferrer"
href={match.getAnchorHref()}
>
{match.matchedText}
</a>
);
} else if (typeof result[result.length - 1] === 'string') {
result[result.length - 1] += text.slice(pos, match.offset + match.matchedText.length);
result[result.length - 1] += text.slice(
pos,
match.offset + match.matchedText.length
);
} else {
result.push(text.slice(pos, match.offset + match.matchedText.length));
}

View File

@ -18,7 +18,13 @@ export function findBreakpoints(text) {
return breakpoints;
}
export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowWidth) {
export function messageHeight(
message,
wrapWidth,
charWidth,
indent = 0,
windowWidth
) {
let pad = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
let height = lineHeight + 8;
@ -26,7 +32,7 @@ export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowW
wrapWidth -= userListWidth;
}
if (pad + (message.length * charWidth) < wrapWidth) {
if (pad + message.length * charWidth < wrapWidth) {
return height;
}
@ -35,7 +41,7 @@ export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowW
let prevPos = 0;
for (let i = 0; i < breaks.length; i++) {
if (pad + ((breaks[i].end - prevBreak) * charWidth) >= wrapWidth) {
if (pad + (breaks[i].end - prevBreak) * charWidth >= wrapWidth) {
prevBreak = prevPos;
pad = indent;
height += lineHeight;
@ -44,7 +50,7 @@ export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowW
prevPos = breaks[i].next;
}
if (pad + ((message.length - prevBreak) * charWidth) >= wrapWidth) {
if (pad + (message.length - prevBreak) * charWidth >= wrapWidth) {
height += lineHeight;
}

View File

@ -99,7 +99,10 @@ export default function initRouter(routes, store) {
history.listen(location => {
const nextMatch = match(patterns, location);
if (nextMatch && nextMatch.location.pathname !== matched.location.pathname) {
if (
nextMatch &&
nextMatch.location.pathname !== matched.location.pathname
) {
matched = nextMatch;
store.dispatch(matched);
}

View File

@ -3,10 +3,7 @@ var webpack = require('webpack');
module.exports = {
mode: 'development',
entry: [
'webpack-hot-middleware/client',
'./src/js/index'
],
entry: ['webpack-hot-middleware/client', './src/js/index'],
output: {
filename: 'bundle.js',
publicPath: '/'
@ -21,12 +18,18 @@ module.exports = {
},
module: {
rules: [
{ test: /\.js$/, loader: 'eslint-loader', exclude: /node_modules/, enforce: 'pre' },
{
test: /\.js$/,
loader: 'eslint-loader',
exclude: /node_modules/,
enforce: 'pre',
options: {
fix: true
}
},
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
{ test: /\.css$/, loader: 'style-loader!css-loader' }
]
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
plugins: [new webpack.HotModuleReplacementPlugin()]
};

View File

@ -3,9 +3,7 @@ var webpack = require('webpack');
module.exports = {
mode: 'production',
entry: [
'./src/js/index'
],
entry: ['./src/js/index'],
output: {
filename: 'bundle.js'
},
@ -19,7 +17,15 @@ module.exports = {
},
module: {
rules: [
{ test: /\.js$/, loader: 'eslint-loader', exclude: /node_modules/, enforce: 'pre' },
{
test: /\.js$/,
loader: 'eslint-loader',
exclude: /node_modules/,
enforce: 'pre',
options: {
fix: true
}
},
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
{ test: /\.css$/, loader: 'style-loader!css-loader' }
]

View File

@ -2581,6 +2581,12 @@ eslint-config-airbnb@^16.1.0:
dependencies:
eslint-config-airbnb-base "^12.1.0"
eslint-config-prettier@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz#5ecd65174d486c22dff389fe036febf502d468a3"
dependencies:
get-stdin "^5.0.1"
eslint-import-resolver-node@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
@ -2648,6 +2654,13 @@ eslint-plugin-jsx-a11y@^6.0.3:
emoji-regex "^6.1.0"
jsx-ast-utils "^2.0.0"
eslint-plugin-prettier@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz#33e4e228bdb06142d03c560ce04ec23f6c767dd7"
dependencies:
fast-diff "^1.1.1"
jest-docblock "^21.0.0"
eslint-plugin-react@^7.7.0:
version "7.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz#f606c719dbd8a1a2b3d25c16299813878cca0160"
@ -2939,6 +2952,10 @@ fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
fast-diff@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@ -3241,6 +3258,10 @@ get-caller-file@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
get-stdin@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@ -4267,6 +4288,10 @@ jest-diff@^22.4.3:
jest-get-type "^22.4.3"
pretty-format "^22.4.3"
jest-docblock@^21.0.0:
version "21.2.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
jest-docblock@^22.4.3:
version "22.4.3"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.4.3.tgz#50886f132b42b280c903c592373bb6e93bb68b19"
@ -5885,6 +5910,10 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
pretty-format@^22.4.3:
version "22.4.3"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f"