Switch to redux and webpack
This commit is contained in:
parent
b247287075
commit
e389454535
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
@ -15,7 +15,7 @@ install:
|
|||||||
- npm install
|
- npm install
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- gulp -p
|
- gulp build
|
||||||
- cd ..
|
- cd ..
|
||||||
- go vet ./...
|
- go vet ./...
|
||||||
- go test -v -race ./...
|
- go test -v -race ./...
|
||||||
|
10
README.md
10
README.md
@ -54,8 +54,12 @@ npm install
|
|||||||
|
|
||||||
Run the build:
|
Run the build:
|
||||||
```bash
|
```bash
|
||||||
gulp -p
|
gulp build
|
||||||
```
|
```
|
||||||
|
|
||||||
The server needs to be rebuilt after this. For development dropping the -p flag
|
The server needs to be rebuilt after this.
|
||||||
will turn off minification and embedding, requiring only one initial server rebuild.
|
|
||||||
|
For development with hot reloading enabled just run:
|
||||||
|
```bash
|
||||||
|
gulp
|
||||||
|
```
|
||||||
|
File diff suppressed because one or more lines are too long
20
client/.babelrc
Normal file
20
client/.babelrc
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"presets": ["react", "es2015", "stage-0"],
|
||||||
|
"plugins": ["transform-decorators-legacy"],
|
||||||
|
"env": {
|
||||||
|
"development": {
|
||||||
|
"plugins": [
|
||||||
|
["react-transform", {
|
||||||
|
"transforms": [{
|
||||||
|
"transform": "react-transform-hmr",
|
||||||
|
"imports": ["react"],
|
||||||
|
"locals": ["module"]
|
||||||
|
}, {
|
||||||
|
"transform": "react-transform-catch-errors",
|
||||||
|
"imports": ["react", "redbox-react"]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,14 @@
|
|||||||
{
|
{
|
||||||
"ecmaFeatures": {
|
"extends": "airbnb",
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"quotes": [2, "single"],
|
"comma-dangle": [2, "never"],
|
||||||
"strict": [2, "never"],
|
"new-cap": [2, { "capIsNewExceptions": ["Map", "List", "Record", "Set"] }],
|
||||||
"eol-last": [0],
|
"no-console": 0,
|
||||||
"new-cap": [2, { "capIsNew": false }],
|
"no-param-reassign": 0,
|
||||||
"react/jsx-uses-react": 2,
|
"react/prop-types": 0
|
||||||
"react/jsx-uses-vars": 2,
|
},
|
||||||
"react/react-in-jsx-scope": 2
|
"globals": {
|
||||||
|
"__DEV__": true
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,37 +1,23 @@
|
|||||||
|
var path = require('path');
|
||||||
var exec = require('child_process').exec;
|
var exec = require('child_process').exec;
|
||||||
|
|
||||||
var gulp = require('gulp');
|
var gulp = require('gulp');
|
||||||
var gutil = require('gulp-util');
|
var gutil = require('gulp-util');
|
||||||
var gulpif = require('gulp-if');
|
var htmlmin = require('gulp-htmlmin');
|
||||||
var minifyHTML = require('gulp-minify-html');
|
var nano = require('gulp-cssnano');
|
||||||
var minifyCSS = require('gulp-minify-css');
|
|
||||||
var autoprefixer = require('gulp-autoprefixer');
|
var autoprefixer = require('gulp-autoprefixer');
|
||||||
var uglify = require('gulp-uglify');
|
|
||||||
var gzip = require('gulp-gzip');
|
var gzip = require('gulp-gzip');
|
||||||
var concat = require('gulp-concat');
|
var concat = require('gulp-concat');
|
||||||
var eslint = require('gulp-eslint');
|
|
||||||
var browserify = require('browserify');
|
|
||||||
var source = require('vinyl-source-stream');
|
|
||||||
var streamify = require('gulp-streamify');
|
|
||||||
var babelify = require('babelify');
|
|
||||||
var strictify = require('strictify');
|
|
||||||
var watchify = require('watchify');
|
|
||||||
var merge = require('merge-stream');
|
|
||||||
var cache = require('gulp-cached');
|
var cache = require('gulp-cached');
|
||||||
|
var express = require('express');
|
||||||
var argv = require('yargs')
|
var webpack = require('webpack');
|
||||||
.alias('p', 'production')
|
|
||||||
.argv;
|
|
||||||
|
|
||||||
if (argv.production) {
|
|
||||||
process.env['NODE_ENV'] = 'production';
|
|
||||||
}
|
|
||||||
|
|
||||||
var deps = Object.keys(require('./package.json').dependencies);
|
|
||||||
|
|
||||||
gulp.task('html', function() {
|
gulp.task('html', function() {
|
||||||
return gulp.src('src/*.html')
|
return gulp.src('src/*.html')
|
||||||
.pipe(minifyHTML())
|
.pipe(htmlmin({
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeAttributeQuotes: true
|
||||||
|
}))
|
||||||
.pipe(gulp.dest('dist'));
|
.pipe(gulp.dest('dist'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,64 +25,24 @@ gulp.task('css', function() {
|
|||||||
return gulp.src(['src/css/fontello.css', 'src/css/style.css'])
|
return gulp.src(['src/css/fontello.css', 'src/css/style.css'])
|
||||||
.pipe(concat('bundle.css'))
|
.pipe(concat('bundle.css'))
|
||||||
.pipe(autoprefixer())
|
.pipe(autoprefixer())
|
||||||
.pipe(minifyCSS())
|
.pipe(nano())
|
||||||
.pipe(gulp.dest('dist'));
|
.pipe(gulp.dest('dist'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('js', function() {
|
gulp.task('js', function(cb) {
|
||||||
return js(false);
|
var config = require('./webpack.config.prod.js');
|
||||||
});
|
var compiler = webpack(config);
|
||||||
|
|
||||||
function js(watch) {
|
process.env['NODE_ENV'] = 'production';
|
||||||
var bundler = browserify('./src/js/app.js', {
|
|
||||||
debug: !argv.production,
|
compiler.run(function(err, stats) {
|
||||||
transform: [
|
if (err) throw new gutil.PluginError('webpack', err);
|
||||||
babelify.configure({
|
|
||||||
presets: ['es2015', 'react']
|
gutil.log('[webpack]', stats.toString({
|
||||||
}),
|
colors: true
|
||||||
strictify
|
}));
|
||||||
],
|
cb();
|
||||||
cache: {},
|
|
||||||
packageCache: {},
|
|
||||||
fullPaths: watch
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bundler.external(deps);
|
|
||||||
|
|
||||||
var rebundle = function() {
|
|
||||||
return bundler.bundle()
|
|
||||||
.on('error', gutil.log)
|
|
||||||
.pipe(source('bundle.js'))
|
|
||||||
.pipe(gulpif(argv.production, streamify(uglify())))
|
|
||||||
.pipe(gulp.dest('dist'));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (watch) {
|
|
||||||
bundler = watchify(bundler);
|
|
||||||
bundler.on('update', rebundle);
|
|
||||||
bundler.on('log', gutil.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
var vendorBundler = browserify({
|
|
||||||
debug: !argv.production,
|
|
||||||
require: deps
|
|
||||||
});
|
|
||||||
|
|
||||||
var vendor = vendorBundler.bundle()
|
|
||||||
.on('error', gutil.log)
|
|
||||||
.pipe(source('vendor.js'))
|
|
||||||
.pipe(gulpif(argv.production, streamify(uglify())))
|
|
||||||
.pipe(gulp.dest('dist'));
|
|
||||||
|
|
||||||
return merge(rebundle(), vendor);
|
|
||||||
}
|
|
||||||
|
|
||||||
gulp.task('lint', function() {
|
|
||||||
return gulp.src('src/js/**/*.{js,jsx}')
|
|
||||||
.pipe(cache('lint'))
|
|
||||||
.pipe(eslint())
|
|
||||||
.pipe(eslint.format())
|
|
||||||
.pipe(eslint.failOnError());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('fonts', function() {
|
gulp.task('fonts', function() {
|
||||||
@ -109,36 +55,54 @@ gulp.task('config', function() {
|
|||||||
.pipe(gulp.dest('dist/gz'));
|
.pipe(gulp.dest('dist/gz'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('gzip', ['html', 'css', 'js', 'fonts'], function() {
|
function compress() {
|
||||||
return gulp.src(['dist/**/!(*.gz)', '!dist/{gz,gz/**}'])
|
return gulp.src(['dist/**/!(*.gz)', '!dist/{gz,gz/**}'])
|
||||||
.pipe(gzip())
|
.pipe(gzip())
|
||||||
.pipe(gulp.dest('dist/gz'));
|
.pipe(gulp.dest('dist/gz'));
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task('gzip:watch', function() {
|
|
||||||
return gulp.src('dist/**/*.{html,css,js}')
|
|
||||||
.pipe(cache('gzip'))
|
|
||||||
.pipe(gzip())
|
|
||||||
.pipe(gulp.dest('dist/gz'));
|
|
||||||
});
|
|
||||||
|
|
||||||
function bindata(cb) {
|
|
||||||
if (argv.production) {
|
|
||||||
exec('go-bindata -nomemcopy -nocompress -pkg assets -o ../assets/bindata.go -prefix "dist/gz" dist/gz/...', cb);
|
|
||||||
} else {
|
|
||||||
exec('go-bindata -debug -pkg assets -o ../assets/bindata.go -prefix "dist/gz" dist/gz/...', cb);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gulp.task('bindata', ['gzip', 'config'], bindata);
|
gulp.task('gzip', ['html', 'css', 'js', 'fonts'], compress);
|
||||||
gulp.task('bindata:watch', ['gzip:watch'], bindata);
|
gulp.task('gzip:dev', ['html', 'css', 'fonts'], compress);
|
||||||
|
|
||||||
gulp.task('watch', ['default'], function() {
|
gulp.task('bindata', ['gzip', 'config'], function(cb) {
|
||||||
gulp.watch('dist/**/*.{html,css,js}', ['gzip:watch', 'bindata:watch'])
|
exec('go-bindata -nomemcopy -nocompress -pkg assets -o ../assets/bindata.go -prefix "dist/gz" dist/gz/...', cb);
|
||||||
gulp.watch('src/*.html', ['html']);
|
|
||||||
gulp.watch('src/css/*.css', ['css']);
|
|
||||||
gulp.watch('src/js/**/*.{js,jsx}', ['lint']);
|
|
||||||
return js(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('default', ['html', 'css', 'js', 'lint', 'fonts', 'config', 'gzip', 'bindata']);
|
gulp.task('bindata:dev', ['gzip:dev', 'config'], function(cb) {
|
||||||
|
exec('go-bindata -debug -pkg assets -o ../assets/bindata.go -prefix "dist/gz" dist/gz/...', cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('dev', ['html', 'css', 'fonts', 'config', 'gzip:dev', 'bindata:dev'], function() {
|
||||||
|
gulp.watch('src/*.html', ['html']);
|
||||||
|
gulp.watch('src/css/*.css', ['css']);
|
||||||
|
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(require('webpack-hot-middleware')(compiler));
|
||||||
|
|
||||||
|
app.use('/', express.static('dist'));
|
||||||
|
|
||||||
|
app.get('*', function (req, res) {
|
||||||
|
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000, 'localhost', function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Listening at http://localhost:3000');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('build', ['html', 'css', 'js', 'fonts', 'config', 'gzip', 'bindata']);
|
||||||
|
|
||||||
|
gulp.task('default', ['dev']);
|
||||||
|
@ -4,43 +4,53 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.3.26",
|
||||||
"babel-eslint": "^5.0.0-beta6",
|
"babel-eslint": "^5.0.0-beta6",
|
||||||
|
"babel-loader": "^6.2.0",
|
||||||
|
"babel-plugin-react-transform": "^2.0.0",
|
||||||
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
"babel-preset-es2015": "^6.3.13",
|
"babel-preset-es2015": "^6.3.13",
|
||||||
"babel-preset-react": "^6.3.13",
|
"babel-preset-react": "^6.3.13",
|
||||||
"babelify": "7.2.0",
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
"browserify": "12.0.1",
|
|
||||||
"eslint": "^1.10.3",
|
"eslint": "^1.10.3",
|
||||||
"eslint-plugin-react": "^3.11.3",
|
"eslint-config-airbnb": "^2.1.1",
|
||||||
|
"eslint-loader": "^1.1.1",
|
||||||
|
"eslint-plugin-react": "^3.13.1",
|
||||||
|
"express": "^4.13.3",
|
||||||
"gulp": "~3.9.0",
|
"gulp": "~3.9.0",
|
||||||
"gulp-autoprefixer": "3.1.0",
|
"gulp-autoprefixer": "3.1.0",
|
||||||
"gulp-cached": "^1.1.0",
|
"gulp-cached": "^1.1.0",
|
||||||
"gulp-concat": "~2.6.0",
|
"gulp-concat": "~2.6.0",
|
||||||
"gulp-eslint": "^1.1.1",
|
"gulp-cssnano": "^2.0.0",
|
||||||
"gulp-gzip": "1.2.0",
|
"gulp-gzip": "1.2.0",
|
||||||
"gulp-if": "~2.0.0",
|
"gulp-htmlmin": "^1.3.0",
|
||||||
"gulp-minify-css": "1.2.2",
|
"gulp-util": "^3.0.7",
|
||||||
"gulp-minify-html": "1.0.4",
|
"react-transform-catch-errors": "^1.0.1",
|
||||||
"gulp-streamify": "1.0.2",
|
"react-transform-hmr": "^1.0.1",
|
||||||
"gulp-uglify": "1.5.1",
|
"redbox-react": "^1.2.0",
|
||||||
"gulp-util": "^3.0.5",
|
"redux-devtools": "^3.0.1",
|
||||||
"merge-stream": "^1.0.0",
|
"redux-devtools-dock-monitor": "^1.0.1",
|
||||||
"reactify": "^1.1.1",
|
"redux-devtools-log-monitor": "^1.0.1",
|
||||||
"strictify": "~0.2.0",
|
"webpack": "^1.12.9",
|
||||||
"vinyl-source-stream": "~1.1.0",
|
"webpack-dev-middleware": "^1.4.0",
|
||||||
"watchify": "3.6.1",
|
"webpack-hot-middleware": "^2.6.0"
|
||||||
"yargs": "~3.31.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"autolinker": "khlieng/Autolinker.js",
|
"autolinker": "^0.22.0",
|
||||||
"backo": "^1.1.0",
|
"backo": "^1.1.0",
|
||||||
"history": "1.13.1",
|
"eventemitter2": "^0.4.14",
|
||||||
"immutable": "^3.7.5",
|
"history": "^1.17.0",
|
||||||
"lodash": "3.10.1",
|
"immutable": "^3.7.6",
|
||||||
|
"lodash": "^3.10.1",
|
||||||
|
"pure-render-decorator": "^0.2.0",
|
||||||
"react": "^0.14.3",
|
"react": "^0.14.3",
|
||||||
"react-dom": "^0.14.3",
|
"react-dom": "^0.14.3",
|
||||||
"react-infinite": "0.7.2",
|
"react-infinite": "0.7.3",
|
||||||
"react-pure-render": "~1.0.1",
|
"react-redux": "^4.0.5",
|
||||||
"react-router": "^1.0.1",
|
"react-router": "^1.0.3",
|
||||||
"reflux": "0.3.0"
|
"redux": "^3.0.5",
|
||||||
|
"redux-simple-router": "^1.0.2",
|
||||||
|
"redux-thunk": "^1.0.2",
|
||||||
|
"reselect": "^2.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,13 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title>IRC</title>
|
<title>Dispatch</title>
|
||||||
|
|
||||||
<link href="//fonts.googleapis.com/css?family=Montserrat|Droid+Sans+Mono" rel="stylesheet">
|
<link href="//fonts.googleapis.com/css?family=Montserrat|Droid+Sans+Mono" rel="stylesheet">
|
||||||
<link href="bundle.css" rel="stylesheet">
|
<link href="/bundle.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script src="vendor.js"></script>
|
<script src="/bundle.js"></script>
|
||||||
<script src="bundle.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
37
client/src/js/actions.js
Normal file
37
client/src/js/actions.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export const ADD_MESSAGE = 'ADD_MESSAGE';
|
||||||
|
export const ADD_MESSAGES = 'ADD_MESSAGES';
|
||||||
|
export const AWAY = 'AWAY';
|
||||||
|
export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT';
|
||||||
|
export const COMMAND = 'COMMAND';
|
||||||
|
export const CONNECT = 'CONNECT';
|
||||||
|
export const DISCONNECT = 'DISCONNECT';
|
||||||
|
export const HIDE_MENU = 'HIDE_MENU';
|
||||||
|
export const INPUT_HISTORY_ADD = 'INPUT_HISTORY_ADD';
|
||||||
|
export const INPUT_HISTORY_DECREMENT = 'INPUT_HISTORY_DECREMENT';
|
||||||
|
export const INPUT_HISTORY_INCREMENT = 'INPUT_HISTORY_INCREMENT';
|
||||||
|
export const INPUT_HISTORY_RESET = 'INPUT_HISTORY_RESET';
|
||||||
|
export const INVITE = 'INVITE';
|
||||||
|
export const JOIN = 'JOIN';
|
||||||
|
export const KICK = 'KICK';
|
||||||
|
export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT';
|
||||||
|
export const PART = 'PART';
|
||||||
|
export const SEARCH_MESSAGES = 'SEARCH_MESSAGES';
|
||||||
|
export const SELECT_TAB = 'SELECT_TAB';
|
||||||
|
export const SEND_MESSAGE = 'SEND_MESSAGE';
|
||||||
|
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';
|
||||||
|
export const SET_NICK = 'SET_NICK';
|
||||||
|
export const SOCKET_CHANNELS = 'SOCKET_CHANNELS';
|
||||||
|
export const SOCKET_JOIN = 'SOCKET_JOIN';
|
||||||
|
export const SOCKET_MESSAGE = 'SOCKET_MESSAGE';
|
||||||
|
export const SOCKET_MODE = 'SOCKET_MODE';
|
||||||
|
export const SOCKET_NICK = 'SOCKET_NICK';
|
||||||
|
export const SOCKET_PART = 'SOCKET_PART';
|
||||||
|
export const SOCKET_PM = 'SOCKET_PM';
|
||||||
|
export const SOCKET_QUIT = 'SOCKET_QUIT';
|
||||||
|
export const SOCKET_SERVERS = 'SOCKET_SERVERS';
|
||||||
|
export const SOCKET_TOPIC = 'SOCKET_TOPIC';
|
||||||
|
export const SOCKET_USERS = 'SOCKET_USERS';
|
||||||
|
export const TAB_HISTORY_POP = 'TAB_HISTORY_POP';
|
||||||
|
export const TOGGLE_MENU = 'TOGGLE_MENU';
|
||||||
|
export const TOGGLE_SEARCH = 'TOGGLE_SEARCH';
|
||||||
|
export const WHOIS = 'WHOIS';
|
@ -1,36 +1,55 @@
|
|||||||
var Reflux = require('reflux');
|
import * as actions from '../actions';
|
||||||
|
import { updateSelection } from './tab';
|
||||||
|
|
||||||
var socket = require('../socket');
|
export function join(channels, server) {
|
||||||
|
return {
|
||||||
|
type: actions.JOIN,
|
||||||
|
channels,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'join',
|
||||||
|
data: { channels, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var channelActions = Reflux.createActions([
|
export function part(channels, server) {
|
||||||
'join',
|
return dispatch => {
|
||||||
'part',
|
dispatch({
|
||||||
'invite',
|
type: actions.PART,
|
||||||
'kick',
|
channels,
|
||||||
'addUser',
|
server,
|
||||||
'removeUser',
|
socket: {
|
||||||
'removeUserAll',
|
type: 'part',
|
||||||
'renameUser',
|
data: { channels, server }
|
||||||
'setUsers',
|
}
|
||||||
'setTopic',
|
});
|
||||||
'setMode',
|
dispatch(updateSelection());
|
||||||
'load'
|
};
|
||||||
]);
|
}
|
||||||
|
|
||||||
channelActions.join.preEmit = (channels, server) => {
|
export function invite(user, channel, server) {
|
||||||
socket.send('join', { server, channels });
|
return {
|
||||||
};
|
type: actions.INVITE,
|
||||||
|
user,
|
||||||
|
channel,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'invite',
|
||||||
|
data: { user, channel, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
channelActions.part.preEmit = (channels, server) => {
|
export function kick(user, channel, server) {
|
||||||
socket.send('part', { server, channels });
|
return {
|
||||||
};
|
type: actions.KICK,
|
||||||
|
user,
|
||||||
channelActions.invite.preEmit = (user, channel, server) => {
|
channel,
|
||||||
socket.send('invite', { server, channel, user });
|
server,
|
||||||
};
|
socket: {
|
||||||
|
type: 'kick',
|
||||||
channelActions.kick.preEmit = (user, channel, server) => {
|
data: { user, channel, server }
|
||||||
socket.send('kick', { server, channel, user });
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
module.exports = channelActions;
|
|
||||||
|
17
client/src/js/actions/environment.js
Normal file
17
client/src/js/actions/environment.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
export function setEnvironment(key, value) {
|
||||||
|
return {
|
||||||
|
type: actions.SET_ENVIRONMENT,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWrapWidth(width) {
|
||||||
|
return setEnvironment('wrapWidth', width);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCharWidth(width) {
|
||||||
|
return setEnvironment('charWidth', width);
|
||||||
|
}
|
@ -1,10 +1,26 @@
|
|||||||
var Reflux = require('reflux');
|
import * as actions from '../actions';
|
||||||
|
|
||||||
var inputHistoryActions = Reflux.createActions([
|
export function addInputHistory(line) {
|
||||||
'add',
|
return {
|
||||||
'reset',
|
type: actions.INPUT_HISTORY_ADD,
|
||||||
'increment',
|
line
|
||||||
'decrement'
|
};
|
||||||
]);
|
}
|
||||||
|
|
||||||
module.exports = inputHistoryActions;
|
export function resetInputHistory() {
|
||||||
|
return {
|
||||||
|
type: actions.INPUT_HISTORY_RESET
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementInputHistory() {
|
||||||
|
return {
|
||||||
|
type: actions.INPUT_HISTORY_INCREMENT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrementInputHistory() {
|
||||||
|
return {
|
||||||
|
type: actions.INPUT_HISTORY_DECREMENT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,19 +1,77 @@
|
|||||||
var Reflux = require('reflux');
|
import * as actions from '../actions';
|
||||||
|
|
||||||
var socket = require('../socket');
|
export function sendMessage(message, to, server) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return dispatch({
|
||||||
|
type: actions.SEND_MESSAGE,
|
||||||
|
from: getState().servers.getIn([server, 'nick']),
|
||||||
|
message,
|
||||||
|
to,
|
||||||
|
server,
|
||||||
|
time: new Date(),
|
||||||
|
socket: {
|
||||||
|
type: 'chat',
|
||||||
|
data: { message, to, server }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var messageActions = Reflux.createActions([
|
export function addMessage(message) {
|
||||||
'send',
|
message.time = new Date();
|
||||||
'add',
|
|
||||||
'addAll',
|
|
||||||
'broadcast',
|
|
||||||
'inform',
|
|
||||||
'command',
|
|
||||||
'setWrapWidth'
|
|
||||||
]);
|
|
||||||
|
|
||||||
messageActions.send.preEmit = (message, to, server) => {
|
return {
|
||||||
socket.send('chat', { server, to, message });
|
type: actions.ADD_MESSAGE,
|
||||||
};
|
message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = messageActions;
|
export function addMessages(messages) {
|
||||||
|
const now = new Date();
|
||||||
|
messages.forEach(message => message.time = now);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: actions.ADD_MESSAGES,
|
||||||
|
messages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcast(message, server, channels) {
|
||||||
|
return addMessages(channels.map(channel => {
|
||||||
|
return {
|
||||||
|
server,
|
||||||
|
to: channel,
|
||||||
|
message,
|
||||||
|
type: 'info'
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inform(message, server, channel) {
|
||||||
|
if (Array.isArray(message)) {
|
||||||
|
return addMessages(message.map(msg => {
|
||||||
|
return {
|
||||||
|
server,
|
||||||
|
to: channel,
|
||||||
|
message: msg,
|
||||||
|
type: 'info'
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return addMessage({
|
||||||
|
server,
|
||||||
|
to: channel,
|
||||||
|
message,
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runCommand(command, channel, server) {
|
||||||
|
return {
|
||||||
|
type: actions.COMMAND,
|
||||||
|
command,
|
||||||
|
channel,
|
||||||
|
server
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,8 +1,21 @@
|
|||||||
var Reflux = require('reflux');
|
import * as actions from '../actions';
|
||||||
|
import { updateSelection } from './tab';
|
||||||
|
|
||||||
var privateChatActions = Reflux.createActions([
|
export function openPrivateChat(server, nick) {
|
||||||
'open',
|
return {
|
||||||
'close'
|
type: actions.OPEN_PRIVATE_CHAT,
|
||||||
]);
|
server,
|
||||||
|
nick
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = privateChatActions;
|
export function closePrivateChat(server, nick) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: actions.CLOSE_PRIVATE_CHAT,
|
||||||
|
server,
|
||||||
|
nick
|
||||||
|
});
|
||||||
|
dispatch(updateSelection());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
|
|
||||||
var routeActions = Reflux.createActions([
|
|
||||||
'navigate'
|
|
||||||
]);
|
|
||||||
|
|
||||||
module.exports = routeActions;
|
|
@ -1,15 +1,20 @@
|
|||||||
var Reflux = require('reflux');
|
import * as actions from '../actions';
|
||||||
|
|
||||||
var socket = require('../socket');
|
export function searchMessages(server, channel, phrase) {
|
||||||
|
return {
|
||||||
|
type: actions.SEARCH_MESSAGES,
|
||||||
|
server,
|
||||||
|
channel,
|
||||||
|
phrase,
|
||||||
|
socket: {
|
||||||
|
type: 'search',
|
||||||
|
data: { server, channel, phrase }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var searchActions = Reflux.createActions([
|
export function toggleSearch() {
|
||||||
'search',
|
return {
|
||||||
'searchDone',
|
type: actions.TOGGLE_SEARCH
|
||||||
'toggle'
|
};
|
||||||
]);
|
}
|
||||||
|
|
||||||
searchActions.search.preEmit = (server, channel, phrase) => {
|
|
||||||
socket.send('search', { server, channel, phrase });
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = searchActions;
|
|
||||||
|
@ -1,45 +1,76 @@
|
|||||||
var Reflux = require('reflux');
|
import * as actions from '../actions';
|
||||||
|
import { updateSelection } from './tab';
|
||||||
|
|
||||||
var socket = require('../socket');
|
export function connect(server, nick, options) {
|
||||||
|
return {
|
||||||
var serverActions = Reflux.createActions([
|
type: actions.CONNECT,
|
||||||
'connect',
|
|
||||||
'disconnect',
|
|
||||||
'whois',
|
|
||||||
'away',
|
|
||||||
'setNick',
|
|
||||||
'load'
|
|
||||||
]);
|
|
||||||
|
|
||||||
serverActions.connect.preEmit = (server, nick, opts) => {
|
|
||||||
socket.send('connect', {
|
|
||||||
server,
|
server,
|
||||||
nick,
|
nick,
|
||||||
username: opts.username || nick,
|
options,
|
||||||
password: opts.password,
|
socket: {
|
||||||
realname: opts.realname || nick,
|
type: 'connect',
|
||||||
tls: opts.tls || false,
|
data: {
|
||||||
name: opts.name || server
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
serverActions.disconnect.preEmit = (server) => {
|
|
||||||
socket.send('quit', { server });
|
|
||||||
};
|
|
||||||
|
|
||||||
serverActions.whois.preEmit = (user, server) => {
|
|
||||||
socket.send('whois', { server, user });
|
|
||||||
};
|
|
||||||
|
|
||||||
serverActions.away.preEmit = (message, server) => {
|
|
||||||
socket.send('away', { server, message });
|
|
||||||
};
|
|
||||||
|
|
||||||
serverActions.setNick.preEmit = (nick, server) => {
|
|
||||||
socket.send('nick', {
|
|
||||||
server,
|
server,
|
||||||
new: nick
|
nick,
|
||||||
});
|
username: options.username || nick,
|
||||||
};
|
password: options.password,
|
||||||
|
realname: options.realname || nick,
|
||||||
|
tls: options.tls || false,
|
||||||
|
name: options.name || server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = serverActions;
|
export function disconnect(server) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: actions.DISCONNECT,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'quit',
|
||||||
|
data: { server }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dispatch(updateSelection());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function whois(user, server) {
|
||||||
|
return {
|
||||||
|
type: actions.WHOIS,
|
||||||
|
user,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'whois',
|
||||||
|
data: { user, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function away(message, server) {
|
||||||
|
return {
|
||||||
|
type: actions.AWAY,
|
||||||
|
message,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'away',
|
||||||
|
data: { message, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNick(nick, server) {
|
||||||
|
return {
|
||||||
|
type: actions.SET_NICK,
|
||||||
|
nick,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'nick',
|
||||||
|
data: {
|
||||||
|
new: nick,
|
||||||
|
server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,9 +1,60 @@
|
|||||||
var Reflux = require('reflux');
|
import { pushPath } from 'redux-simple-router';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
var tabActions = Reflux.createActions([
|
export function select(server, channel, pm) {
|
||||||
'select',
|
if (pm) {
|
||||||
'hideMenu',
|
return pushPath(`/${server}/pm/${channel}`);
|
||||||
'toggleMenu'
|
} else if (channel) {
|
||||||
]);
|
return pushPath(`/${server}/${encodeURIComponent(channel)}`);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = tabActions;
|
return pushPath(`/${server}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSelection() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const history = state.tab.history;
|
||||||
|
const { servers } = state;
|
||||||
|
const { server } = state.tab.selected;
|
||||||
|
|
||||||
|
if (servers.size === 0) {
|
||||||
|
dispatch(pushPath('/connect'));
|
||||||
|
} else if (history.size > 0) {
|
||||||
|
const tab = history.last();
|
||||||
|
dispatch(select(tab.server, tab.channel || tab.user, tab.user));
|
||||||
|
} else if (servers.has(server)) {
|
||||||
|
dispatch(select(server));
|
||||||
|
} else {
|
||||||
|
dispatch(pushPath('/'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSelectedChannel(server, channel = null) {
|
||||||
|
return {
|
||||||
|
type: actions.SELECT_TAB,
|
||||||
|
server,
|
||||||
|
channel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSelectedUser(server, user = null) {
|
||||||
|
return {
|
||||||
|
type: actions.SELECT_TAB,
|
||||||
|
server,
|
||||||
|
user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideMenu() {
|
||||||
|
return {
|
||||||
|
type: actions.HIDE_MENU
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleMenu() {
|
||||||
|
return {
|
||||||
|
type: actions.TOGGLE_MENU
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
import { Router, Route, IndexRoute } from 'react-router';
|
|
||||||
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
|
||||||
import './irc';
|
|
||||||
import './command';
|
|
||||||
import socket from './socket';
|
|
||||||
import util from './util';
|
|
||||||
import App from './components/App.jsx';
|
|
||||||
import Connect from './components/Connect.jsx';
|
|
||||||
import Chat from './components/Chat.jsx';
|
|
||||||
import Settings from './components/Settings.jsx';
|
|
||||||
import routeActions from './actions/route';
|
|
||||||
|
|
||||||
let uuid = localStorage.uuid;
|
|
||||||
if (!uuid) {
|
|
||||||
routeActions.navigate('connect', true);
|
|
||||||
localStorage.uuid = uuid = util.UUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on('connect', () => socket.send('uuid', uuid));
|
|
||||||
socket.on('error', error => console.log(error.server + ': ' + error.message));
|
|
||||||
|
|
||||||
const routes = (
|
|
||||||
<Route path="/" component={App}>
|
|
||||||
<Route path="connect" component={Connect} />
|
|
||||||
<Route path="settings" component={Settings} />
|
|
||||||
<Route path="/:server" component={Chat} />
|
|
||||||
<Route path="/:server/:channel" component={Chat} />
|
|
||||||
<IndexRoute component={Settings} />
|
|
||||||
</Route>
|
|
||||||
);
|
|
||||||
const history = createBrowserHistory();
|
|
||||||
|
|
||||||
render(<Router routes={routes} history={history} />, document.getElementById('root'));
|
|
@ -1,114 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var channelStore = require('./stores/channel');
|
|
||||||
var channelActions = require('./actions/channel');
|
|
||||||
var messageActions = require('./actions/message');
|
|
||||||
var serverActions = require('./actions/server');
|
|
||||||
var tabActions = require('./actions/tab');
|
|
||||||
|
|
||||||
messageActions.command.listen(function(line, channel, server) {
|
|
||||||
var params = line.slice(1).split(' ');
|
|
||||||
var command = params[0].toLowerCase();
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case 'nick':
|
|
||||||
if (params[1]) {
|
|
||||||
serverActions.setNick(params[1], server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'quit':
|
|
||||||
serverActions.disconnect(server);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'join':
|
|
||||||
if (params[1]) {
|
|
||||||
channelActions.join([params[1]], server);
|
|
||||||
tabActions.select(server, params[1]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'part':
|
|
||||||
if (params[1]) {
|
|
||||||
channelActions.part([params[1]], server);
|
|
||||||
} else if (channel) {
|
|
||||||
channelActions.part([channel], server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'me':
|
|
||||||
if (params.length > 1) {
|
|
||||||
messageActions.send('\x01ACTION ' + params.slice(1).join(' ') + '\x01', channel, server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'topic':
|
|
||||||
let topic = channelStore.getTopic(server, channel);
|
|
||||||
if (topic) {
|
|
||||||
messageActions.add({
|
|
||||||
server: server,
|
|
||||||
to: channel,
|
|
||||||
message: topic
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
messageActions.inform('No topic set', server, channel);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'invite':
|
|
||||||
if (params[1] && params[2] && server) {
|
|
||||||
channelActions.invite(params[1], params[2], server);
|
|
||||||
} else if (params[1] && channel) {
|
|
||||||
channelActions.invite(params[1], channel, server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'kick':
|
|
||||||
if (params[1] && channel) {
|
|
||||||
channelActions.kick(params[1], channel, server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
case 'msg':
|
|
||||||
if (params.length > 2) {
|
|
||||||
let dest = params[1];
|
|
||||||
let message = params.slice(2).join(' ');
|
|
||||||
|
|
||||||
messageActions.send(message, dest, server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'say':
|
|
||||||
if (params.length > 1) {
|
|
||||||
let message = params.slice(1).join(' ');
|
|
||||||
|
|
||||||
messageActions.send(message, channel, server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'whois':
|
|
||||||
if (params[1]) {
|
|
||||||
serverActions.whois(params[1], server);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'away':
|
|
||||||
serverActions.away(params[1], server);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'help':
|
|
||||||
messageActions.inform([
|
|
||||||
_.escape('/join <channel> - Join a channel'),
|
|
||||||
'/part [channel] - Leave the current or entered channel',
|
|
||||||
_.escape('/nick <nick> - Change nick'),
|
|
||||||
'/quit - Disconnect from the current server',
|
|
||||||
_.escape('/me <message> - Send action message'),
|
|
||||||
'/topic - Show topic for the current channel',
|
|
||||||
_.escape('/msg <target> <message> - Send message to the entered channel or user'),
|
|
||||||
_.escape('/say <message> - Send message to the current chat'),
|
|
||||||
'/away [message] - Set or clear away message'
|
|
||||||
], server, channel);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
108
client/src/js/commands.js
Normal file
108
client/src/js/commands.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import createCommandMiddleware from './middleware/command';
|
||||||
|
import { COMMAND } from './actions';
|
||||||
|
import { setNick, disconnect, whois, away } from './actions/server';
|
||||||
|
import { join, part, invite, kick } from './actions/channel';
|
||||||
|
import { select } from './actions/tab';
|
||||||
|
import { sendMessage, addMessage, inform } from './actions/message';
|
||||||
|
|
||||||
|
const help = [
|
||||||
|
'/join <channel> - Join a channel',
|
||||||
|
'/part [channel] - Leave the current or specified channel',
|
||||||
|
'/nick <nick> - Change nick',
|
||||||
|
'/quit - Disconnect from the current server',
|
||||||
|
'/me <message> - Send action message',
|
||||||
|
'/topic - Show topic for the current channel',
|
||||||
|
'/msg <target> <message> - Send message to the specified channel or user',
|
||||||
|
'/say <message> - Send message to the current chat',
|
||||||
|
'/invite <user> [channel] - Invite user to the current or specified channel',
|
||||||
|
'/kick <user> - Kick user from the current channel',
|
||||||
|
'/whois <user> - Get information about user',
|
||||||
|
'/away [message] - Set or clear away message'
|
||||||
|
].map(_.escape);
|
||||||
|
|
||||||
|
export default createCommandMiddleware(COMMAND, {
|
||||||
|
join({ dispatch, server }, channel) {
|
||||||
|
if (channel) {
|
||||||
|
dispatch(join([channel], server));
|
||||||
|
dispatch(select(server, channel));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
part({ dispatch, server, channel }, partChannel) {
|
||||||
|
if (partChannel) {
|
||||||
|
dispatch(part([partChannel], server));
|
||||||
|
} else {
|
||||||
|
dispatch(part([channel], server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
nick({ dispatch, server }, nick) {
|
||||||
|
if (nick) {
|
||||||
|
dispatch(setNick(nick, server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
quit({ dispatch, server }) {
|
||||||
|
dispatch(disconnect(server));
|
||||||
|
},
|
||||||
|
|
||||||
|
me({ dispatch, server, channel }, ...params) {
|
||||||
|
if (params.length > 0) {
|
||||||
|
dispatch(sendMessage(`\x01ACTION ${params.join(' ')}\x01`, channel, server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
topic({ dispatch, getState, server, channel }) {
|
||||||
|
const topic = getState().channels.getIn([server, channel, 'topic']);
|
||||||
|
if (topic) {
|
||||||
|
dispatch(addMessage({
|
||||||
|
server,
|
||||||
|
to: channel,
|
||||||
|
message: topic
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(inform('No topic set', server, channel));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
msg({ dispatch, server }, target, ...message) {
|
||||||
|
if (target && message) {
|
||||||
|
dispatch(sendMessage(message.join(' '), target, server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
say({ dispatch, server, channel }, ...message) {
|
||||||
|
if (channel && message) {
|
||||||
|
dispatch(sendMessage(message.join(' '), channel, server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
invite({ dispatch, server, channel }, user, inviteChannel) {
|
||||||
|
if (user && inviteChannel) {
|
||||||
|
dispatch(invite(user, inviteChannel, server));
|
||||||
|
} else if (user && channel) {
|
||||||
|
dispatch(invite(user, channel, server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
kick({ dispatch, server, channel }, user) {
|
||||||
|
if (user && channel) {
|
||||||
|
dispatch(kick(user, channel, server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
whois({ dispatch, server }, user) {
|
||||||
|
if (user) {
|
||||||
|
dispatch(whois(user, server));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
away({ dispatch, server }, message) {
|
||||||
|
dispatch(away(message, server));
|
||||||
|
},
|
||||||
|
|
||||||
|
help({ dispatch, server, channel }) {
|
||||||
|
dispatch(inform(help, server, channel));
|
||||||
|
}
|
||||||
|
});
|
@ -1,52 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Reflux from 'reflux';
|
|
||||||
import { Router } from 'react-router';
|
|
||||||
import TabList from './TabList.jsx';
|
|
||||||
import routeActions from '../actions/route';
|
|
||||||
import tabActions from '../actions/tab';
|
|
||||||
import PureMixin from '../mixins/pure';
|
|
||||||
|
|
||||||
export default React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.listenTo(routeActions.navigate, 'navigate'),
|
|
||||||
Reflux.listenTo(tabActions.hideMenu, 'hideMenu'),
|
|
||||||
Reflux.listenTo(tabActions.toggleMenu, 'toggleMenu')
|
|
||||||
],
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
menuToggled: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
navigate(path, replace) {
|
|
||||||
const { history } = this.props;
|
|
||||||
if (!replace) {
|
|
||||||
history.pushState(null, path);
|
|
||||||
} else {
|
|
||||||
history.replaceState(null, path);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hideMenu() {
|
|
||||||
this.setState({ menuToggled: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleMenu() {
|
|
||||||
this.setState({ menuToggled: !this.state.menuToggled });
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const mainClass = this.state.menuToggled ? 'main-container off-canvas' : 'main-container';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TabList menuToggled={this.state.menuToggled} />
|
|
||||||
<div className={mainClass}>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,53 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Reflux from 'reflux';
|
|
||||||
import Router from 'react-router';
|
|
||||||
import ChatTitle from './ChatTitle.jsx';
|
|
||||||
import Search from './Search.jsx';
|
|
||||||
import MessageBox from './MessageBox.jsx';
|
|
||||||
import MessageInput from './MessageInput.jsx';
|
|
||||||
import UserList from './UserList.jsx';
|
|
||||||
import selectedTabStore from '../stores/selectedTab';
|
|
||||||
import tabActions from '../actions/tab';
|
|
||||||
import PureMixin from '../mixins/pure';
|
|
||||||
|
|
||||||
export default React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Router.State,
|
|
||||||
Reflux.connect(selectedTabStore, 'selectedTab')
|
|
||||||
],
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
if (!window.loaded) {
|
|
||||||
const { params } = this.props;
|
|
||||||
if (params.channel) {
|
|
||||||
tabActions.select(params.server, '#' + params.channel);
|
|
||||||
} else if (params.server) {
|
|
||||||
tabActions.select(params.server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let chatClass;
|
|
||||||
const tab = this.state.selectedTab;
|
|
||||||
|
|
||||||
if (!tab.channel) {
|
|
||||||
chatClass = 'chat-server';
|
|
||||||
} else if (tab.channel[0] !== '#') {
|
|
||||||
chatClass = 'chat-private';
|
|
||||||
} else {
|
|
||||||
chatClass = 'chat-channel';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={chatClass}>
|
|
||||||
<ChatTitle />
|
|
||||||
<Search />
|
|
||||||
<MessageBox />
|
|
||||||
<MessageInput />
|
|
||||||
<UserList />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
56
client/src/js/components/ChatTitle.js
Normal file
56
client/src/js/components/ChatTitle.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
import Autolinker from 'autolinker';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import Navicon from '../components/Navicon';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class ChatTitle extends Component {
|
||||||
|
handleLeaveClick = () => {
|
||||||
|
const { tab, channel, disconnect, part, closePrivateChat } = this.props;
|
||||||
|
|
||||||
|
if (tab.channel) {
|
||||||
|
part([channel.get('name')], tab.server);
|
||||||
|
} else if (tab.user) {
|
||||||
|
closePrivateChat(tab.server, tab.user);
|
||||||
|
} else {
|
||||||
|
disconnect(tab.server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { title, tab, channel, toggleSearch } = this.props;
|
||||||
|
const topic = Autolinker.link(channel.get('topic') || '', { keepOriginalText: true });
|
||||||
|
|
||||||
|
let leaveTitle;
|
||||||
|
if (tab.channel) {
|
||||||
|
leaveTitle = 'Leave';
|
||||||
|
} else if (tab.user) {
|
||||||
|
leaveTitle = 'Close';
|
||||||
|
} else {
|
||||||
|
leaveTitle = 'Disconnect';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="chat-title-bar">
|
||||||
|
<Navicon />
|
||||||
|
<span className="chat-title">{title}</span>
|
||||||
|
<div className="chat-topic-wrap">
|
||||||
|
<span className="chat-topic" dangerouslySetInnerHTML={{ __html: topic }}></span>
|
||||||
|
</div>
|
||||||
|
<i className="icon-search" title="Search" onClick={toggleSearch} />
|
||||||
|
<i
|
||||||
|
className="icon-logout button-leave"
|
||||||
|
title={leaveTitle}
|
||||||
|
onClick={this.handleLeaveClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="userlist-bar">
|
||||||
|
<i className="icon-user" />
|
||||||
|
<span className="chat-usercount">{channel.get('users', List()).size || null}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,89 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Reflux = require('reflux');
|
|
||||||
var Autolinker = require('autolinker');
|
|
||||||
|
|
||||||
var Navicon = require('./Navicon.jsx');
|
|
||||||
var channelStore = require('../stores/channel');
|
|
||||||
var selectedTabStore = require('../stores/selectedTab');
|
|
||||||
var serverActions = require('../actions/server');
|
|
||||||
var channelActions = require('../actions/channel');
|
|
||||||
var searchActions = require('../actions/search');
|
|
||||||
var privateChatActions = require('../actions/privateChat');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
function buildState(tab) {
|
|
||||||
return {
|
|
||||||
selectedTab: tab,
|
|
||||||
usercount: channelStore.getUsers(tab.server, tab.channel).size,
|
|
||||||
topic: channelStore.getTopic(tab.server, tab.channel)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var ChatTitle = React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.listenTo(channelStore, 'channelsChanged'),
|
|
||||||
Reflux.listenTo(selectedTabStore, 'selectedTabChanged')
|
|
||||||
],
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return buildState(selectedTabStore.getState());
|
|
||||||
},
|
|
||||||
|
|
||||||
channelsChanged() {
|
|
||||||
this.setState(buildState(this.state.selectedTab));
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedTabChanged(tab) {
|
|
||||||
this.setState(buildState(tab));
|
|
||||||
},
|
|
||||||
|
|
||||||
handleLeaveClick() {
|
|
||||||
var tab = this.state.selectedTab;
|
|
||||||
|
|
||||||
if (!tab.channel) {
|
|
||||||
serverActions.disconnect(tab.server);
|
|
||||||
} else if (tab.channel[0] === '#') {
|
|
||||||
channelActions.part([tab.channel], tab.server);
|
|
||||||
} else {
|
|
||||||
privateChatActions.close(tab.server, tab.channel);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var tab = this.state.selectedTab;
|
|
||||||
var topic = Autolinker.link(this.state.topic || '', { keepOriginalText: true });
|
|
||||||
var leaveTitle;
|
|
||||||
|
|
||||||
if (!tab.channel) {
|
|
||||||
leaveTitle = 'Disconnect';
|
|
||||||
} else if (tab.channel[0] !== '#') {
|
|
||||||
leaveTitle = 'Close';
|
|
||||||
} else {
|
|
||||||
leaveTitle = 'Leave';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="chat-title-bar">
|
|
||||||
<Navicon />
|
|
||||||
<span className="chat-title">{tab.name}</span>
|
|
||||||
<div className="chat-topic-wrap">
|
|
||||||
<span className="chat-topic" dangerouslySetInnerHTML={{ __html: topic }}></span>
|
|
||||||
</div>
|
|
||||||
<i className="icon-search" title="Search" onClick={searchActions.toggle}></i>
|
|
||||||
<i
|
|
||||||
className="icon-logout button-leave"
|
|
||||||
title={leaveTitle}
|
|
||||||
onClick={this.handleLeaveClick}></i>
|
|
||||||
</div>
|
|
||||||
<div className="userlist-bar">
|
|
||||||
<i className="icon-user"></i>
|
|
||||||
<span className="chat-usercount">{this.state.usercount || null}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = ChatTitle;
|
|
@ -1,82 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var Navicon = require('./Navicon.jsx');
|
|
||||||
var serverActions = require('../actions/server');
|
|
||||||
var channelActions = require('../actions/channel');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var Connect = React.createClass({
|
|
||||||
mixins: [PureMixin],
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
showOptionals: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
var address = e.target.address.value.trim();
|
|
||||||
var nick = e.target.nick.value.trim();
|
|
||||||
var channels = _.filter(_.map(e.target.channels.value.split(','), _.trim));
|
|
||||||
var opts = {
|
|
||||||
name: e.target.name.value.trim(),
|
|
||||||
tls: e.target.ssl.checked
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.showOptionals) {
|
|
||||||
opts.realname = e.target.realname.value.trim();
|
|
||||||
opts.username = e.target.username.value.trim();
|
|
||||||
opts.password = e.target.password.value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address.indexOf('.') > 0 && nick) {
|
|
||||||
serverActions.connect(address, nick, opts);
|
|
||||||
|
|
||||||
if (channels.length > 0) {
|
|
||||||
channelActions.join(channels, address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleShowClick: function() {
|
|
||||||
this.setState({ showOptionals: !this.state.showOptionals});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var optionals = null;
|
|
||||||
|
|
||||||
if (this.state.showOptionals) {
|
|
||||||
optionals = (
|
|
||||||
<div>
|
|
||||||
<input name="username" type="text" placeholder="Username" />
|
|
||||||
<input name="password" type="text" placeholder="Password" />
|
|
||||||
<input name="realname" type="text" placeholder="Realname" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="connect">
|
|
||||||
<Navicon />
|
|
||||||
<form ref="form" className="connect-form" onSubmit={this.handleSubmit}>
|
|
||||||
<h1>Connect</h1>
|
|
||||||
<input name="name" type="text" placeholder="Name" defaultValue="Freenode" />
|
|
||||||
<input name="address" type="text" placeholder="Address" defaultValue="irc.freenode.net" />
|
|
||||||
<input name="nick" type="text" placeholder="Nick" />
|
|
||||||
<input name="channels" type="text" placeholder="Channels" />
|
|
||||||
{optionals}
|
|
||||||
<p>
|
|
||||||
<label><input name="ssl" type="checkbox" />SSL</label>
|
|
||||||
<i className="icon-ellipsis" onClick={this.handleShowClick}></i>
|
|
||||||
</p>
|
|
||||||
<input type="submit" value="Connect" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Connect;
|
|
90
client/src/js/components/MessageBox.js
Normal file
90
client/src/js/components/MessageBox.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import Infinite from 'react-infinite';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import MessageHeader from './MessageHeader';
|
||||||
|
import MessageLine from './MessageLine';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class MessageBox extends Component {
|
||||||
|
state = {
|
||||||
|
height: window.innerHeight - 100
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateWidth();
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUpdate() {
|
||||||
|
const el = this.refs.list.refs.scrollable;
|
||||||
|
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
setTimeout(this.updateWidth, 0);
|
||||||
|
|
||||||
|
if (this.autoScroll) {
|
||||||
|
const el = this.refs.list.refs.scrollable;
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidth = () => {
|
||||||
|
const { setWrapWidth } = this.props;
|
||||||
|
const { list } = this.refs;
|
||||||
|
if (list) {
|
||||||
|
const width = list.refs.scrollable.offsetWidth - 30;
|
||||||
|
if (this.width !== width) {
|
||||||
|
this.width = width;
|
||||||
|
setWrapWidth(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
this.updateWidth();
|
||||||
|
this.setState({ height: window.innerHeight - 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tab, messages, select, openPrivateChat } = this.props;
|
||||||
|
const dest = tab.channel || tab.user || tab.server;
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
messages.forEach((message, j) => {
|
||||||
|
const key = message.server + dest + j;
|
||||||
|
lines.push(
|
||||||
|
<MessageHeader
|
||||||
|
key={key}
|
||||||
|
message={message}
|
||||||
|
select={select}
|
||||||
|
openPrivateChat={openPrivateChat}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 1; i < message.lines.length; i++) {
|
||||||
|
lines.push(
|
||||||
|
<MessageLine key={key + '-' + i} type={message.type} line={message.lines[i]} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="messagebox">
|
||||||
|
<Infinite
|
||||||
|
ref="list"
|
||||||
|
className="messagebox-scrollable"
|
||||||
|
containerHeight={this.state.height}
|
||||||
|
elementHeight={24}
|
||||||
|
displayBottomUpwards={false}
|
||||||
|
>
|
||||||
|
{lines}
|
||||||
|
</Infinite>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,93 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Reflux from 'reflux';
|
|
||||||
import Infinite from 'react-infinite';
|
|
||||||
import MessageHeader from './MessageHeader.jsx';
|
|
||||||
import MessageLine from './MessageLine.jsx';
|
|
||||||
import messageLineStore from '../stores/messageLine';
|
|
||||||
import selectedTabStore from '../stores/selectedTab';
|
|
||||||
import messageActions from '../actions/message';
|
|
||||||
import PureMixin from '../mixins/pure';
|
|
||||||
|
|
||||||
export default React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.connect(messageLineStore, 'messages'),
|
|
||||||
Reflux.connect(selectedTabStore, 'selectedTab')
|
|
||||||
],
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
height: window.innerHeight - 100
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.updateWidth();
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener('resize', this.handleResize);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUpdate() {
|
|
||||||
var el = this.refs.list.refs.scrollable;
|
|
||||||
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
setTimeout(this.updateWidth, 0);
|
|
||||||
|
|
||||||
if (this.autoScroll) {
|
|
||||||
var el = this.refs.list.refs.scrollable;
|
|
||||||
el.scrollTop = el.scrollHeight;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleResize() {
|
|
||||||
this.updateWidth();
|
|
||||||
this.setState({ height: window.innerHeight - 100 });
|
|
||||||
},
|
|
||||||
|
|
||||||
updateWidth() {
|
|
||||||
const { list } = this.refs;
|
|
||||||
if (list) {
|
|
||||||
const width = list.refs.scrollable.offsetWidth - 30;
|
|
||||||
if (this.width !== width) {
|
|
||||||
this.width = width;
|
|
||||||
messageActions.setWrapWidth(width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const tab = this.state.selectedTab;
|
|
||||||
const dest = tab.channel || tab.server;
|
|
||||||
const lines = [];
|
|
||||||
|
|
||||||
this.state.messages.forEach((message, j) => {
|
|
||||||
const key = message.server + dest + j;
|
|
||||||
|
|
||||||
lines.push(<MessageHeader key={key} message={message} />);
|
|
||||||
|
|
||||||
for (let i = 1; i < message.lines.length; i++) {
|
|
||||||
lines.push(
|
|
||||||
<MessageLine key={key + '-' + i} type={message.type} line={message.lines[i]} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="messagebox">
|
|
||||||
<Infinite
|
|
||||||
ref="list"
|
|
||||||
className="messagebox-scrollable"
|
|
||||||
containerHeight={this.state.height}
|
|
||||||
elementHeight={24}
|
|
||||||
displayBottomUpwards={false}>
|
|
||||||
{lines}
|
|
||||||
</Infinite>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
46
client/src/js/components/MessageHeader.js
Normal file
46
client/src/js/components/MessageHeader.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import Autolinker from 'autolinker';
|
||||||
|
import { timestamp } from '../util';
|
||||||
|
|
||||||
|
export default class MessageHeader extends Component {
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
return nextProps.message.lines[0] !== this.props.message.lines[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSenderClick = () => {
|
||||||
|
const { message, openPrivateChat, select } = this.props;
|
||||||
|
|
||||||
|
openPrivateChat(message.server, message.from);
|
||||||
|
select(message.server, message.from, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { message } = this.props;
|
||||||
|
const line = Autolinker.link(message.lines[0], { stripPrefix: false });
|
||||||
|
let sender = null;
|
||||||
|
let messageClass = 'message';
|
||||||
|
|
||||||
|
if (message.from) {
|
||||||
|
sender = (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
<span className="message-sender" onClick={this.handleSenderClick}>
|
||||||
|
{message.from}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type) {
|
||||||
|
messageClass += ' message-' + message.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className={messageClass}>
|
||||||
|
<span className="message-time">{timestamp(message.time)}</span>
|
||||||
|
{sender}
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: ' ' + line }}></span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,51 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Autolinker = require('autolinker');
|
|
||||||
|
|
||||||
var util = require('../util');
|
|
||||||
var privateChatActions = require('../actions/privateChat');
|
|
||||||
var tabActions = require('../actions/tab');
|
|
||||||
|
|
||||||
var MessageHeader = React.createClass({
|
|
||||||
shouldComponentUpdate(nextProps) {
|
|
||||||
return nextProps.message.lines[0] !== this.props.message.lines[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSenderClick() {
|
|
||||||
var message = this.props.message;
|
|
||||||
|
|
||||||
privateChatActions.open(message.server, message.from);
|
|
||||||
tabActions.select(message.server, message.from);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var message = this.props.message;
|
|
||||||
var sender = null;
|
|
||||||
var messageClass = 'message';
|
|
||||||
var line = Autolinker.link(message.lines[0], { keepOriginalText: true });
|
|
||||||
|
|
||||||
if (message.from) {
|
|
||||||
sender = (
|
|
||||||
<span>
|
|
||||||
{' '}
|
|
||||||
<span className="message-sender" onClick={this.handleSenderClick}>
|
|
||||||
{message.from}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type) {
|
|
||||||
messageClass += ' message-' + message.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className={messageClass}>
|
|
||||||
<span className="message-time">{util.timestamp(message.time)}</span>
|
|
||||||
{sender}
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: ' ' + line }}></span>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = MessageHeader;
|
|
57
client/src/js/components/MessageInput.js
Normal file
57
client/src/js/components/MessageInput.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class MessageInput extends Component {
|
||||||
|
state = {
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKey = e => {
|
||||||
|
const { tab, runCommand, sendMessage, addInputHistory, incrementInputHistory,
|
||||||
|
decrementInputHistory, resetInputHistory } = this.props;
|
||||||
|
|
||||||
|
if (e.which === 13 && e.target.value) {
|
||||||
|
if (e.target.value[0] === '/') {
|
||||||
|
runCommand(e.target.value, tab.channel || tab.user, tab.server);
|
||||||
|
} else if (tab.channel) {
|
||||||
|
sendMessage(e.target.value, tab.channel, tab.server);
|
||||||
|
} else if (tab.user) {
|
||||||
|
sendMessage(e.target.value, tab.user, tab.server);
|
||||||
|
}
|
||||||
|
|
||||||
|
addInputHistory(e.target.value);
|
||||||
|
resetInputHistory();
|
||||||
|
this.setState({ value: '' });
|
||||||
|
} else if (e.which === 38) {
|
||||||
|
e.preventDefault();
|
||||||
|
incrementInputHistory();
|
||||||
|
} else if (e.which === 40) {
|
||||||
|
decrementInputHistory();
|
||||||
|
} else if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
resetInputHistory();
|
||||||
|
} else if (e.key === 'Unidentified') {
|
||||||
|
this.setState({ value: e.target.value });
|
||||||
|
resetInputHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = e => {
|
||||||
|
this.setState({ value: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="message-input-wrap">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
className="message-input"
|
||||||
|
type="text"
|
||||||
|
value={this.props.history || this.state.value}
|
||||||
|
onKeyDown={this.handleKey}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,66 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Reflux = require('reflux');
|
|
||||||
|
|
||||||
var inputHistoryStore = require('../stores/inputHistory');
|
|
||||||
var selectedTabStore = require('../stores/selectedTab');
|
|
||||||
var messageActions = require('../actions/message');
|
|
||||||
var inputHistoryActions = require('../actions/inputHistory');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var MessageInput = React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.connect(inputHistoryStore, 'history')
|
|
||||||
],
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
handleKey(e) {
|
|
||||||
if (e.which === 13 && e.target.value) {
|
|
||||||
var tab = selectedTabStore.getState();
|
|
||||||
|
|
||||||
if (e.target.value[0] === '/') {
|
|
||||||
messageActions.command(e.target.value, tab.channel, tab.server);
|
|
||||||
} else {
|
|
||||||
messageActions.send(e.target.value, tab.channel, tab.server);
|
|
||||||
}
|
|
||||||
|
|
||||||
inputHistoryActions.add(e.target.value);
|
|
||||||
inputHistoryActions.reset();
|
|
||||||
this.setState({ value: '' });
|
|
||||||
} else if (e.which === 38) {
|
|
||||||
e.preventDefault();
|
|
||||||
inputHistoryActions.increment();
|
|
||||||
} else if (e.which === 40) {
|
|
||||||
inputHistoryActions.decrement();
|
|
||||||
} else if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
||||||
inputHistoryActions.reset();
|
|
||||||
} else if (e.key === 'Unidentified') {
|
|
||||||
inputHistoryActions.reset();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleChange(e) {
|
|
||||||
this.setState({ value: e.target.value });
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="message-input-wrap">
|
|
||||||
<input
|
|
||||||
ref="input"
|
|
||||||
className="message-input"
|
|
||||||
type="text"
|
|
||||||
value={this.state.history || this.state.value}
|
|
||||||
onKeyDown={this.handleKey}
|
|
||||||
onChange={this.handleChange} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = MessageInput;
|
|
25
client/src/js/components/MessageLine.js
Normal file
25
client/src/js/components/MessageLine.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import Autolinker from 'autolinker';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class MessageLine extends Component {
|
||||||
|
render() {
|
||||||
|
const line = Autolinker.link(this.props.line, { stripPrefix: false });
|
||||||
|
|
||||||
|
let messageClass = 'message';
|
||||||
|
if (this.props.type) {
|
||||||
|
messageClass += ' message-' + this.props.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
paddingLeft: window.messageIndent + 'px'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className={messageClass} style={style}>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: line }}></span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Autolinker = require('autolinker');
|
|
||||||
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var MessageLine = React.createClass({
|
|
||||||
mixins: [PureMixin],
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var line = Autolinker.link(this.props.line, { keepOriginalText: true });
|
|
||||||
var messageClass = 'message';
|
|
||||||
var style = {
|
|
||||||
paddingLeft: window.messageIndent + 'px'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.props.type) {
|
|
||||||
messageClass += ' message-' + this.props.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className={messageClass} style={style}>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: line }}></span>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = MessageLine;
|
|
16
client/src/js/components/Navicon.js
Normal file
16
client/src/js/components/Navicon.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import { toggleMenu } from '../actions/tab';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
class Navicon extends Component {
|
||||||
|
render() {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
return (
|
||||||
|
<i className="icon-menu navicon" onClick={() => dispatch(toggleMenu())}></i>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect()(Navicon);
|
@ -1,13 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
|
|
||||||
var tabActions = require('../actions/tab');
|
|
||||||
|
|
||||||
var Navicon = React.createClass({
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<i className="icon-menu navicon" onClick={tabActions.toggleMenu}></i>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Navicon;
|
|
37
client/src/js/components/Search.js
Normal file
37
client/src/js/components/Search.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import { timestamp } from '../util';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class Search extends Component {
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (!prevProps.search.show && this.props.search.show) {
|
||||||
|
this.refs.input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { search, onSearch } = this.props;
|
||||||
|
const results = search.results.map(result => {
|
||||||
|
return (
|
||||||
|
<p key={result.id}>{timestamp(new Date(result.time * 1000))} {result.from} {result.content}</p>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
display: search.show ? 'block' : 'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search" style={style}>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
className="search-input"
|
||||||
|
type="text"
|
||||||
|
onChange={e => onSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="search-results">{results}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Reflux = require('reflux');
|
|
||||||
|
|
||||||
var util = require('../util');
|
|
||||||
var searchStore = require('../stores/search');
|
|
||||||
var selectedTabStore = require('../stores/selectedTab');
|
|
||||||
var searchActions = require('../actions/search');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var Search = React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.connect(searchStore, 'search'),
|
|
||||||
Reflux.connect(selectedTabStore, 'selectedTab')
|
|
||||||
],
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (!prevState.search.show && this.state.search.show) {
|
|
||||||
this.refs.input.getDOMNode().focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleChange(e) {
|
|
||||||
var tab = this.state.selectedTab;
|
|
||||||
|
|
||||||
if (tab.channel) {
|
|
||||||
searchActions.search(tab.server, tab.channel, e.target.value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var style = {
|
|
||||||
display: this.state.search.show ? 'block' : 'none'
|
|
||||||
};
|
|
||||||
|
|
||||||
var results = this.state.search.results.map(result => {
|
|
||||||
return (
|
|
||||||
<p key={result.id}>{util.timestamp(new Date(result.time * 1000))} {result.from} {result.content}</p>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="search" style={style}>
|
|
||||||
<input
|
|
||||||
ref="input"
|
|
||||||
className="search-input"
|
|
||||||
type="text"
|
|
||||||
onChange={this.handleChange} />
|
|
||||||
<div className="search-results">{results}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Search;
|
|
14
client/src/js/components/Settings.js
Normal file
14
client/src/js/components/Settings.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import Navicon from './Navicon';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class Settings extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navicon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
|
|
||||||
var Navicon = require('./Navicon.jsx');
|
|
||||||
|
|
||||||
var Settings = React.createClass({
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Navicon />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Settings;
|
|
69
client/src/js/components/TabList.js
Normal file
69
client/src/js/components/TabList.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import TabListItem from './TabListItem';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class TabList extends Component {
|
||||||
|
handleConnectClick = () => {
|
||||||
|
this.props.pushPath('/connect');
|
||||||
|
this.props.hideMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSettingsClick = () => {
|
||||||
|
this.props.pushPath('/settings');
|
||||||
|
this.props.hideMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { channels, servers, privateChats, showMenu, select, selected } = this.props;
|
||||||
|
const className = showMenu ? 'tablist off-canvas' : 'tablist';
|
||||||
|
const tabs = [];
|
||||||
|
|
||||||
|
channels.forEach((server, address) => {
|
||||||
|
tabs.push(
|
||||||
|
<TabListItem
|
||||||
|
key={address}
|
||||||
|
server
|
||||||
|
content={servers.getIn([address, 'name'])}
|
||||||
|
selected={selected.server === address && selected.channel === null && selected.user === null}
|
||||||
|
onClick={() => select(address)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
server.forEach((channel, name) => {
|
||||||
|
tabs.push(
|
||||||
|
<TabListItem
|
||||||
|
key={address + channel.get('name')}
|
||||||
|
content={channel.get('name')}
|
||||||
|
selected={selected.server === address && selected.channel === name}
|
||||||
|
onClick={() => select(address, channel.get('name'))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (privateChats.has(address)) {
|
||||||
|
privateChats.get(address).forEach(nick => {
|
||||||
|
tabs.push(
|
||||||
|
<TabListItem
|
||||||
|
key={address + nick}
|
||||||
|
content={nick}
|
||||||
|
selected={selected.server === address && selected.user === nick}
|
||||||
|
onClick={() => select(address, nick, true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<button className="button-connect" onClick={this.handleConnectClick}>Connect</button>
|
||||||
|
<div className="tab-container">{tabs}</div>
|
||||||
|
<div className="side-buttons">
|
||||||
|
<i className="icon-user"></i>
|
||||||
|
<i className="icon-cog" onClick={this.handleSettingsClick}></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Reflux = require('reflux');
|
|
||||||
|
|
||||||
var TabListItem = require('./TabListItem.jsx');
|
|
||||||
var channelStore = require('../stores/channel');
|
|
||||||
var privateChatStore = require('../stores/privateChat');
|
|
||||||
var serverStore = require('../stores/server');
|
|
||||||
var routeActions = require('../actions/route');
|
|
||||||
var tabActions = require('../actions/tab');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var TabList = React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.connect(serverStore, 'servers'),
|
|
||||||
Reflux.connect(channelStore, 'channels'),
|
|
||||||
Reflux.connect(privateChatStore, 'privateChats')
|
|
||||||
],
|
|
||||||
|
|
||||||
handleConnectClick() {
|
|
||||||
routeActions.navigate('connect');
|
|
||||||
tabActions.hideMenu();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSettingsClick() {
|
|
||||||
routeActions.navigate('settings');
|
|
||||||
tabActions.hideMenu();
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var className = this.props.menuToggled ? 'tablist off-canvas' : 'tablist';
|
|
||||||
var tabs = [];
|
|
||||||
|
|
||||||
this.state.channels.forEach((server, address) => {
|
|
||||||
tabs.push(
|
|
||||||
<TabListItem
|
|
||||||
key={address}
|
|
||||||
server={address}
|
|
||||||
channel={null}
|
|
||||||
name={this.state.servers.getIn([address, 'name'])}>
|
|
||||||
</TabListItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
server.forEach((channel, name) => {
|
|
||||||
tabs.push(
|
|
||||||
<TabListItem
|
|
||||||
key={address + name}
|
|
||||||
server={address}
|
|
||||||
channel={name}
|
|
||||||
name={name}>
|
|
||||||
</TabListItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.state.privateChats.has(address)) {
|
|
||||||
this.state.privateChats.get(address).forEach(nick => {
|
|
||||||
tabs.push(
|
|
||||||
<TabListItem
|
|
||||||
key={address + nick}
|
|
||||||
server={address}
|
|
||||||
channel={nick}
|
|
||||||
name={nick}>
|
|
||||||
</TabListItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<button className="button-connect" onClick={this.handleConnectClick}>Connect</button>
|
|
||||||
<div className="tab-container">{tabs}</div>
|
|
||||||
<div className="side-buttons">
|
|
||||||
<i className="icon-user"></i>
|
|
||||||
<i className="icon-cog" onClick={this.handleSettingsClick}></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = TabList;
|
|
21
client/src/js/components/TabListItem.js
Normal file
21
client/src/js/components/TabListItem.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class TabListItem extends Component {
|
||||||
|
render() {
|
||||||
|
const classes = [];
|
||||||
|
|
||||||
|
if (this.props.server) {
|
||||||
|
classes.push('tab-server');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.selected) {
|
||||||
|
classes.push('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className={classes.join(' ')} onClick={this.props.onClick}>{this.props.content}</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Reflux = require('reflux');
|
|
||||||
|
|
||||||
var selectedTabStore = require('../stores/selectedTab');
|
|
||||||
var tabActions = require('../actions/tab');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var TabListItem = React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.connect(selectedTabStore, 'tab')
|
|
||||||
],
|
|
||||||
|
|
||||||
handleClick() {
|
|
||||||
tabActions.select(this.props.server, this.props.channel);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var classes = [];
|
|
||||||
|
|
||||||
if (!this.props.channel) {
|
|
||||||
classes.push('tab-server');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.server === this.state.tab.server &&
|
|
||||||
this.props.channel === this.state.tab.channel) {
|
|
||||||
classes.push('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className={classes.join(' ')} onClick={this.handleClick}>{this.props.name}</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = TabListItem;
|
|
53
client/src/js/components/UserList.js
Normal file
53
client/src/js/components/UserList.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import Infinite from 'react-infinite';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import UserListItem from './UserListItem';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class UserList extends Component {
|
||||||
|
state = {
|
||||||
|
height: window.innerHeight - 100
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
this.setState({ height: window.innerHeight - 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tab, openPrivateChat, select } = this.props;
|
||||||
|
const users = [];
|
||||||
|
const style = {};
|
||||||
|
|
||||||
|
if (!tab.channel) {
|
||||||
|
style.display = 'none';
|
||||||
|
} else {
|
||||||
|
this.props.users.forEach(user => {
|
||||||
|
users.push(
|
||||||
|
<UserListItem
|
||||||
|
key={user.nick}
|
||||||
|
user={user}
|
||||||
|
tab={tab}
|
||||||
|
openPrivateChat={openPrivateChat}
|
||||||
|
select={select}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="userlist" style={style}>
|
||||||
|
<Infinite containerHeight={this.state.height} elementHeight={24}>
|
||||||
|
{users}
|
||||||
|
</Infinite>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,83 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var Reflux = require('reflux');
|
|
||||||
var Infinite = require('react-infinite');
|
|
||||||
|
|
||||||
var UserListItem = require('./UserListItem.jsx');
|
|
||||||
var channelStore = require('../stores/channel');
|
|
||||||
var selectedTabStore = require('../stores/selectedTab');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var UserList = React.createClass({
|
|
||||||
mixins: [
|
|
||||||
PureMixin,
|
|
||||||
Reflux.listenTo(channelStore, 'channelsChanged'),
|
|
||||||
Reflux.listenTo(selectedTabStore, 'selectedTabChanged')
|
|
||||||
],
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
var tab = selectedTabStore.getState();
|
|
||||||
|
|
||||||
return {
|
|
||||||
users: channelStore.getUsers(tab.server, tab.channel),
|
|
||||||
selectedTab: tab,
|
|
||||||
height: window.innerHeight - 100
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener('resize', this.handleResize);
|
|
||||||
},
|
|
||||||
|
|
||||||
channelsChanged() {
|
|
||||||
var tab = this.state.selectedTab;
|
|
||||||
|
|
||||||
this.setState({ users: channelStore.getUsers(tab.server, tab.channel) });
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedTabChanged(tab) {
|
|
||||||
this.setState({
|
|
||||||
selectedTab: tab,
|
|
||||||
users: channelStore.getUsers(tab.server, tab.channel)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleResize() {
|
|
||||||
this.setState({ height: window.innerHeight - 100 });
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
var tab = this.state.selectedTab;
|
|
||||||
var users = [];
|
|
||||||
var style = {};
|
|
||||||
|
|
||||||
if (!tab.channel || tab.channel[0] !== '#') {
|
|
||||||
style.display = 'none';
|
|
||||||
} else {
|
|
||||||
this.state.users.forEach(user => {
|
|
||||||
users.push(<UserListItem key={user.nick} user={user} />);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (users.length > 1) {
|
|
||||||
return (
|
|
||||||
<div className="userlist" style={style}>
|
|
||||||
<Infinite containerHeight={this.state.height} elementHeight={24}>
|
|
||||||
{users}
|
|
||||||
</Infinite>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className="userlist" style={style}>
|
|
||||||
<div>{users}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = UserList;
|
|
16
client/src/js/components/UserListItem.js
Normal file
16
client/src/js/components/UserListItem.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class UserListItem extends Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const { tab, user, openPrivateChat, select } = this.props;
|
||||||
|
|
||||||
|
openPrivateChat(tab.server, user.nick);
|
||||||
|
select(tab.server, user.nick, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <p onClick={this.handleClick}>{this.props.user.renderName}</p>;
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
|
|
||||||
var selectedTabStore = require('../stores/selectedTab');
|
|
||||||
var privateChatActions = require('../actions/privateChat');
|
|
||||||
var tabActions = require('../actions/tab');
|
|
||||||
var PureMixin = require('../mixins/pure');
|
|
||||||
|
|
||||||
var UserListItem = React.createClass({
|
|
||||||
mixins: [PureMixin],
|
|
||||||
|
|
||||||
handleClick() {
|
|
||||||
var server = selectedTabStore.getServer();
|
|
||||||
|
|
||||||
privateChatActions.open(server, this.props.user.nick);
|
|
||||||
tabActions.select(server, this.props.user.nick);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <p onClick={this.handleClick}>{this.props.user.renderName}</p>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = UserListItem;
|
|
34
client/src/js/containers/App.js
Normal file
34
client/src/js/containers/App.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { pushPath } from 'redux-simple-router';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import TabList from '../components/TabList';
|
||||||
|
import * as actions from '../actions/tab';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
const { showMenu, children } = this.props;
|
||||||
|
const mainClass = showMenu ? 'main-container off-canvas' : 'main-container';
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TabList {...this.props} />
|
||||||
|
<div className={mainClass}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
servers: state.servers,
|
||||||
|
channels: state.channels,
|
||||||
|
privateChats: state.privateChats,
|
||||||
|
showMenu: state.showMenu,
|
||||||
|
selected: state.tab.selected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, { pushPath, ...actions })(App);
|
154
client/src/js/containers/Chat.js
Normal file
154
client/src/js/containers/Chat.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { List, Map } from 'immutable';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import ChatTitle from '../components/ChatTitle';
|
||||||
|
import Search from '../components/Search';
|
||||||
|
import MessageBox from '../components/MessageBox';
|
||||||
|
import MessageInput from '../components/MessageInput';
|
||||||
|
import UserList from '../components/UserList';
|
||||||
|
import { part } from '../actions/channel';
|
||||||
|
import { openPrivateChat, closePrivateChat } from '../actions/privateChat';
|
||||||
|
import { searchMessages, toggleSearch } from '../actions/search';
|
||||||
|
import { select, setSelectedChannel, setSelectedUser } from '../actions/tab';
|
||||||
|
import { runCommand, sendMessage } from '../actions/message';
|
||||||
|
import { disconnect } from '../actions/server';
|
||||||
|
import * as inputHistoryActions from '../actions/inputHistory';
|
||||||
|
import { setWrapWidth, setCharWidth } from '../actions/environment';
|
||||||
|
import { stringWidth, wrapMessages } from '../util';
|
||||||
|
|
||||||
|
function updateSelected({ params, dispatch }) {
|
||||||
|
if (params.channel) {
|
||||||
|
dispatch(setSelectedChannel(params.server, params.channel));
|
||||||
|
} else if (params.user) {
|
||||||
|
dispatch(setSelectedUser(params.server, params.user));
|
||||||
|
} else if (params.server) {
|
||||||
|
dispatch(setSelectedChannel(params.server));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCharWidth() {
|
||||||
|
const charWidth = stringWidth(' ', '16px Droid Sans Mono');
|
||||||
|
window.messageIndent = 6 * charWidth;
|
||||||
|
return setCharWidth(charWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@pure
|
||||||
|
class Chat extends Component {
|
||||||
|
componentWillMount() {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(updateCharWidth());
|
||||||
|
setTimeout(() => dispatch(updateCharWidth()), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
updateSelected(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.params.server !== this.props.params.server ||
|
||||||
|
nextProps.params.channel !== this.props.params.channel ||
|
||||||
|
nextProps.params.user !== this.props.params.user) {
|
||||||
|
updateSelected(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tab, channel, search, history, dispatch } = this.props;
|
||||||
|
|
||||||
|
let chatClass;
|
||||||
|
if (tab.channel) {
|
||||||
|
chatClass = 'chat-channel';
|
||||||
|
} else if (tab.user) {
|
||||||
|
chatClass = 'chat-private';
|
||||||
|
} else {
|
||||||
|
chatClass = 'chat-server';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={chatClass}>
|
||||||
|
<ChatTitle {...this.props } />
|
||||||
|
<Search
|
||||||
|
search={search}
|
||||||
|
onSearch={phrase => tab.channel &&
|
||||||
|
dispatch(searchMessages(tab.server, tab.channel, phrase))}
|
||||||
|
/>
|
||||||
|
<MessageBox {...this.props } />
|
||||||
|
<MessageInput
|
||||||
|
tab={tab}
|
||||||
|
channel={channel}
|
||||||
|
runCommand={this.props.runCommand}
|
||||||
|
sendMessage={this.props.sendMessage}
|
||||||
|
history={history}
|
||||||
|
{...bindActionCreators(inputHistoryActions, dispatch)}
|
||||||
|
/>
|
||||||
|
<UserList {...this.props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabSelector = state => state.tab.selected;
|
||||||
|
const messageSelector = state => state.messages;
|
||||||
|
|
||||||
|
const selectedMessagesSelector = createSelector(
|
||||||
|
tabSelector,
|
||||||
|
messageSelector,
|
||||||
|
(tab, messages) => messages.getIn([tab.server, tab.channel || tab.user || tab.server], List())
|
||||||
|
);
|
||||||
|
|
||||||
|
const wrapWidthSelector = state => state.environment.get('wrapWidth');
|
||||||
|
const charWidthSelector = state => state.environment.get('charWidth');
|
||||||
|
|
||||||
|
const wrappedMessagesSelector = createSelector(
|
||||||
|
selectedMessagesSelector,
|
||||||
|
wrapWidthSelector,
|
||||||
|
charWidthSelector,
|
||||||
|
(messages, width, charWidth) => wrapMessages(messages, width, charWidth, 6 * charWidth)
|
||||||
|
);
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const tab = state.tab.selected;
|
||||||
|
const channel = state.channels.getIn([tab.server, tab.channel], Map());
|
||||||
|
|
||||||
|
let title;
|
||||||
|
if (tab.channel) {
|
||||||
|
title = channel.get('name');
|
||||||
|
} else if (tab.user) {
|
||||||
|
title = tab.user;
|
||||||
|
} else {
|
||||||
|
title = state.servers.getIn([tab.server, 'name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
search: state.search,
|
||||||
|
users: channel.get('users', List()),
|
||||||
|
history: state.input.index === -1 ? null : state.input.history.get(state.input.index),
|
||||||
|
messages: wrappedMessagesSelector(state),
|
||||||
|
channel,
|
||||||
|
tab
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
dispatch,
|
||||||
|
...bindActionCreators({
|
||||||
|
select,
|
||||||
|
toggleSearch,
|
||||||
|
searchMessages,
|
||||||
|
runCommand,
|
||||||
|
sendMessage,
|
||||||
|
part,
|
||||||
|
disconnect,
|
||||||
|
openPrivateChat,
|
||||||
|
closePrivateChat,
|
||||||
|
setWrapWidth
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Chat);
|
81
client/src/js/containers/Connect.js
Normal file
81
client/src/js/containers/Connect.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import Navicon from '../components/Navicon';
|
||||||
|
import * as serverActions from '../actions/server';
|
||||||
|
import { join } from '../actions/channel';
|
||||||
|
import { select } from '../actions/tab';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
class Connect extends Component {
|
||||||
|
state = {
|
||||||
|
showOptionals: false
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const 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 opts = {
|
||||||
|
name: e.target.name.value.trim(),
|
||||||
|
tls: e.target.ssl.checked
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.showOptionals) {
|
||||||
|
opts.realname = e.target.realname.value.trim();
|
||||||
|
opts.username = e.target.username.value.trim();
|
||||||
|
opts.password = e.target.password.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.indexOf('.') > 0 && nick) {
|
||||||
|
dispatch(serverActions.connect(address, nick, opts));
|
||||||
|
dispatch(select(address));
|
||||||
|
|
||||||
|
if (channels.length > 0) {
|
||||||
|
dispatch(join(channels, address));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShowClick = () => {
|
||||||
|
this.setState({ showOptionals: !this.state.showOptionals });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let optionals = null;
|
||||||
|
|
||||||
|
if (this.state.showOptionals) {
|
||||||
|
optionals = (
|
||||||
|
<div>
|
||||||
|
<input name="username" type="text" placeholder="Username" />
|
||||||
|
<input name="password" type="text" placeholder="Password" />
|
||||||
|
<input name="realname" type="text" placeholder="Realname" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="connect">
|
||||||
|
<Navicon />
|
||||||
|
<form ref="form" className="connect-form" onSubmit={this.handleSubmit}>
|
||||||
|
<h1>Connect</h1>
|
||||||
|
<input name="name" type="text" placeholder="Name" defaultValue="Freenode" />
|
||||||
|
<input name="address" type="text" placeholder="Address" defaultValue="irc.freenode.net" />
|
||||||
|
<input name="nick" type="text" placeholder="Nick" />
|
||||||
|
<input name="channels" type="text" placeholder="Channels" />
|
||||||
|
{optionals}
|
||||||
|
<p>
|
||||||
|
<label><input name="ssl" type="checkbox" />SSL</label>
|
||||||
|
<i className="icon-ellipsis" onClick={this.handleShowClick}></i>
|
||||||
|
</p>
|
||||||
|
<input type="submit" value="Connect" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect()(Connect);
|
10
client/src/js/containers/DevTools.js
Normal file
10
client/src/js/containers/DevTools.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createDevTools } from 'redux-devtools';
|
||||||
|
import DockMonitor from 'redux-devtools-dock-monitor';
|
||||||
|
import LogMonitor from 'redux-devtools-log-monitor';
|
||||||
|
|
||||||
|
export default createDevTools(
|
||||||
|
<DockMonitor toggleVisibilityKey="ctrl-h" changePositionKey="ctrl-q">
|
||||||
|
<LogMonitor theme="tomorrow" />
|
||||||
|
</DockMonitor>
|
||||||
|
);
|
20
client/src/js/containers/Root.dev.js
Normal file
20
client/src/js/containers/Root.dev.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Router } from 'react-router';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
import DevTools from './DevTools';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class Root extends Component {
|
||||||
|
render() {
|
||||||
|
const { store, routes, history } = this.props;
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<div>
|
||||||
|
<Router routes={routes} history={history} />
|
||||||
|
<DevTools />
|
||||||
|
</div>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
5
client/src/js/containers/Root.js
Normal file
5
client/src/js/containers/Root.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
if (__DEV__) {
|
||||||
|
module.exports = require('./Root.dev');
|
||||||
|
} else {
|
||||||
|
module.exports = require('./Root.prod');
|
||||||
|
}
|
16
client/src/js/containers/Root.prod.js
Normal file
16
client/src/js/containers/Root.prod.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Router } from 'react-router';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import pure from 'pure-render-decorator';
|
||||||
|
|
||||||
|
@pure
|
||||||
|
export default class Root extends Component {
|
||||||
|
render() {
|
||||||
|
const { store, routes, history } = this.props;
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router routes={routes} history={history} />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
31
client/src/js/index.js
Normal file
31
client/src/js/index.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { syncReduxAndRouter, replacePath } from 'redux-simple-router';
|
||||||
|
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
||||||
|
import configureStore from './store';
|
||||||
|
import createRoutes from './routes';
|
||||||
|
import Socket from './util/Socket';
|
||||||
|
import handleSocket from './socket';
|
||||||
|
import { createUUID } from './util';
|
||||||
|
import Root from './containers/Root';
|
||||||
|
|
||||||
|
const socket = __DEV__ ?
|
||||||
|
new Socket(`${window.location.hostname}:1337`) :
|
||||||
|
new Socket(window.location.host);
|
||||||
|
|
||||||
|
const store = configureStore(socket);
|
||||||
|
const routes = createRoutes();
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
|
syncReduxAndRouter(history, store);
|
||||||
|
handleSocket(socket, store);
|
||||||
|
|
||||||
|
let uuid = localStorage.uuid;
|
||||||
|
if (!uuid) {
|
||||||
|
store.dispatch(replacePath('/connect'));
|
||||||
|
localStorage.uuid = uuid = createUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('connect', () => socket.send('uuid', uuid));
|
||||||
|
|
||||||
|
render(<Root store={store} routes={routes} history={history} />, document.getElementById('root'));
|
@ -1,98 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var socket = require('./socket');
|
|
||||||
var selectedTabStore = require('./stores/selectedTab');
|
|
||||||
var channelActions = require('./actions/channel');
|
|
||||||
var messageActions = require('./actions/message');
|
|
||||||
var serverActions = require('./actions/server');
|
|
||||||
var routeActions = require('./actions/route');
|
|
||||||
var searchActions = require('./actions/search');
|
|
||||||
|
|
||||||
function withReason(message, reason) {
|
|
||||||
return message + (reason ? ' (' + reason + ')' : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on('join', function(data) {
|
|
||||||
channelActions.addUser(data.user, data.server, data.channels[0]);
|
|
||||||
messageActions.inform(data.user + ' joined the channel', data.server, data.channels[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('part', function(data) {
|
|
||||||
channelActions.removeUser(data.user, data.server, data.channels[0]);
|
|
||||||
messageActions.inform(withReason(data.user + ' left the channel', data.reason), data.server, data.channels[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('quit', function(data) {
|
|
||||||
messageActions.broadcast(withReason(data.user + ' quit', data.reason), data.server, data.user);
|
|
||||||
channelActions.removeUserAll(data.user, data.server);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('nick', function(data) {
|
|
||||||
messageActions.broadcast(data.old + ' changed nick to ' + data.new, data.server, data.old);
|
|
||||||
channelActions.renameUser(data.old, data.new, data.server);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('message', function(data) {
|
|
||||||
messageActions.add(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('pm', function(data) {
|
|
||||||
messageActions.add(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('motd', function(data) {
|
|
||||||
messageActions.addAll(_.map(data.content, line => {
|
|
||||||
return {
|
|
||||||
server: data.server,
|
|
||||||
to: data.server,
|
|
||||||
message: line
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('users', function(data) {
|
|
||||||
channelActions.setUsers(data.users, data.server, data.channel);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('topic', function(data) {
|
|
||||||
channelActions.setTopic(data.topic, data.server, data.channel);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('mode', function(data) {
|
|
||||||
channelActions.setMode(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('whois', function(data) {
|
|
||||||
var tab = selectedTabStore.getState();
|
|
||||||
|
|
||||||
messageActions.inform([
|
|
||||||
'Nick: ' + data.nick,
|
|
||||||
'Username: ' + data.username,
|
|
||||||
'Realname: ' + data.realname,
|
|
||||||
'Host: ' + data.host,
|
|
||||||
'Server: ' + data.server,
|
|
||||||
'Channels: ' + data.channels
|
|
||||||
], tab.server, tab.channel);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('servers', function(data) {
|
|
||||||
window.loaded = true;
|
|
||||||
|
|
||||||
if (data === null) {
|
|
||||||
routeActions.navigate('connect', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
serverActions.load(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('channels', function(data) {
|
|
||||||
channelActions.load(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('search', function(data) {
|
|
||||||
searchActions.searchDone(data.results);
|
|
||||||
});
|
|
||||||
|
|
||||||
serverActions.connect.listen(function(server) {
|
|
||||||
messageActions.inform('Connecting...', server);
|
|
||||||
});
|
|
20
client/src/js/middleware/command.js
Normal file
20
client/src/js/middleware/command.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default function createCommandMiddleware(type, handlers) {
|
||||||
|
return store => next => action => {
|
||||||
|
if (action.type === type) {
|
||||||
|
const words = action.command.slice(1).split(' ');
|
||||||
|
const command = words[0];
|
||||||
|
const params = words.slice(1);
|
||||||
|
|
||||||
|
if (handlers.hasOwnProperty(command)) {
|
||||||
|
handlers[command]({
|
||||||
|
dispatch: store.dispatch,
|
||||||
|
getState: store.getState,
|
||||||
|
server: action.server,
|
||||||
|
channel: action.channel
|
||||||
|
}, ...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
}
|
9
client/src/js/middleware/socket.js
Normal file
9
client/src/js/middleware/socket.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function createSocketMiddleware(socket) {
|
||||||
|
return () => next => action => {
|
||||||
|
if (action.socket) {
|
||||||
|
socket.send(action.socket.type, action.socket.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
}
|
@ -1,17 +0,0 @@
|
|||||||
var shallowEqual = require('react-pure-render/shallowEqual');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (this.context.router) {
|
|
||||||
var changed = this.pureComponentLastPath !== this.context.router.getCurrentPath();
|
|
||||||
this.pureComponentLastPath = this.context.router.getCurrentPath();
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !shallowEqual(this.props, nextProps) ||
|
|
||||||
!shallowEqual(this.state, nextState);
|
|
||||||
}
|
|
||||||
};
|
|
191
client/src/js/reducers/channels.js
Normal file
191
client/src/js/reducers/channels.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { Map, List, Record } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const User = Record({
|
||||||
|
nick: null,
|
||||||
|
renderName: null,
|
||||||
|
mode: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateRenderName(user) {
|
||||||
|
let name = user.nick;
|
||||||
|
|
||||||
|
if (user.mode.indexOf('o') !== -1) {
|
||||||
|
name = '@' + name;
|
||||||
|
} else if (user.mode.indexOf('v') !== -1) {
|
||||||
|
name = '+' + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.set('renderName', name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUser(nick, mode) {
|
||||||
|
return updateRenderName(new User({
|
||||||
|
nick,
|
||||||
|
renderName: nick,
|
||||||
|
mode: mode || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadUser(nick) {
|
||||||
|
let mode;
|
||||||
|
|
||||||
|
if (nick[0] === '@') {
|
||||||
|
mode = 'o';
|
||||||
|
} else if (nick[0] === '+') {
|
||||||
|
mode = 'v';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
return createUser(nick.slice(1), mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createUser(nick, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareUsers(a, b) {
|
||||||
|
a = a.renderName.toLowerCase();
|
||||||
|
b = b.renderName.toLowerCase();
|
||||||
|
|
||||||
|
if (a[0] === '@' && b[0] !== '@') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b[0] === '@' && a[0] !== '@') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a[0] === '+' && b[0] !== '+') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b[0] === '+' && a[0] !== '+') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a > b) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createReducer(Map(), {
|
||||||
|
[actions.PART](state, action) {
|
||||||
|
const { channels, server } = action;
|
||||||
|
return state.withMutations(s => {
|
||||||
|
channels.forEach(channel => s.deleteIn([server, channel]));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_JOIN](state, action) {
|
||||||
|
const { server, channels, user } = action;
|
||||||
|
const channel = channels[0];
|
||||||
|
return state
|
||||||
|
.setIn([server, channel, 'name'], channels[0])
|
||||||
|
.updateIn([server, channel, 'users'], List(), users => {
|
||||||
|
return users.push(createUser(user)).sort(compareUsers);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_PART](state, action) {
|
||||||
|
const { server, channels, user } = action;
|
||||||
|
const channel = channels[0];
|
||||||
|
if (state.hasIn([server, channel])) {
|
||||||
|
return state.updateIn([server, channel, 'users'], users => users.filter(u => u.nick !== user));
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_QUIT](state, action) {
|
||||||
|
const { server, user } = action;
|
||||||
|
return state.withMutations(s => {
|
||||||
|
s.get(server).forEach((v, channel) => {
|
||||||
|
s.updateIn([server, channel, 'users'], users => users.filter(u => u.nick !== user));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_NICK](state, action) {
|
||||||
|
const { server, channels } = action;
|
||||||
|
return state.withMutations(s => {
|
||||||
|
channels.forEach(channel => {
|
||||||
|
s.updateIn([server, channel, 'users'], users => {
|
||||||
|
const i = users.findIndex(user => user.nick === action.old);
|
||||||
|
return users.update(i, user => {
|
||||||
|
return updateRenderName(user.set('nick', action.new));
|
||||||
|
}).sort(compareUsers);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_USERS](state, action) {
|
||||||
|
const { server, channel, users } = action;
|
||||||
|
return state.setIn([server, channel, 'users'],
|
||||||
|
List(users.map(user => loadUser(user)).sort(compareUsers)));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_TOPIC](state, action) {
|
||||||
|
const { server, channel, topic } = action;
|
||||||
|
return state.setIn([server, channel, 'topic'], topic);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_MODE](state, action) {
|
||||||
|
const { server, channel, user, remove, add } = action;
|
||||||
|
|
||||||
|
const i = state.getIn([server, channel, 'users']).findIndex(u => u.nick === user);
|
||||||
|
return state
|
||||||
|
.updateIn([server, channel, 'users', i], u => {
|
||||||
|
let mode = u.mode;
|
||||||
|
let j = remove.length;
|
||||||
|
while (j--) {
|
||||||
|
mode = mode.replace(remove[j], '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateRenderName(u.set('mode', mode + add));
|
||||||
|
})
|
||||||
|
.updateIn([server, channel, 'users'], users => users.sort(compareUsers));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_CHANNELS](state, action) {
|
||||||
|
if (!action.data) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.withMutations(s => {
|
||||||
|
action.data.forEach(channel => {
|
||||||
|
s.setIn([channel.server, channel.name], Map({
|
||||||
|
users: List(),
|
||||||
|
topic: channel.topic,
|
||||||
|
name: channel.name
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_SERVERS](state, action) {
|
||||||
|
if (!action.data) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.withMutations(s => {
|
||||||
|
action.data.forEach(server => {
|
||||||
|
if (!state.has(server.address)) {
|
||||||
|
s.set(server.address, Map());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.CONNECT](state, action) {
|
||||||
|
const { server } = action;
|
||||||
|
if (!state.has(server)) {
|
||||||
|
return state.set(server, Map());
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.DISCONNECT](state, action) {
|
||||||
|
return state.delete(action.server);
|
||||||
|
}
|
||||||
|
});
|
9
client/src/js/reducers/environment.js
Normal file
9
client/src/js/reducers/environment.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Map } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
export default createReducer(Map(), {
|
||||||
|
[actions.SET_ENVIRONMENT](state, action) {
|
||||||
|
return state.set(action.key, action.value);
|
||||||
|
}
|
||||||
|
});
|
24
client/src/js/reducers/index.js
Normal file
24
client/src/js/reducers/index.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { routeReducer } from 'redux-simple-router';
|
||||||
|
import channels from './channels';
|
||||||
|
import environment from './environment';
|
||||||
|
import input from './input';
|
||||||
|
import messages from './messages';
|
||||||
|
import privateChats from './privateChats';
|
||||||
|
import search from './search';
|
||||||
|
import servers from './servers';
|
||||||
|
import showMenu from './showMenu';
|
||||||
|
import tab from './tab';
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
routing: routeReducer,
|
||||||
|
channels,
|
||||||
|
environment,
|
||||||
|
input,
|
||||||
|
messages,
|
||||||
|
privateChats,
|
||||||
|
search,
|
||||||
|
servers,
|
||||||
|
showMenu,
|
||||||
|
tab
|
||||||
|
});
|
45
client/src/js/reducers/input.js
Normal file
45
client/src/js/reducers/input.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { List, Record } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const HISTORY_MAX_LENGTH = 128;
|
||||||
|
|
||||||
|
const State = Record({
|
||||||
|
history: List(),
|
||||||
|
index: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createReducer(new State(), {
|
||||||
|
[actions.INPUT_HISTORY_ADD](state, action) {
|
||||||
|
const { line } = action;
|
||||||
|
if (line.trim() && line !== state.history.get(0)) {
|
||||||
|
if (history.length === HISTORY_MAX_LENGTH) {
|
||||||
|
return state.set('history', state.history.unshift(line).pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.set('history', state.history.unshift(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.INPUT_HISTORY_RESET](state) {
|
||||||
|
return state.set('index', -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.INPUT_HISTORY_INCREMENT](state) {
|
||||||
|
if (state.index < state.history.size - 1) {
|
||||||
|
return state.set('index', state.index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.INPUT_HISTORY_DECREMENT](state) {
|
||||||
|
if (state.index >= 0) {
|
||||||
|
return state.set('index', state.index - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
});
|
68
client/src/js/reducers/messages.js
Normal file
68
client/src/js/reducers/messages.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { List, Map, Record } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const Message = Record({
|
||||||
|
id: null,
|
||||||
|
server: null,
|
||||||
|
from: null,
|
||||||
|
to: null,
|
||||||
|
message: '',
|
||||||
|
time: null,
|
||||||
|
type: null,
|
||||||
|
lines: []
|
||||||
|
});
|
||||||
|
|
||||||
|
function addMessage(state, message) {
|
||||||
|
let dest = message.to || message.from;
|
||||||
|
if (message.from && message.from.indexOf('.') !== -1) {
|
||||||
|
dest = message.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.message.indexOf('\x01ACTION') === 0) {
|
||||||
|
const from = message.from;
|
||||||
|
message.from = null;
|
||||||
|
message.type = 'action';
|
||||||
|
message.message = from + message.message.slice(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.updateIn([message.server, dest], List(), list => list.push(new Message(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createReducer(Map(), {
|
||||||
|
[actions.SEND_MESSAGE](state, action) {
|
||||||
|
return addMessage(state, action);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.ADD_MESSAGE](state, action) {
|
||||||
|
return addMessage(state, action.message);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.ADD_MESSAGES](state, action) {
|
||||||
|
return state.withMutations(s =>
|
||||||
|
action.messages.forEach(message =>
|
||||||
|
addMessage(s, message)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
[actions.SOCKET_MESSAGE](state, action) {
|
||||||
|
return addMessage(state, action);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_PM](state, action) {
|
||||||
|
return addMessage(state, action);
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
[actions.DISCONNECT](state, action) {
|
||||||
|
return state.delete(action.server);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.PART](state, action) {
|
||||||
|
return state.withMutations(s =>
|
||||||
|
action.channels.forEach(channel =>
|
||||||
|
s.deleteIn([action.server, channel])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
29
client/src/js/reducers/privateChats.js
Normal file
29
client/src/js/reducers/privateChats.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Set, Map } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
function open(state, server, nick) {
|
||||||
|
return state.update(server, Set(), chats => chats.add(nick));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createReducer(Map(), {
|
||||||
|
[actions.OPEN_PRIVATE_CHAT](state, action) {
|
||||||
|
return open(state, action.server, action.nick);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.CLOSE_PRIVATE_CHAT](state, action) {
|
||||||
|
return state.update(action.server, chats => chats.delete(action.nick));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_PM](state, action) {
|
||||||
|
if (action.from.indexOf('.') === -1) {
|
||||||
|
return open(state, action.server, action.from);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.DISCONNECT](state, action) {
|
||||||
|
return state.delete(action.server);
|
||||||
|
}
|
||||||
|
});
|
18
client/src/js/reducers/search.js
Normal file
18
client/src/js/reducers/search.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { List, Record } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const State = Record({
|
||||||
|
show: false,
|
||||||
|
results: List()
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createReducer(new State(), {
|
||||||
|
[actions.SOCKET_SEARCH](state, action) {
|
||||||
|
return state.set('results', List(action.results));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.TOGGLE_SEARCH](state) {
|
||||||
|
return state.set('show', !state.show);
|
||||||
|
}
|
||||||
|
});
|
46
client/src/js/reducers/servers.js
Normal file
46
client/src/js/reducers/servers.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Map, Record } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const Server = Record({
|
||||||
|
nick: null,
|
||||||
|
name: null
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createReducer(Map(), {
|
||||||
|
[actions.CONNECT](state, action) {
|
||||||
|
let { server } = action;
|
||||||
|
const { nick, options } = action;
|
||||||
|
|
||||||
|
const i = server.indexOf(':');
|
||||||
|
if (i > 0) {
|
||||||
|
server = server.slice(0, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.set(server, new Server({
|
||||||
|
nick,
|
||||||
|
name: options.name || server
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.DISCONNECT](state, action) {
|
||||||
|
return state.delete(action.server);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SET_NICK](state, action) {
|
||||||
|
const { server, nick } = action;
|
||||||
|
return state.update(server, s => s.set('nick', nick));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SOCKET_SERVERS](state, action) {
|
||||||
|
if (!action.data) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.withMutations(s => {
|
||||||
|
action.data.forEach(server => {
|
||||||
|
s.set(server.address, new Server(server));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
12
client/src/js/reducers/showMenu.js
Normal file
12
client/src/js/reducers/showMenu.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
export default createReducer(false, {
|
||||||
|
[actions.TOGGLE_MENU](state) {
|
||||||
|
return !state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.HIDE_MENU]() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
48
client/src/js/reducers/tab.js
Normal file
48
client/src/js/reducers/tab.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Record, List } from 'immutable';
|
||||||
|
import { UPDATE_PATH } from 'redux-simple-router';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const Tab = Record({
|
||||||
|
server: null,
|
||||||
|
channel: null,
|
||||||
|
user: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const State = Record({
|
||||||
|
selected: new Tab(),
|
||||||
|
history: List()
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createReducer(new State(), {
|
||||||
|
[actions.SELECT_TAB](state, action) {
|
||||||
|
const tab = new Tab(action);
|
||||||
|
return state
|
||||||
|
.set('selected', tab)
|
||||||
|
.update('history', history => history.push(tab));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.PART](state, action) {
|
||||||
|
return state.set('history', state.history.filter(tab =>
|
||||||
|
!(tab.server === action.server && tab.channel === action.channels[0])
|
||||||
|
));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.CLOSE_PRIVATE_CHAT](state, action) {
|
||||||
|
return state.set('history', state.history.filter(tab =>
|
||||||
|
!(tab.server === action.server && tab.user === action.nick)
|
||||||
|
));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.DISCONNECT](state, action) {
|
||||||
|
return state.set('history', state.history.filter(tab => tab.server !== action.server));
|
||||||
|
},
|
||||||
|
|
||||||
|
[UPDATE_PATH](state, action) {
|
||||||
|
if (action.payload.path.indexOf('.') === -1 && state.selected.server) {
|
||||||
|
return state.set('selected', new Tab());
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
});
|
19
client/src/js/routes.js
Normal file
19
client/src/js/routes.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Route, IndexRoute } from 'react-router';
|
||||||
|
import App from './containers/App';
|
||||||
|
import Connect from './containers/Connect';
|
||||||
|
import Chat from './containers/Chat';
|
||||||
|
import Settings from './components/Settings';
|
||||||
|
|
||||||
|
export default function createRoutes() {
|
||||||
|
return (
|
||||||
|
<Route path="/" component={App}>
|
||||||
|
<Route path="connect" component={Connect} />
|
||||||
|
<Route path="settings" component={Settings} />
|
||||||
|
<Route path="/:server" component={Chat} />
|
||||||
|
<Route path="/:server/:channel" component={Chat} />
|
||||||
|
<Route path="/:server/pm/:user" component={Chat} />
|
||||||
|
<IndexRoute component={Settings} />
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
}
|
@ -1,85 +1,81 @@
|
|||||||
var EventEmitter = require('events').EventEmitter;
|
import { replacePath } from 'redux-simple-router';
|
||||||
|
import { broadcast, inform, addMessage, addMessages } from './actions/message';
|
||||||
|
import { select } from './actions/tab';
|
||||||
|
import { normalizeChannel } from './util';
|
||||||
|
|
||||||
var Backoff = require('backo');
|
function withReason(message, reason) {
|
||||||
|
return message + (reason ? ` (${reason})` : '');
|
||||||
class Socket extends EventEmitter {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.connectTimeout = 20000;
|
|
||||||
this.pingTimeout = 30000;
|
|
||||||
this.backoff = new Backoff({
|
|
||||||
min: 1000,
|
|
||||||
max: 5000,
|
|
||||||
jitter: 0.25
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.ws = new WebSocket('ws://' + window.location.host + '/ws');
|
|
||||||
|
|
||||||
this.timeoutConnect = setTimeout(() => {
|
|
||||||
this.ws.close();
|
|
||||||
this.retry();
|
|
||||||
}, this.connectTimeout);
|
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
|
||||||
clearTimeout(this.timeoutConnect);
|
|
||||||
this.backoff.reset();
|
|
||||||
this.emit('connect');
|
|
||||||
this.setTimeoutPing();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onclose = () => {
|
|
||||||
clearTimeout(this.timeoutConnect);
|
|
||||||
clearTimeout(this.timeoutPing);
|
|
||||||
if (!this.closing) {
|
|
||||||
this.emit('disconnect');
|
|
||||||
this.retry();
|
|
||||||
}
|
|
||||||
this.closing = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onerror = () => {
|
|
||||||
clearTimeout(this.timeoutConnect);
|
|
||||||
clearTimeout(this.timeoutPing);
|
|
||||||
this.closing = true;
|
|
||||||
this.ws.close();
|
|
||||||
this.retry();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onmessage = (e) => {
|
|
||||||
this.setTimeoutPing();
|
|
||||||
|
|
||||||
var msg = JSON.parse(e.data);
|
|
||||||
|
|
||||||
if (msg.type === 'ping') {
|
|
||||||
this.send('pong');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(msg.type, msg.data);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
retry() {
|
|
||||||
setTimeout(() => this.connect(), this.backoff.duration());
|
|
||||||
}
|
|
||||||
|
|
||||||
send(type, data) {
|
|
||||||
this.ws.send(JSON.stringify({ type, data }));
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeoutPing() {
|
|
||||||
clearTimeout(this.timeoutPing);
|
|
||||||
this.timeoutPing = setTimeout(() => {
|
|
||||||
this.emit('disconnect');
|
|
||||||
this.closing = true;
|
|
||||||
this.ws.close();
|
|
||||||
this.connect();
|
|
||||||
}, this.pingTimeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new Socket();
|
export default function handleSocket(socket, { dispatch, getState }) {
|
||||||
|
socket.onAny(data => {
|
||||||
|
const type = `SOCKET_${socket.event.toUpperCase()}`;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
dispatch({ type, data });
|
||||||
|
} else {
|
||||||
|
dispatch({ type, ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('message', message => dispatch(addMessage(message)));
|
||||||
|
socket.on('pm', message => dispatch(addMessage(message)));
|
||||||
|
|
||||||
|
socket.on('join', data => {
|
||||||
|
const state = getState();
|
||||||
|
const { server, channel } = state.tab.selected;
|
||||||
|
const { nick } = state.servers.get(server);
|
||||||
|
const [joinedChannel] = data.channels;
|
||||||
|
if (channel &&
|
||||||
|
server === data.server &&
|
||||||
|
nick === data.user &&
|
||||||
|
channel !== joinedChannel &&
|
||||||
|
normalizeChannel(channel) === normalizeChannel(joinedChannel)) {
|
||||||
|
dispatch(select(server, joinedChannel));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('servers', data => {
|
||||||
|
if (!data) {
|
||||||
|
dispatch(replacePath('/connect'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('join', ({ user, server, channels }) =>
|
||||||
|
dispatch(inform(`${user} joined the channel`, server, channels[0]))
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on('part', ({ user, server, channels, reason }) =>
|
||||||
|
dispatch(inform(withReason(`${user} left the channel`, reason), server, channels[0]))
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on('quit', ({ user, server, reason, channels }) =>
|
||||||
|
dispatch(broadcast(withReason(`${user} quit`, reason), server, channels))
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on('nick', data =>
|
||||||
|
dispatch(broadcast(`${data.old} changed nick to ${data.new}`, data.server, data.channels))
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on('motd', ({ content, server }) =>
|
||||||
|
dispatch(addMessages(content.map(line => {
|
||||||
|
return {
|
||||||
|
server,
|
||||||
|
to: server,
|
||||||
|
message: line
|
||||||
|
};
|
||||||
|
})))
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on('whois', data => {
|
||||||
|
const tab = getState().tab.selected;
|
||||||
|
|
||||||
|
dispatch(inform([
|
||||||
|
`Nick: ${data.nick}`,
|
||||||
|
`Username: ${data.username}`,
|
||||||
|
`Realname: ${data.realname}`,
|
||||||
|
`Host: ${data.host}`,
|
||||||
|
`Server: ${data.server}`,
|
||||||
|
`Channels: ${data.channels}`
|
||||||
|
], tab.server, tab.channel));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
5
client/src/js/store/index.js
Normal file
5
client/src/js/store/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
if (__DEV__) {
|
||||||
|
module.exports = require('./store.dev');
|
||||||
|
} else {
|
||||||
|
module.exports = require('./store.prod');
|
||||||
|
}
|
27
client/src/js/store/store.dev.js
Normal file
27
client/src/js/store/store.dev.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import reducer from '../reducers';
|
||||||
|
import createSocketMiddleware from '../middleware/socket';
|
||||||
|
import commands from '../commands';
|
||||||
|
import DevTools from '../containers/DevTools';
|
||||||
|
|
||||||
|
export default function configureStore(socket, initialState) {
|
||||||
|
const finalCreateStore = compose(
|
||||||
|
applyMiddleware(
|
||||||
|
thunk,
|
||||||
|
createSocketMiddleware(socket),
|
||||||
|
commands
|
||||||
|
),
|
||||||
|
DevTools.instrument()
|
||||||
|
)(createStore);
|
||||||
|
|
||||||
|
const store = finalCreateStore(reducer, initialState);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
module.hot.accept('../reducers', () => {
|
||||||
|
store.replaceReducer(require('../reducers').default);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
15
client/src/js/store/store.prod.js
Normal file
15
client/src/js/store/store.prod.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import reducer from '../reducers';
|
||||||
|
import createSocketMiddleware from '../middleware/socket';
|
||||||
|
import commands from '../commands';
|
||||||
|
|
||||||
|
export default function configureStore(socket, initialState) {
|
||||||
|
const finalCreateStore = applyMiddleware(
|
||||||
|
thunk,
|
||||||
|
createSocketMiddleware(socket),
|
||||||
|
commands
|
||||||
|
)(createStore);
|
||||||
|
|
||||||
|
return finalCreateStore(reducer, initialState);
|
||||||
|
}
|
@ -1,204 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
var Immutable = require('immutable');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var actions = require('../actions/channel');
|
|
||||||
var serverActions = require('../actions/server');
|
|
||||||
|
|
||||||
var channels = Immutable.Map();
|
|
||||||
var empty = Immutable.List();
|
|
||||||
|
|
||||||
var User = Immutable.Record({
|
|
||||||
nick: null,
|
|
||||||
renderName: null,
|
|
||||||
mode: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateRenderName(user) {
|
|
||||||
var name = user.nick;
|
|
||||||
|
|
||||||
if (user.mode.indexOf('o') !== -1) {
|
|
||||||
name = '@' + name;
|
|
||||||
} else if (user.mode.indexOf('v') !== -1) {
|
|
||||||
name = '+' + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.set('renderName', name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createUser(nick, mode) {
|
|
||||||
return updateRenderName(new User({
|
|
||||||
nick: nick,
|
|
||||||
renderName: nick,
|
|
||||||
mode: mode || ''
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadUser(nick) {
|
|
||||||
var mode;
|
|
||||||
|
|
||||||
if (nick[0] === '@') {
|
|
||||||
mode = 'o';
|
|
||||||
} else if (nick[0] === '+') {
|
|
||||||
mode = 'v';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode) {
|
|
||||||
nick = nick.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createUser(nick, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareUsers(a, b) {
|
|
||||||
a = a.renderName.toLowerCase();
|
|
||||||
b = b.renderName.toLowerCase();
|
|
||||||
|
|
||||||
if (a[0] === '@' && b[0] !== '@') {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (b[0] === '@' && a[0] !== '@') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (a[0] === '+' && b[0] !== '+') {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (b[0] === '+' && a[0] !== '+') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a > b) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
return channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
var channelStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
this.listenToMany(actions);
|
|
||||||
this.listenTo(serverActions.connect, 'addServer');
|
|
||||||
this.listenTo(serverActions.disconnect, 'removeServer');
|
|
||||||
this.listenTo(serverActions.load, 'loadServers');
|
|
||||||
},
|
|
||||||
|
|
||||||
part(partChannels, server) {
|
|
||||||
_.each(partChannels, function(channel) {
|
|
||||||
channels = channels.deleteIn([server, channel]);
|
|
||||||
});
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
addUser(user, server, channel) {
|
|
||||||
channels = channels.updateIn([server, channel, 'users'], empty, users => {
|
|
||||||
return users.push(createUser(user)).sort(compareUsers);
|
|
||||||
});
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeUser(user, server, channel) {
|
|
||||||
if (channels.hasIn([server, channel])) {
|
|
||||||
channels = channels.updateIn([server, channel, 'users'], users => users.filter(u => u.nick !== user));
|
|
||||||
this.trigger(channels);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeUserAll(user, server) {
|
|
||||||
channels.get(server).forEach((v, k) => {
|
|
||||||
channels = channels.updateIn([server, k, 'users'], users => users.filter(u => u.nick !== user));
|
|
||||||
});
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
renameUser(oldNick, newNick, server) {
|
|
||||||
channels.get(server).forEach((v, k) => {
|
|
||||||
channels = channels.updateIn([server, k, 'users'], users => {
|
|
||||||
var i = users.findIndex(user => user.nick === oldNick);
|
|
||||||
return users.update(i, user => {
|
|
||||||
return updateRenderName(user.set('nick', newNick));
|
|
||||||
}).sort(compareUsers);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
setUsers(users, server, channel) {
|
|
||||||
users = _.map(users, user => loadUser(user)).sort(compareUsers);
|
|
||||||
channels = channels.setIn([server, channel, 'users'], Immutable.List(users));
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
setTopic(topic, server, channel) {
|
|
||||||
channels = channels.setIn([server, channel, 'topic'], topic);
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
setMode(mode) {
|
|
||||||
var i = channels.getIn([mode.server, mode.channel, 'users']).findIndex(u => u.nick === mode.user);
|
|
||||||
|
|
||||||
channels = channels.updateIn([mode.server, mode.channel, 'users', i], user => {
|
|
||||||
_.each(mode.remove, remove => {
|
|
||||||
user = user.set('mode', user.mode.replace(remove, ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
user = user.set('mode', user.mode + mode.add);
|
|
||||||
|
|
||||||
return updateRenderName(user);
|
|
||||||
});
|
|
||||||
channels = channels.updateIn([mode.server, mode.channel, 'users'], users => users.sort(compareUsers));
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
load(storedChannels) {
|
|
||||||
_.each(storedChannels, function(channel) {
|
|
||||||
channels = channels.setIn([channel.server, channel.name], Immutable.Map({
|
|
||||||
users: Immutable.List(),
|
|
||||||
topic: channel.topic
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
addServer(server) {
|
|
||||||
if (!channels.has(server)) {
|
|
||||||
channels = channels.set(server, Immutable.Map());
|
|
||||||
this.trigger(channels);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeServer(server) {
|
|
||||||
channels = channels.delete(server);
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
loadServers(storedServers) {
|
|
||||||
_.each(storedServers, function(server) {
|
|
||||||
if (!channels.has(server.address)) {
|
|
||||||
channels = channels.set(server.address, Immutable.Map());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.trigger(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
getChannels(server) {
|
|
||||||
return channels.get(server);
|
|
||||||
},
|
|
||||||
|
|
||||||
getUsers(server, channel) {
|
|
||||||
return channels.getIn([server, channel, 'users']) || empty;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTopic(server, channel) {
|
|
||||||
return channels.getIn([server, channel, 'topic']);
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: getState,
|
|
||||||
getState
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = channelStore;
|
|
@ -1,64 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
|
|
||||||
var actions = require('../actions/inputHistory');
|
|
||||||
|
|
||||||
var HISTORY_MAX_LENGTH = 128;
|
|
||||||
|
|
||||||
var history = [];
|
|
||||||
var index = -1;
|
|
||||||
var stored = localStorage.inputHistory;
|
|
||||||
|
|
||||||
if (stored) {
|
|
||||||
history = JSON.parse(stored);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
if (index !== -1) {
|
|
||||||
return history[index];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var inputHistoryStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
this.listenToMany(actions);
|
|
||||||
},
|
|
||||||
|
|
||||||
add(line) {
|
|
||||||
if (line.trim() && line !== history[0]) {
|
|
||||||
history.unshift(line);
|
|
||||||
|
|
||||||
if (history.length > HISTORY_MAX_LENGTH) {
|
|
||||||
history.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.inputHistory = JSON.stringify(history);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
if (index !== -1) {
|
|
||||||
index = -1;
|
|
||||||
this.trigger(history[index]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
if (index !== history.length - 1) {
|
|
||||||
index++;
|
|
||||||
this.trigger(history[index]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
decrement() {
|
|
||||||
if (index !== -1) {
|
|
||||||
index--;
|
|
||||||
this.trigger(history[index]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: getState,
|
|
||||||
getState
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = inputHistoryStore;
|
|
@ -1,139 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
var Immutable = require('immutable');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var serverStore = require('./server');
|
|
||||||
var channelStore = require('./channel');
|
|
||||||
var actions = require('../actions/message');
|
|
||||||
var serverActions = require('../actions/server');
|
|
||||||
var channelActions = require('../actions/channel');
|
|
||||||
|
|
||||||
var messages = Immutable.Map();
|
|
||||||
var empty = Immutable.List();
|
|
||||||
|
|
||||||
var Message = Immutable.Record({
|
|
||||||
id: null,
|
|
||||||
server: null,
|
|
||||||
from: null,
|
|
||||||
to: null,
|
|
||||||
message: '',
|
|
||||||
time: null,
|
|
||||||
type: null,
|
|
||||||
lines: []
|
|
||||||
});
|
|
||||||
|
|
||||||
function addMessage(message, dest, mutable) {
|
|
||||||
message.time = new Date();
|
|
||||||
|
|
||||||
if (message.message.indexOf('\x01ACTION') === 0) {
|
|
||||||
var from = message.from;
|
|
||||||
message.from = null;
|
|
||||||
message.type = 'action';
|
|
||||||
message.message = from + message.message.slice(7);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mutable) {
|
|
||||||
mutable.updateIn([message.server, dest], empty, list => list.push(new Message(message)));
|
|
||||||
} else {
|
|
||||||
messages = messages.updateIn([message.server, dest], empty, list => list.push(new Message(message)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDest(message) {
|
|
||||||
var dest = message.to || message.from;
|
|
||||||
if (message.from && message.from.indexOf('.') !== -1) {
|
|
||||||
dest = message.server;
|
|
||||||
}
|
|
||||||
return dest;
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
this.listenToMany(actions);
|
|
||||||
this.listenTo(serverActions.disconnect, 'disconnect');
|
|
||||||
this.listenTo(channelActions.part, 'part');
|
|
||||||
},
|
|
||||||
|
|
||||||
send(message, to, server) {
|
|
||||||
addMessage({
|
|
||||||
server: server,
|
|
||||||
from: serverStore.getNick(server),
|
|
||||||
to: to,
|
|
||||||
message: message
|
|
||||||
}, to);
|
|
||||||
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
add(message) {
|
|
||||||
addMessage(message, getDest(message));
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
addAll(newMessages) {
|
|
||||||
messages = messages.withMutations(mutable => {
|
|
||||||
_.each(newMessages, message => {
|
|
||||||
addMessage(message, getDest(message), mutable);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
broadcast(message, server, user) {
|
|
||||||
channelStore.getChannels(server).forEach((channel, channelName) => {
|
|
||||||
if (!user || (user && channel.get('users').find(u => u.nick === user))) {
|
|
||||||
addMessage({
|
|
||||||
server: server,
|
|
||||||
to: channelName,
|
|
||||||
message: message,
|
|
||||||
type: 'info'
|
|
||||||
}, channelName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
inform(message, server, channel) {
|
|
||||||
if (_.isArray(message)) {
|
|
||||||
_.each(message, (msg) => {
|
|
||||||
addMessage({
|
|
||||||
server: server,
|
|
||||||
to: channel,
|
|
||||||
message: msg,
|
|
||||||
type: 'info'
|
|
||||||
}, channel || server);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
addMessage({
|
|
||||||
server: server,
|
|
||||||
to: channel,
|
|
||||||
message: message,
|
|
||||||
type: 'info'
|
|
||||||
}, channel || server);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnect(server) {
|
|
||||||
messages = messages.delete(server);
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
part(channels, server) {
|
|
||||||
_.each(channels, function(channel) {
|
|
||||||
messages = messages.deleteIn([server, channel]);
|
|
||||||
});
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
getMessages(server, dest) {
|
|
||||||
return messages.getIn([server, dest]) || empty;
|
|
||||||
},
|
|
||||||
|
|
||||||
getState() {
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = messageStore;
|
|
@ -1,69 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
|
|
||||||
var util = require('../util');
|
|
||||||
var messageStore = require('./message');
|
|
||||||
var selectedTabStore = require('./selectedTab');
|
|
||||||
var messageActions = require('../actions/message');
|
|
||||||
|
|
||||||
var tab = selectedTabStore.getState();
|
|
||||||
var width = window.innerWidth;
|
|
||||||
var messages;
|
|
||||||
var prev;
|
|
||||||
|
|
||||||
function updateCharWidth() {
|
|
||||||
window.charWidth = util.stringWidth(' ', '16px Droid Sans Mono');
|
|
||||||
window.messageIndent = 6 * window.charWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrap() {
|
|
||||||
var next = messageStore.getMessages(tab.server, tab.channel || tab.server);
|
|
||||||
if (next !== prev) {
|
|
||||||
prev = next;
|
|
||||||
messages = util.wrapMessages(next, width, window.charWidth, window.messageIndent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageLineStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
updateCharWidth();
|
|
||||||
wrap();
|
|
||||||
|
|
||||||
// Temporary hack incase this runs before the font has loaded
|
|
||||||
setTimeout(updateCharWidth, 1000);
|
|
||||||
|
|
||||||
this.listenTo(messageActions.setWrapWidth, 'setWrapWidth');
|
|
||||||
this.listenTo(messageStore, 'messagesChanged');
|
|
||||||
this.listenTo(selectedTabStore, 'selectedTabChanged');
|
|
||||||
},
|
|
||||||
|
|
||||||
setWrapWidth(w) {
|
|
||||||
width = w;
|
|
||||||
messages = util.wrapMessages(messages, width, window.charWidth, window.messageIndent);
|
|
||||||
this.trigger(messages);
|
|
||||||
},
|
|
||||||
|
|
||||||
messagesChanged() {
|
|
||||||
if (wrap()) {
|
|
||||||
this.trigger(messages);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedTabChanged(selectedTab) {
|
|
||||||
tab = selectedTab;
|
|
||||||
|
|
||||||
if (wrap()) {
|
|
||||||
this.trigger(messages);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: getState,
|
|
||||||
getState
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = messageLineStore;
|
|
@ -1,47 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
var Immutable = require('immutable');
|
|
||||||
|
|
||||||
var actions = require('../actions/privateChat');
|
|
||||||
var messageActions = require('../actions/message');
|
|
||||||
var serverActions = require('../actions/server');
|
|
||||||
|
|
||||||
var privateChats = Immutable.Map();
|
|
||||||
var empty = Immutable.List();
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
return privateChats;
|
|
||||||
}
|
|
||||||
|
|
||||||
var privateChatStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
this.listenToMany(actions);
|
|
||||||
this.listenTo(messageActions.add, 'messageAdded');
|
|
||||||
this.listenTo(serverActions.disconnect, 'disconnect');
|
|
||||||
},
|
|
||||||
|
|
||||||
open(server, nick) {
|
|
||||||
privateChats = privateChats.update(server, empty, chats => chats.push(nick));
|
|
||||||
this.trigger(privateChats);
|
|
||||||
},
|
|
||||||
|
|
||||||
close(server, nick) {
|
|
||||||
privateChats = privateChats.update(server, chats => chats.delete(chats.indexOf(nick)));
|
|
||||||
this.trigger(privateChats);
|
|
||||||
},
|
|
||||||
|
|
||||||
messageAdded(message) {
|
|
||||||
if (!message.to && message.from.indexOf('.') === -1) {
|
|
||||||
this.open(message.server, message.from);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnect(server) {
|
|
||||||
privateChats = privateChats.delete(server);
|
|
||||||
this.trigger(privateChats);
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: getState,
|
|
||||||
getState
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = privateChatStore;
|
|
@ -1,36 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
var Immutable = require('immutable');
|
|
||||||
|
|
||||||
var actions = require('../actions/search');
|
|
||||||
|
|
||||||
var Search = Immutable.Record({
|
|
||||||
show: false,
|
|
||||||
results: Immutable.List()
|
|
||||||
});
|
|
||||||
|
|
||||||
var search = new Search();
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
return search;
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
this.listenToMany(actions);
|
|
||||||
},
|
|
||||||
|
|
||||||
searchDone(results) {
|
|
||||||
search = search.set('results', Immutable.List(results));
|
|
||||||
this.trigger(search);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggle() {
|
|
||||||
search = search.set('show', !search.show);
|
|
||||||
this.trigger(search);
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: getState,
|
|
||||||
getState
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = searchStore;
|
|
@ -1,180 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
var Immutable = require('immutable');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var serverStore = require('./server');
|
|
||||||
var actions = require('../actions/tab');
|
|
||||||
var channelActions = require('../actions/channel');
|
|
||||||
var serverActions = require('../actions/server');
|
|
||||||
var routeActions = require('../actions/route');
|
|
||||||
var privateChatActions = require('../actions/privateChat');
|
|
||||||
|
|
||||||
var Tab = Immutable.Record({
|
|
||||||
server: null,
|
|
||||||
channel: null,
|
|
||||||
name: null
|
|
||||||
});
|
|
||||||
|
|
||||||
var selectedTab = new Tab();
|
|
||||||
var history = [];
|
|
||||||
|
|
||||||
function selectPrevTab() {
|
|
||||||
history.pop();
|
|
||||||
|
|
||||||
if (history.length > 0) {
|
|
||||||
selectedTab = history[history.length - 1];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateChannelName(name) {
|
|
||||||
selectedTab = selectedTab.set('channel', name).set('name', name);
|
|
||||||
history[history.length - 1] = selectedTab;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
return selectedTab;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedTabStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
this.listenToMany(actions);
|
|
||||||
this.listenTo(channelActions.part, 'part');
|
|
||||||
this.listenTo(privateChatActions.close, 'close');
|
|
||||||
this.listenTo(serverActions.disconnect, 'disconnect');
|
|
||||||
this.listenTo(channelActions.addUser, 'userAdded');
|
|
||||||
this.listenTo(channelActions.load, 'loadChannels');
|
|
||||||
this.listenTo(serverActions.load, 'loadServers');
|
|
||||||
this.listenTo(routeActions.navigate, 'navigate');
|
|
||||||
},
|
|
||||||
|
|
||||||
select(server, channel = null) {
|
|
||||||
selectedTab = new Tab({
|
|
||||||
server,
|
|
||||||
channel,
|
|
||||||
name: channel || serverStore.getName(server)
|
|
||||||
});
|
|
||||||
|
|
||||||
history.push(selectedTab);
|
|
||||||
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
},
|
|
||||||
|
|
||||||
part(channels, server) {
|
|
||||||
if (server === selectedTab.server &&
|
|
||||||
channels.indexOf(selectedTab.channel) !== -1) {
|
|
||||||
if (!selectPrevTab()) {
|
|
||||||
selectedTab = selectedTab
|
|
||||||
.set('channel', null)
|
|
||||||
.set('name', serverStore.getName(server));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
close(server, nick) {
|
|
||||||
if (server === selectedTab.server &&
|
|
||||||
nick === selectedTab.channel) {
|
|
||||||
if (!selectPrevTab()) {
|
|
||||||
selectedTab = selectedTab
|
|
||||||
.set('channel', null)
|
|
||||||
.set('name', serverStore.getName(server));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnect(server) {
|
|
||||||
if (server === selectedTab.server) {
|
|
||||||
_.remove(history, { server: server });
|
|
||||||
|
|
||||||
if (!selectPrevTab()) {
|
|
||||||
selectedTab = new Tab();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
userAdded(user, server, channel) {
|
|
||||||
if (selectedTab.channel &&
|
|
||||||
server === selectedTab.server &&
|
|
||||||
user === serverStore.getNick(server) &&
|
|
||||||
channel.toLowerCase().indexOf(selectedTab.channel.toLowerCase()) !== -1) {
|
|
||||||
// Update the selected channel incase the casing is different
|
|
||||||
updateChannelName(channel);
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
loadChannels(channels) {
|
|
||||||
_.each(channels, (channel) => {
|
|
||||||
if (channel.server === selectedTab.server &&
|
|
||||||
channel.name !== selectedTab.channel &&
|
|
||||||
channel.name.indexOf(selectedTab.channel) !== -1) {
|
|
||||||
// Handle double hashtag channel names, only a single hashtag
|
|
||||||
// gets added to the channel in the URL on page load
|
|
||||||
updateChannelName(channel.name);
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loadServers(servers) {
|
|
||||||
var server = _.find(servers, { address: selectedTab.server });
|
|
||||||
|
|
||||||
if (server && !selectedTab.channel) {
|
|
||||||
selectedTab = selectedTab.set('name', server.name);
|
|
||||||
history[history.length - 1] = selectedTab;
|
|
||||||
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
navigate(route) {
|
|
||||||
if (route.indexOf('.') === -1 && selectedTab.server) {
|
|
||||||
selectedTab = new Tab();
|
|
||||||
this.trigger(selectedTab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getServer() {
|
|
||||||
return selectedTab.server;
|
|
||||||
},
|
|
||||||
|
|
||||||
getChannel() {
|
|
||||||
return selectedTab.channel;
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: getState,
|
|
||||||
getState
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedTabStore.listen(tab => {
|
|
||||||
var channel = tab.channel;
|
|
||||||
|
|
||||||
actions.hideMenu();
|
|
||||||
|
|
||||||
if (tab.server) {
|
|
||||||
if (channel) {
|
|
||||||
while (channel[0] === '#') {
|
|
||||||
channel = channel.slice(1);
|
|
||||||
}
|
|
||||||
routeActions.navigate('/' + tab.server + '/' + channel);
|
|
||||||
} else {
|
|
||||||
routeActions.navigate('/' + tab.server);
|
|
||||||
}
|
|
||||||
} else if (serverStore.getState().size === 0) {
|
|
||||||
routeActions.navigate('connect');
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.selectedTab = JSON.stringify(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = selectedTabStore;
|
|
@ -1,67 +0,0 @@
|
|||||||
var Reflux = require('reflux');
|
|
||||||
var Immutable = require('immutable');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var actions = require('../actions/server');
|
|
||||||
var tabActions = require('../actions/tab');
|
|
||||||
|
|
||||||
var servers = Immutable.Map();
|
|
||||||
var Server = Immutable.Record({
|
|
||||||
nick: null,
|
|
||||||
name: null
|
|
||||||
});
|
|
||||||
|
|
||||||
function getState() {
|
|
||||||
return servers;
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverStore = Reflux.createStore({
|
|
||||||
init() {
|
|
||||||
this.listenToMany(actions);
|
|
||||||
},
|
|
||||||
|
|
||||||
connect(server, nick, opts) {
|
|
||||||
var i = server.indexOf(':');
|
|
||||||
if (i > 0) {
|
|
||||||
server = server.slice(0, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
servers = servers.set(server, new Server({
|
|
||||||
nick: nick,
|
|
||||||
name: opts.name || server
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.trigger(servers);
|
|
||||||
tabActions.select(server);
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnect(server) {
|
|
||||||
servers = servers.delete(server);
|
|
||||||
this.trigger(servers);
|
|
||||||
},
|
|
||||||
|
|
||||||
setNick(nick, server) {
|
|
||||||
servers = servers.update(server, s => s.set('nick', nick));
|
|
||||||
this.trigger(servers);
|
|
||||||
},
|
|
||||||
|
|
||||||
load(storedServers) {
|
|
||||||
_.each(storedServers, function(server) {
|
|
||||||
servers = servers.set(server.address, new Server(server));
|
|
||||||
});
|
|
||||||
this.trigger(servers);
|
|
||||||
},
|
|
||||||
|
|
||||||
getNick(server) {
|
|
||||||
return servers.getIn([server, 'nick']);
|
|
||||||
},
|
|
||||||
|
|
||||||
getName(server) {
|
|
||||||
return servers.getIn([server, 'name']);
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: getState,
|
|
||||||
getState
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = serverStore;
|
|
@ -1,112 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
exports.UUID = function() {
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
||||||
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.timestamp = function(date) {
|
|
||||||
date = date || new Date();
|
|
||||||
|
|
||||||
var h = _.padLeft(date.getHours(), 2, '0');
|
|
||||||
var m = _.padLeft(date.getMinutes(), 2, '0');
|
|
||||||
|
|
||||||
return h + ':' + m;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.wrapMessages = function(messages, width, charWidth, indent = 0) {
|
|
||||||
return messages.withMutations(m => {
|
|
||||||
for (var j = 0, llen = messages.size; j < llen; j++) {
|
|
||||||
var message = messages.get(j);
|
|
||||||
var lineWidth = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
|
|
||||||
|
|
||||||
if (lineWidth + message.message.length * charWidth < width) {
|
|
||||||
m.setIn([j, 'lines'], [message.message]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var words = message.message.split(' ');
|
|
||||||
var line = '';
|
|
||||||
var wrapped = [];
|
|
||||||
var wordCount = 0;
|
|
||||||
var hasWrapped = false;
|
|
||||||
|
|
||||||
// Add empty line if first word after timestamp + sender wraps
|
|
||||||
if (words.length > 0 && message.from && lineWidth + words[0].length * charWidth >= width) {
|
|
||||||
wrapped.push(line);
|
|
||||||
lineWidth = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0, wlen = words.length; i < wlen; i++) {
|
|
||||||
var word = words[i];
|
|
||||||
|
|
||||||
if (hasWrapped) {
|
|
||||||
hasWrapped = false;
|
|
||||||
lineWidth += indent;
|
|
||||||
}
|
|
||||||
|
|
||||||
lineWidth += word.length * charWidth;
|
|
||||||
wordCount++;
|
|
||||||
|
|
||||||
if (lineWidth >= width) {
|
|
||||||
if (wordCount !== 1) {
|
|
||||||
wrapped.push(line);
|
|
||||||
|
|
||||||
if (i !== wlen - 1) {
|
|
||||||
line = word + ' ';
|
|
||||||
lineWidth = (word.length + 1) * charWidth;
|
|
||||||
wordCount = 1;
|
|
||||||
} else {
|
|
||||||
wrapped.push(word);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wrapped.push(word);
|
|
||||||
lineWidth = 0;
|
|
||||||
wordCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasWrapped = true;
|
|
||||||
} else if (i !== wlen - 1) {
|
|
||||||
line += word + ' ';
|
|
||||||
lineWidth += charWidth;
|
|
||||||
} else {
|
|
||||||
line += word;
|
|
||||||
wrapped.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.setIn([j, 'lines'], wrapped);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
var ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
exports.stringWidth = function(str, font) {
|
|
||||||
ctx.font = font;
|
|
||||||
return ctx.measureText(str).width;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.scrollbarWidth = function() {
|
|
||||||
var outer = document.createElement('div');
|
|
||||||
outer.style.visibility = 'hidden';
|
|
||||||
outer.style.width = '100px';
|
|
||||||
|
|
||||||
document.body.appendChild(outer);
|
|
||||||
|
|
||||||
var widthNoScroll = outer.offsetWidth;
|
|
||||||
outer.style.overflow = 'scroll';
|
|
||||||
|
|
||||||
var inner = document.createElement('div');
|
|
||||||
inner.style.width = '100%';
|
|
||||||
outer.appendChild(inner);
|
|
||||||
|
|
||||||
var widthWithScroll = inner.offsetWidth;
|
|
||||||
|
|
||||||
outer.parentNode.removeChild(outer);
|
|
||||||
|
|
||||||
return widthNoScroll - widthWithScroll;
|
|
||||||
};
|
|
83
client/src/js/util/Socket.js
Normal file
83
client/src/js/util/Socket.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import EventEmitter2 from 'eventemitter2';
|
||||||
|
import Backoff from 'backo';
|
||||||
|
|
||||||
|
export default class Socket extends EventEmitter2 {
|
||||||
|
constructor(address) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.address = address;
|
||||||
|
this.connectTimeout = 20000;
|
||||||
|
this.pingTimeout = 30000;
|
||||||
|
this.backoff = new Backoff({
|
||||||
|
min: 1000,
|
||||||
|
max: 5000,
|
||||||
|
jitter: 0.25
|
||||||
|
});
|
||||||
|
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.ws = new WebSocket(`ws://${this.address}/ws`);
|
||||||
|
|
||||||
|
this.timeoutConnect = setTimeout(() => {
|
||||||
|
this.ws.close();
|
||||||
|
this.retry();
|
||||||
|
}, this.connectTimeout);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
clearTimeout(this.timeoutConnect);
|
||||||
|
this.backoff.reset();
|
||||||
|
this.emit('connect');
|
||||||
|
this.setTimeoutPing();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
clearTimeout(this.timeoutConnect);
|
||||||
|
clearTimeout(this.timeoutPing);
|
||||||
|
if (!this.closing) {
|
||||||
|
this.emit('disconnect');
|
||||||
|
this.retry();
|
||||||
|
}
|
||||||
|
this.closing = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = () => {
|
||||||
|
clearTimeout(this.timeoutConnect);
|
||||||
|
clearTimeout(this.timeoutPing);
|
||||||
|
this.closing = true;
|
||||||
|
this.ws.close();
|
||||||
|
this.retry();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (e) => {
|
||||||
|
this.setTimeoutPing();
|
||||||
|
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (msg.type === 'ping') {
|
||||||
|
this.send('pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit(msg.type, msg.data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
retry() {
|
||||||
|
setTimeout(() => this.connect(), this.backoff.duration());
|
||||||
|
}
|
||||||
|
|
||||||
|
send(type, data) {
|
||||||
|
this.ws.send(JSON.stringify({ type, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeoutPing() {
|
||||||
|
clearTimeout(this.timeoutPing);
|
||||||
|
this.timeoutPing = setTimeout(() => {
|
||||||
|
this.emit('disconnect');
|
||||||
|
this.closing = true;
|
||||||
|
this.ws.close();
|
||||||
|
this.connect();
|
||||||
|
}, this.pingTimeout);
|
||||||
|
}
|
||||||
|
}
|
8
client/src/js/util/createReducer.js
Normal file
8
client/src/js/util/createReducer.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function createReducer(initialState, handlers) {
|
||||||
|
return function reducer(state = initialState, action) {
|
||||||
|
if (handlers.hasOwnProperty(action.type)) {
|
||||||
|
return handlers[action.type](state, action);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
}
|
54
client/src/js/util/index.js
Normal file
54
client/src/js/util/index.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { padLeft } from 'lodash';
|
||||||
|
|
||||||
|
export wrapMessages from './wrapMessages';
|
||||||
|
|
||||||
|
export function normalizeChannel(channel) {
|
||||||
|
if (channel.indexOf('#') !== 0) {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel.split('#').join('').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUUID() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timestamp(date = new Date()) {
|
||||||
|
const h = padLeft(date.getHours(), 2, '0');
|
||||||
|
const m = padLeft(date.getMinutes(), 2, '0');
|
||||||
|
return h + ':' + m;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
export function stringWidth(str, font) {
|
||||||
|
ctx.font = font;
|
||||||
|
return ctx.measureText(str).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollbarWidth() {
|
||||||
|
const outer = document.createElement('div');
|
||||||
|
outer.style.visibility = 'hidden';
|
||||||
|
outer.style.width = '100px';
|
||||||
|
|
||||||
|
document.body.appendChild(outer);
|
||||||
|
|
||||||
|
const widthNoScroll = outer.offsetWidth;
|
||||||
|
outer.style.overflow = 'scroll';
|
||||||
|
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.style.width = '100%';
|
||||||
|
outer.appendChild(inner);
|
||||||
|
|
||||||
|
const widthWithScroll = inner.offsetWidth;
|
||||||
|
|
||||||
|
outer.parentNode.removeChild(outer);
|
||||||
|
|
||||||
|
return widthNoScroll - widthWithScroll;
|
||||||
|
}
|
65
client/src/js/util/wrapMessages.js
Normal file
65
client/src/js/util/wrapMessages.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
export default function wrapMessages(messages, width, charWidth, indent = 0) {
|
||||||
|
return messages.withMutations(m => {
|
||||||
|
for (let j = 0, llen = messages.size; j < llen; j++) {
|
||||||
|
const message = messages.get(j);
|
||||||
|
let lineWidth = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
|
||||||
|
|
||||||
|
if (lineWidth + message.message.length * charWidth < width) {
|
||||||
|
m.setIn([j, 'lines'], [message.message]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const words = message.message.split(' ');
|
||||||
|
const wrapped = [];
|
||||||
|
let line = '';
|
||||||
|
let wordCount = 0;
|
||||||
|
let hasWrapped = false;
|
||||||
|
|
||||||
|
// Add empty line if first word after timestamp + sender wraps
|
||||||
|
if (words.length > 0 && message.from && lineWidth + words[0].length * charWidth >= width) {
|
||||||
|
wrapped.push(line);
|
||||||
|
lineWidth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0, wlen = words.length; i < wlen; i++) {
|
||||||
|
const word = words[i];
|
||||||
|
|
||||||
|
if (hasWrapped) {
|
||||||
|
hasWrapped = false;
|
||||||
|
lineWidth += indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineWidth += word.length * charWidth;
|
||||||
|
wordCount++;
|
||||||
|
|
||||||
|
if (lineWidth >= width) {
|
||||||
|
if (wordCount !== 1) {
|
||||||
|
wrapped.push(line);
|
||||||
|
|
||||||
|
if (i !== wlen - 1) {
|
||||||
|
line = word + ' ';
|
||||||
|
lineWidth = (word.length + 1) * charWidth;
|
||||||
|
wordCount = 1;
|
||||||
|
} else {
|
||||||
|
wrapped.push(word);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wrapped.push(word);
|
||||||
|
lineWidth = 0;
|
||||||
|
wordCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWrapped = true;
|
||||||
|
} else if (i !== wlen - 1) {
|
||||||
|
line += word + ' ';
|
||||||
|
lineWidth += charWidth;
|
||||||
|
} else {
|
||||||
|
line += word;
|
||||||
|
wrapped.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.setIn([j, 'lines'], wrapped);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
30
client/webpack.config.dev.js
Normal file
30
client/webpack.config.dev.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: 'eval',
|
||||||
|
entry: [
|
||||||
|
'webpack-hot-middleware/client',
|
||||||
|
'./src/js/index'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
publicPath: '/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
preLoaders: [
|
||||||
|
{ test: /\.js$/, loader: 'eslint', exclude: /node_modules/ }
|
||||||
|
],
|
||||||
|
loaders: [
|
||||||
|
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
__DEV__: true,
|
||||||
|
}),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.NoErrorsPlugin()
|
||||||
|
]
|
||||||
|
};
|
38
client/webpack.config.prod.js
Normal file
38
client/webpack.config.prod.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: [
|
||||||
|
'./src/js/index'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
publicPath: '/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
preLoaders: [
|
||||||
|
{ test: /\.js$/, loader: 'eslint', exclude: /node_modules/ }
|
||||||
|
],
|
||||||
|
loaders: [
|
||||||
|
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
__DEV__: false,
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: JSON.stringify('production')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new webpack.optimize.DedupePlugin(),
|
||||||
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
compress: {
|
||||||
|
warnings: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -52,6 +52,7 @@ func (i *ircHandler) nick(msg *irc.Message) {
|
|||||||
Server: i.client.Host,
|
Server: i.client.Host,
|
||||||
Old: msg.Nick,
|
Old: msg.Nick,
|
||||||
New: msg.Trailing,
|
New: msg.Trailing,
|
||||||
|
Channels: channelStore.FindUserChannels(msg.Nick, i.client.Host),
|
||||||
})
|
})
|
||||||
|
|
||||||
channelStore.RenameUser(msg.Nick, msg.Trailing, i.client.Host)
|
channelStore.RenameUser(msg.Nick, msg.Trailing, i.client.Host)
|
||||||
@ -129,6 +130,7 @@ func (i *ircHandler) quit(msg *irc.Message) {
|
|||||||
Server: i.client.Host,
|
Server: i.client.Host,
|
||||||
User: msg.Nick,
|
User: msg.Nick,
|
||||||
Reason: msg.Trailing,
|
Reason: msg.Trailing,
|
||||||
|
Channels: channelStore.FindUserChannels(msg.Nick, i.client.Host),
|
||||||
})
|
})
|
||||||
|
|
||||||
channelStore.RemoveUserAll(msg.Nick, i.client.Host)
|
channelStore.RemoveUserAll(msg.Nick, i.client.Host)
|
||||||
|
@ -30,6 +30,7 @@ type Nick struct {
|
|||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
Old string `json:"old"`
|
Old string `json:"old"`
|
||||||
New string `json:"new"`
|
New string `json:"new"`
|
||||||
|
Channels []string `json:"channels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Join struct {
|
type Join struct {
|
||||||
@ -55,6 +56,7 @@ type Quit struct {
|
|||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Channels []string `json:"channels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
|
@ -95,6 +95,23 @@ func (c *ChannelStore) SetMode(server, channel, user, add, remove string) {
|
|||||||
c.userLock.Unlock()
|
c.userLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ChannelStore) FindUserChannels(user, server string) []string {
|
||||||
|
var channels []string
|
||||||
|
|
||||||
|
c.userLock.Lock()
|
||||||
|
for channel, users := range c.users[server] {
|
||||||
|
for _, nick := range users {
|
||||||
|
if user == nick {
|
||||||
|
channels = append(channels, channel)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.userLock.Unlock()
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChannelStore) GetTopic(server, channel string) string {
|
func (c *ChannelStore) GetTopic(server, channel string) string {
|
||||||
c.topicLock.Lock()
|
c.topicLock.Lock()
|
||||||
defer c.topicLock.Unlock()
|
defer c.topicLock.Unlock()
|
||||||
|
@ -57,3 +57,12 @@ func TestTopic(t *testing.T) {
|
|||||||
channelStore.SetTopic("the topic", "srv", "#chan")
|
channelStore.SetTopic("the topic", "srv", "#chan")
|
||||||
assert.Equal(t, "the topic", channelStore.GetTopic("srv", "#chan"))
|
assert.Equal(t, "the topic", channelStore.GetTopic("srv", "#chan"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindUserChannels(t *testing.T) {
|
||||||
|
channelStore := NewChannelStore()
|
||||||
|
channelStore.AddUser("user", "srv", "#chan1")
|
||||||
|
channelStore.AddUser("user", "srv", "#chan2")
|
||||||
|
channelStore.AddUser("user2", "srv", "#chan3")
|
||||||
|
channelStore.AddUser("user", "srv2", "#chan4")
|
||||||
|
assert.Equal(t, []string{"#chan1", "#chan2"}, channelStore.FindUserChannels("user", "srv"))
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user