├── packages ├── p2p-chat-utils │ ├── noop.js │ ├── has.js │ ├── arr2keys.js │ ├── pickBy.js │ ├── ensure-unique-file.js │ ├── event-all.js │ ├── package.json │ ├── parse-chunks.js │ ├── EventObservable.js │ ├── is-ip-larger.js │ ├── each.js │ ├── get-new-address.js │ ├── event-replaceable.js │ ├── index.js │ ├── pickByMap.js │ ├── get-port.js │ ├── md5.js │ ├── ensure-unique-filename.js │ └── ipset.js ├── p2p-chat-core │ ├── .eslintrc │ ├── index.js │ ├── lib │ │ ├── defaultOpts.js │ │ ├── msgTypes.js │ │ ├── ensureMergeIPset.js │ │ ├── getTag.js │ │ ├── Store.js │ │ ├── enhanceSocket │ │ │ ├── index.js │ │ │ └── Parse.js │ │ ├── login.js │ │ ├── connect.js │ │ ├── fileHanderMakers.js │ │ ├── actions.js │ │ ├── fileInfoPool.js │ │ ├── Chat.js │ │ └── socketHandler.js │ └── package.json ├── p2p-chat │ ├── .gitignore │ ├── src │ │ ├── components │ │ │ ├── Aside │ │ │ │ ├── ChatList │ │ │ │ │ ├── ChatList.scss │ │ │ │ │ ├── DialogType.js │ │ │ │ │ ├── redux.js │ │ │ │ │ └── index.js │ │ │ │ └── ListItem │ │ │ │ │ ├── ListItem.scss │ │ │ │ │ └── index.js │ │ │ ├── Settings │ │ │ │ ├── MyInfo │ │ │ │ │ ├── MyInfo.scss │ │ │ │ │ └── index.js │ │ │ │ ├── validators.js │ │ │ │ ├── Login │ │ │ │ │ ├── redux.js │ │ │ │ │ └── index.js │ │ │ │ ├── ConnectRange │ │ │ │ │ └── index.js │ │ │ │ ├── Connect │ │ │ │ │ └── index.js │ │ │ │ └── CreateChannel │ │ │ │ │ └── index.js │ │ │ ├── Chatting │ │ │ │ ├── FilePanel │ │ │ │ │ ├── FileReceive.scss │ │ │ │ │ ├── FilePanel.scss │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── FileInfo.js │ │ │ │ │ ├── FileReceive.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── redux.js │ │ │ │ ├── Messages │ │ │ │ │ ├── Messages.scss │ │ │ │ │ ├── Text.scss │ │ │ │ │ ├── Text.js │ │ │ │ │ └── index.js │ │ │ │ └── Dialog │ │ │ │ │ ├── Dialog.scss │ │ │ │ │ ├── subscribe.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── redux.js │ │ │ └── Common │ │ │ │ ├── CustomCard │ │ │ │ └── index.js │ │ │ │ └── Modal │ │ │ │ ├── index.scss │ │ │ │ └── index.js │ │ ├── views │ │ │ ├── Settings.scss │ │ │ ├── SettingsRedux.js │ │ │ ├── AsideRedux.js │ │ │ ├── ChattingRedux.js │ │ │ ├── ModalBtn.js │ │ │ ├── ModalBtnRedux.js │ │ │ ├── Aside.js │ │ │ ├── Chatting.js │ │ │ └── Settings.js │ │ ├── global │ │ │ ├── devToolsMiddleware.js │ │ │ ├── render.js │ │ │ ├── subscribe.js │ │ │ ├── reducer.js │ │ │ ├── redux.js │ │ │ └── ipc.js │ │ ├── utils │ │ │ ├── constants.js │ │ │ ├── getNewState.js │ │ │ ├── storage.js │ │ │ ├── createReducer.js │ │ │ ├── message.js │ │ │ ├── watchState.js │ │ │ ├── hot.js │ │ │ └── format.js │ │ ├── index.ejs │ │ ├── layouts │ │ │ ├── DevTools.js │ │ │ ├── global.scss │ │ │ └── App.js │ │ ├── index.js │ │ └── selectors │ │ │ └── chatInfo.js │ ├── build │ │ ├── icon.ico │ │ ├── icons │ │ │ ├── 32x32.png │ │ │ └── 512x512.png │ │ └── build-win-portable.js │ ├── main │ │ ├── count.js │ │ ├── makePlainError.js │ │ ├── handleError.js │ │ ├── loadUserConf.js │ │ ├── setContextMenu.js │ │ ├── menu.js │ │ ├── worker.js │ │ └── index.js │ ├── config │ │ ├── generateScopedName.js │ │ ├── analyzer.js │ │ ├── dep-externals.js │ │ ├── css.js │ │ ├── devServer.js │ │ ├── main.config.js │ │ └── renderer.config.js │ ├── jsconfig.json │ ├── webpack.config.js │ └── package.json └── p2p-chat-logger │ ├── package.json │ ├── README.md │ ├── index.d.ts │ └── index.js ├── .eslintignore ├── .prettierignore ├── postcss.config.js ├── .eslintrc ├── package.json ├── LICENSE ├── .gitignore └── README.md /packages/p2p-chat-utils/noop.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dgeibi" 3 | } 4 | -------------------------------------------------------------------------------- /packages/p2p-chat/.gitignore: -------------------------------------------------------------------------------- 1 | /index.js* 2 | /worker.js* 3 | report.html -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Aside/ChatList/ChatList.scss: -------------------------------------------------------------------------------- 1 | .menu { 2 | border-right: 0; 3 | } 4 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/index.js: -------------------------------------------------------------------------------- 1 | const Chat = require('./lib/Chat') 2 | 3 | module.exports = new Chat() 4 | -------------------------------------------------------------------------------- /packages/p2p-chat/build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgeibi/p2p-chat/HEAD/packages/p2p-chat/build/icon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | assets/ 4 | public/ 5 | /packages/p2p-chat/index.js* 6 | /packages/p2p-chat/worker.js* -------------------------------------------------------------------------------- /packages/p2p-chat/src/views/Settings.scss: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: 10px 11px; 3 | border-bottom: 1px solid #eee; 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | assets/ 4 | public/ 5 | /packages/p2p-chat/index.js* 6 | /packages/p2p-chat/worker.js* -------------------------------------------------------------------------------- /packages/p2p-chat/build/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgeibi/p2p-chat/HEAD/packages/p2p-chat/build/icons/32x32.png -------------------------------------------------------------------------------- /packages/p2p-chat/build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgeibi/p2p-chat/HEAD/packages/p2p-chat/build/icons/512x512.png -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Aside/ChatList/DialogType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | USER: 'user', 3 | CHANNEL: 'channel', 4 | } 5 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/global/devToolsMiddleware.js: -------------------------------------------------------------------------------- 1 | import DevTools from '../layouts/DevTools' 2 | 3 | export default DevTools.instrument() 4 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/lib/defaultOpts.js: -------------------------------------------------------------------------------- 1 | const defaultOpts = { 2 | username: 'anonymous', 3 | port: 8087, 4 | } 5 | 6 | module.exports = defaultOpts 7 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/has.js: -------------------------------------------------------------------------------- 1 | const hasOwn = Object.prototype.hasOwnProperty 2 | const has = (obj, key) => hasOwn.call(obj, key) 3 | module.exports = has 4 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Settings/MyInfo/MyInfo.scss: -------------------------------------------------------------------------------- 1 | .my-info { 2 | color: rgba(0, 0, 0, 0.85); 3 | margin-top: 5px; 4 | padding-left: 2px; 5 | } 6 | -------------------------------------------------------------------------------- /packages/p2p-chat/main/count.js: -------------------------------------------------------------------------------- 1 | export default function count(limit = 4) { 2 | let tick = 0 3 | return () => { 4 | if (tick > limit) return false 5 | tick += 1 6 | return true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/p2p-chat/config/generateScopedName.js: -------------------------------------------------------------------------------- 1 | module.exports = (localName, resourcePath) => { 2 | const componentName = resourcePath.split('/').slice(-2, -1) 3 | return `${componentName}_${localName}` 4 | } 5 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export default (constants = {}, prefix) => { 2 | Object.keys(constants).forEach(key => { 3 | constants[key] = `${prefix}/${key}` // eslint-disable-line 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/arr2keys.js: -------------------------------------------------------------------------------- 1 | module.exports = function arr2keys(arr) { 2 | return arr.reduce((obj, key) => { 3 | obj[key] = 1 // eslint-disable-line no-param-reassign 4 | return obj 5 | }, {}) 6 | } 7 | -------------------------------------------------------------------------------- /packages/p2p-chat/main/makePlainError.js: -------------------------------------------------------------------------------- 1 | export default err => { 2 | if (!err) return null 3 | const { message, stack, name, code, errno, syscall } = err 4 | return { message, stack, name, code, errno, syscall, ...err } 5 | } 6 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/pickBy.js: -------------------------------------------------------------------------------- 1 | const pickBy = (src, testFn) => 2 | Object.keys(src).reduce((o, k) => { 3 | if (testFn(src[k], k, src)) o[k] = src[k] // eslint-disable-line 4 | return o 5 | }, {}) 6 | module.exports = pickBy 7 | -------------------------------------------------------------------------------- /packages/p2p-chat/main/handleError.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | 3 | process.on('uncaughtException', err => { 4 | electron.dialog.showErrorBox( 5 | 'A JavaScript error occurred in the main process', 6 | err.stack 7 | ) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/views/SettingsRedux.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import login, * as loginActions from '../components/Settings/Login/redux' 4 | 5 | export default combineReducers({ login }) 6 | export { loginActions } 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ file, env }) => ({ 2 | parser: file.extname === '.scss' ? 'postcss-scss' : false, 3 | plugins: { 4 | precss: file.extname === '.scss', 5 | cssnano: env === 'production' ? {} : false, 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/views/AsideRedux.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import chatList, * as chatListActions from '../components/Aside/ChatList/redux' 4 | 5 | export default combineReducers({ chatList }) 6 | export { chatListActions } 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "dgeibi/react" 4 | ], 5 | "rules": { 6 | "import/no-extraneous-dependencies": "off", 7 | "class-methods-use-this": "off", 8 | "global-require": "off", 9 | "import/first": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/FilePanel/FileReceive.scss: -------------------------------------------------------------------------------- 1 | .filename { 2 | font-weight: 700; 3 | } 4 | 5 | .card > div { 6 | white-space: nowrap; 7 | word-break: break-all; 8 | text-overflow: ellipsis; 9 | overflow: hidden; 10 | } 11 | -------------------------------------------------------------------------------- /packages/p2p-chat/config/analyzer.js: -------------------------------------------------------------------------------- 1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') 2 | 3 | module.exports = config => { 4 | config.plugin(BundleAnalyzerPlugin, [ 5 | { 6 | analyzerMode: 'static', 7 | }, 8 | ]) 9 | } 10 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/Messages/Messages.scss: -------------------------------------------------------------------------------- 1 | .messages { 2 | flex-grow: 1; 3 | overflow-y: auto; 4 | overflow-x: hidden; 5 | margin-bottom: 16px; 6 | padding: 0 5px 0 0; 7 | } 8 | 9 | .alert { 10 | margin-bottom: 10px; 11 | } 12 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/getNewState.js: -------------------------------------------------------------------------------- 1 | const getNewState = (state, key, ...restKeys) => { 2 | const newState = Object.assign({}, state) 3 | if (key) newState[key] = getNewState(newState[key], ...restKeys) 4 | return newState 5 | } 6 | 7 | export default getNewState 8 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/lib/msgTypes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CHANNEL_CREATE: 'channel-create', 3 | FILE_ACCEPTED: 'file-accepted', 4 | TEXT: 'text', 5 | FILEINFO: 'fileinfo', 6 | FILE: 'file', 7 | GREETING: 'greeting', 8 | GREETING_REPLY: 'greeting-reply', 9 | } 10 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Common/CustomCard/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card } from 'antd' 3 | 4 | const style = { 5 | width: '150px', 6 | padding: '8px 10px', 7 | overflow: 'hidden', 8 | } 9 | export default props => 10 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Common/Modal/index.scss: -------------------------------------------------------------------------------- 1 | .modal :global .ant-modal-body { 2 | max-height: calc(100vh - 130px); 3 | overflow-y: auto; 4 | } 5 | 6 | .modal { 7 | top: 15px; 8 | width: 520px !important; 9 | margin: 0 auto !important; 10 | padding-bottom: 0; 11 | } 12 | -------------------------------------------------------------------------------- /packages/p2p-chat-logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p2p-chat-logger", 3 | "version": "1.4.5", 4 | "description": "logger", 5 | "private": true, 6 | "main": "index.js", 7 | "author": "dgeibi", 8 | "license": "MIT", 9 | "dependencies": { 10 | "chalk": "^2.0.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/storage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | set(key, value) { 3 | localStorage.setItem(key, JSON.stringify(value)) 4 | }, 5 | get(key) { 6 | try { 7 | return JSON.parse(localStorage.getItem(key)) 8 | } catch (e) { 9 | return null 10 | } 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Aside/ListItem/ListItem.scss: -------------------------------------------------------------------------------- 1 | .latest__badge { 2 | color: #fff; 3 | padding: 0 6px; 4 | font-size: 12px; 5 | border-radius: 10px; 6 | height: 20px; 7 | font-family: tahoma; 8 | background: #87d068; 9 | } 10 | 11 | .title-offline.title { 12 | color: grey; 13 | } 14 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/FilePanel/FilePanel.scss: -------------------------------------------------------------------------------- 1 | .filePanel { 2 | padding: 5px; 3 | display: flex; 4 | height: 120px; 5 | overflow-x: auto; 6 | } 7 | 8 | .clearWrapper { 9 | padding: 0 3px; 10 | display: inline-flex; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/FilePanel/constants.js: -------------------------------------------------------------------------------- 1 | export const fileLoadStates = { 2 | success: 'success', 3 | waitting: 'waitting', 4 | active: 'active', 5 | exception: 'exception', 6 | } 7 | 8 | export const cardTypes = { 9 | RECEIVE: 'file:receive', 10 | INFO: 'file:info', 11 | } 12 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/global/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from '../layouts/App' 4 | 5 | export default (store, history) => { 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/ensure-unique-file.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const ensureFilename = require('./ensure-unique-filename') 3 | 4 | module.exports = pathname => { 5 | if (!pathname) throw Error('should pass a pathname') 6 | const path = ensureFilename(pathname) 7 | fs.ensureFileSync(path) 8 | return path 9 | } 10 | -------------------------------------------------------------------------------- /packages/p2p-chat/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "experimentalDecorators": true, 5 | "jsx": "react", 6 | "allowSyntheticDefaultImports": true, 7 | "module": "commonjs", 8 | }, 9 | "include": [ 10 | "src/**/*" 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/createReducer.js: -------------------------------------------------------------------------------- 1 | import has from 'p2p-chat-utils/has' 2 | 3 | const createReducer = (reducerMap, initialState) => (state = initialState, action) => { 4 | if (has(reducerMap, action.type)) { 5 | return reducerMap[action.type](state, action) 6 | } 7 | return state 8 | } 9 | 10 | export default createReducer 11 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/views/ChattingRedux.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import dialog, * as dialogActions from '../components/Chatting/Dialog/redux' 4 | import filePanel, * as filePanelActions from '../components/Chatting/FilePanel/redux' 5 | 6 | export default combineReducers({ dialog, filePanel }) 7 | export { dialogActions, filePanelActions } 8 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/event-all.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Array} meta 3 | */ 4 | module.exports = (meta, callback) => { 5 | const stats = meta.map(([emitter, name], index) => { 6 | emitter.once(name, () => { 7 | stats[index] = true 8 | if (stats.every(x => x)) { 9 | callback() 10 | } 11 | }) 12 | 13 | return false 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p2p-chat-utils", 3 | "version": "1.4.3", 4 | "description": "", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "dgeibi", 11 | "license": "MIT", 12 | "dependencies": { 13 | "fs-extra": "^5.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/p2p-chat/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (env = {}) => { 2 | const mode = env.production ? 'production' : 'development' 3 | process.env.NODE_ENV = mode 4 | 5 | return [require('./config/renderer.config'), require('./config/main.config')].map(x => { 6 | if (typeof x === 'function') { 7 | x = x(env) // eslint-disable-line 8 | } 9 | return x 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/parse-chunks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a stream to parse from ndjson-body. 3 | * @param {Array} chunks - array of buffers 4 | * @returns {?object} 5 | */ 6 | const parseChunks = chunks => { 7 | const buffer = Buffer.concat(chunks) 8 | try { 9 | return JSON.parse(buffer.toString()) 10 | } catch (e) { 11 | return null 12 | } 13 | } 14 | 15 | module.exports = parseChunks 16 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/layouts/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/message.js: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | 3 | export function showError(error) { 4 | if (error && typeof error === 'object') { 5 | message.error(error.message) 6 | console.error(error.stack) // eslint-disable-line 7 | } else if (typeof error === 'string') { 8 | message.error(error) 9 | } 10 | } 11 | 12 | export function showInfo(msg) { 13 | message.info(msg) 14 | } 15 | -------------------------------------------------------------------------------- /packages/p2p-chat/main/loadUserConf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import Store from 'electron-store' 3 | 4 | export default locals => { 5 | const userConf = new Store({ 6 | name: locals.tag, 7 | cwd: locals.settingsDir, 8 | }) 9 | locals.users = userConf.get('users', {}) 10 | locals.channels = userConf.get('channels', {}) 11 | locals.userConf = userConf 12 | 13 | return userConf 14 | } 15 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Common/Modal/index.js: -------------------------------------------------------------------------------- 1 | import { Modal as AntdModal } from 'antd' 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | import styles from './index.scss' 5 | 6 | const Modal = ({ className, ...props }) => ( 7 | 8 | ) 9 | 10 | Modal.propTypes = { 11 | className: PropTypes.string, 12 | } 13 | 14 | export default Modal 15 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/lib/ensureMergeIPset.js: -------------------------------------------------------------------------------- 1 | const IPset = require('p2p-chat-utils/ipset') 2 | 3 | module.exports = function ensureMergeIPset(payload) { 4 | const { ipset, ipsetMerged, ipsetStore } = payload 5 | const ret = {} 6 | if (ipset) { 7 | ret.ipset = ipsetMerged ? ipset : ipset.mergeStore(ipsetStore) 8 | ret.ipsetMerged = true 9 | } else { 10 | ret.ipset = IPset(ipsetStore) 11 | } 12 | return Object.assign(payload, ret) 13 | } 14 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/lib/getTag.js: -------------------------------------------------------------------------------- 1 | const md5 = require('p2p-chat-utils/md5') 2 | const { machineIdSync } = require('node-machine-id') 3 | 4 | const machineId = machineIdSync({ original: true }) 5 | 6 | /** 7 | * Get tag according to port, host, username 8 | * @param {number} port 9 | * @param {string} username 10 | * @returns {string} 11 | */ 12 | module.exports = function getTag(port, username) { 13 | return md5.dataSync(machineId + port + username) 14 | } 15 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/Messages/Text.scss: -------------------------------------------------------------------------------- 1 | .text { 2 | margin: 16px 2px; 3 | padding: 5px; 4 | border-radius: 7px; 5 | background: #fff; 6 | box-shadow: 0 0 0.2px 0.6px rgba(0, 0, 0, 0.2); 7 | } 8 | 9 | .text__header { 10 | color: #346fb5; 11 | } 12 | 13 | .text__footer { 14 | text-align: right; 15 | color: rgba(0, 0, 0, 0.34); 16 | } 17 | 18 | .text__main { 19 | font-size: 1.2em; 20 | white-space: pre-wrap; 21 | word-break: break-all; 22 | } 23 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/index.js: -------------------------------------------------------------------------------- 1 | import { push } from 'connected-react-router' 2 | 3 | import { configureStore, history } from './global/redux' 4 | import ipc from './global/ipc' 5 | import render from './global/render' 6 | import subscribe from './global/subscribe' 7 | 8 | const store = configureStore() 9 | 10 | render(store, history) 11 | ipc(store.dispatch) 12 | subscribe(store) 13 | 14 | // redirect to root 15 | if (history.location.pathname !== '/') store.dispatch(push('/')) 16 | -------------------------------------------------------------------------------- /packages/p2p-chat/config/dep-externals.js: -------------------------------------------------------------------------------- 1 | const has = require('p2p-chat-utils/has') 2 | 3 | const matchExternals = (req, dependencies) => { 4 | if (has(dependencies, req)) return true 5 | if (has(dependencies, req.split('/')[0])) return true 6 | return false 7 | } 8 | 9 | module.exports = dependencies => (context, request, callback) => { 10 | if (matchExternals(request, dependencies)) { 11 | callback(null, `commonjs ${request}`) 12 | return 13 | } 14 | callback() 15 | } 16 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/Dialog/Dialog.scss: -------------------------------------------------------------------------------- 1 | .dialog { 2 | display: flex; 3 | height: 100vh; 4 | flex-direction: column; 5 | padding: 10px 16px; 6 | } 7 | 8 | .text { 9 | width: 100%; 10 | font-size: 1.1em; 11 | margin: 15px 0; 12 | resize: none; 13 | } 14 | 15 | .send-btn-div { 16 | text-align: right; 17 | } 18 | 19 | .user-info { 20 | color: rgba(0, 0, 0, 0.85); 21 | padding: 8px 0 8px 8px; 22 | line-height: 22px; 23 | border-bottom: 1px solid #d9d9d9; 24 | } 25 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/EventObservable.js: -------------------------------------------------------------------------------- 1 | module.exports = emitter => { 2 | const observables = [] 3 | return { 4 | observe(type, listener) { 5 | const destory = () => { 6 | emitter.removeListener(type, listener) 7 | } 8 | emitter.on(type, listener) 9 | observables.push(destory) 10 | return destory 11 | }, 12 | removeAllObservables() { 13 | observables.forEach(x => { 14 | x() 15 | }) 16 | observables.splice(0) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/is-ip-larger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * compare IPs: check whether `a` is larger than `b` 3 | * @param {string} a 4 | * @param {string} b 5 | * @returns {boolean} 6 | */ 7 | function isIPLarger(a, b) { 8 | const aParts = a.split('.').map(Number) 9 | const bParts = b.split('.').map(Number) 10 | return aParts.reduce((isLarger, num, index) => { 11 | if (isLarger) return true 12 | if (num > bParts[index]) return true 13 | return false 14 | }, false) 15 | } 16 | 17 | module.exports = isIPLarger 18 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/Dialog/subscribe.js: -------------------------------------------------------------------------------- 1 | import { throttle } from 'lodash' 2 | 3 | import watchState from '../../../utils/watchState' 4 | import storage from '../../../utils/storage' 5 | 6 | export default watchState( 7 | ['chatting', 'dialog'], 8 | throttle( 9 | (_dialog, state) => { 10 | const { logined, tag } = state.settings.login 11 | if (logined) { 12 | storage.set(`dialog-${tag}`, _dialog) 13 | } 14 | }, 15 | 200, 16 | { leading: true } 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/each.js: -------------------------------------------------------------------------------- 1 | const fn = (object, callback) => key => { 2 | const value = object[key] 3 | if (value !== undefined) { 4 | callback(value) 5 | } 6 | } 7 | 8 | module.exports = function each(object, keys, callback) { 9 | if (!object || typeof object !== 'object') return 10 | 11 | if (typeof keys === 'function') { 12 | Object.keys(object).forEach(fn(object, keys)) 13 | } else if (typeof callback === 'function' && Array.isArray(keys)) { 14 | keys.forEach(fn(object, callback)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/p2p-chat/config/css.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ rule, extract }) => config => { 2 | if (!rule || !rule.use) { 3 | throw Error('rule and rule.use required') 4 | } 5 | if (extract) { 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') // eslint-disable-line global-require 7 | const use = [MiniCssExtractPlugin.loader, ...rule.use] 8 | config.rule(Object.assign({}, rule, { use })) 9 | } else { 10 | const use = ['style-loader', ...rule.use] 11 | config.rule(Object.assign({}, rule, { use })) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/p2p-chat-logger/README.md: -------------------------------------------------------------------------------- 1 | # logger 2 | 3 | a wrapper of `chalk` 4 | 5 | ## Install 6 | 7 | ``` 8 | $ npm install -S dgeibi/logger 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` js 14 | const logger = require('logger'); 15 | 16 | logger.error(Error('something wrong')); 17 | ``` 18 | 19 | ## Methods 20 | 21 | - `logger.log`: `console.log` 22 | - `logger.warn`: yellow 23 | - `logger.error`: red 24 | - `logger.err`: red 25 | - `logger.info`: green 26 | - `logger.debug`: blue 27 | - `logger.verbose`: cyan 28 | 29 | ## LICENSE 30 | 31 | [MIT](LICENSE) 32 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/watchState.js: -------------------------------------------------------------------------------- 1 | const safeValue = (obj, path) => 2 | path.reduce((s, p) => { 3 | if (!s || s === true) return undefined 4 | const v = s[p] 5 | if (v !== undefined) return v 6 | return undefined 7 | }, obj) 8 | 9 | const watchState = (path, callback) => { 10 | let cache 11 | return obj => { 12 | const value = safeValue(obj, path) 13 | if (value !== undefined && cache !== value) { 14 | cache = value 15 | callback(value, obj) 16 | } 17 | } 18 | } 19 | 20 | export default watchState 21 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/get-new-address.js: -------------------------------------------------------------------------------- 1 | function getParts(parts, zero = true) { 2 | const last = parts[parts.length - 1] 3 | if (last < 254) return [...parts.slice(0, parts.length - 1), last + 1] 4 | return [...getParts(parts.slice(0, parts.length - 1)), zero ? 0 : 1] 5 | } 6 | 7 | /** 8 | * get next IP address 9 | * @param {string} host 10 | * @returns {string} 11 | */ 12 | function getNewAddress(host) { 13 | const parts = host.split('.').map(Number) 14 | return getParts(parts, false).join('.') 15 | } 16 | 17 | module.exports = getNewAddress 18 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/hot.js: -------------------------------------------------------------------------------- 1 | export default ({ sourceModule, replaceGetter, args }) => { 2 | if (process.env.NODE_ENV !== 'production' && sourceModule.hot) { 3 | sourceModule.hot.accept() 4 | sourceModule.hot.dispose(data => { 5 | const replace = replaceGetter() 6 | if (typeof replace === 'function') { 7 | // eslint-disable-next-line 8 | data.replace = replace 9 | } 10 | }) 11 | if (sourceModule.hot.data && typeof sourceModule.hot.data.replace === 'function') { 12 | sourceModule.hot.data.replace.apply(null, args) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/layouts/global.scss: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100vh; 3 | } 4 | 5 | $barWidth: 7px; 6 | ::-webkit-scrollbar { 7 | width: $barWidth; 8 | height: $barWidth; 9 | } 10 | 11 | $radius: 5px; 12 | ::-webkit-scrollbar-track { 13 | background: rgba(243, 243, 243, 0.43); 14 | border-radius: $radius; 15 | } 16 | 17 | ::-webkit-scrollbar-thumb { 18 | background: rgba(180, 180, 180, 0.89); 19 | border-radius: $radius; 20 | } 21 | 22 | .col { 23 | height: 100vh; 24 | overflow-y: auto; 25 | overflow-x: hidden; 26 | } 27 | 28 | .col-1 { 29 | border-right: 0.6px solid #eee; 30 | } 31 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Aside/ListItem/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import styles from './ListItem.scss' 5 | 6 | const ListItem = ({ title, badge, online }) => ( 7 |
8 | {title} {badge > 0 && {badge}} 9 |
10 | ) 11 | 12 | ListItem.propTypes = { 13 | title: PropTypes.string.isRequired, 14 | badge: PropTypes.number.isRequired, 15 | online: PropTypes.bool, 16 | } 17 | 18 | export default ListItem 19 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/lib/Store.js: -------------------------------------------------------------------------------- 1 | const each = require('p2p-chat-utils/each') 2 | 3 | module.exports = class Store { 4 | constructor() { 5 | this.store = {} 6 | } 7 | 8 | remove(key) { 9 | this.store[key] = undefined 10 | } 11 | 12 | add(key, value = true) { 13 | this.store[key] = value 14 | } 15 | 16 | has(key) { 17 | return Boolean(this.store[key]) 18 | } 19 | 20 | get(key) { 21 | return this.store[key] || null 22 | } 23 | 24 | destory() { 25 | this.store = {} 26 | } 27 | 28 | each(keys, callback) { 29 | each(this.store, keys, callback) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/utils/format.js: -------------------------------------------------------------------------------- 1 | function toFixed(num) { 2 | return num.toFixed(2) 3 | } 4 | 5 | export function formatSize(size) { 6 | if (size > 1e9) return `${toFixed(size / 1e9)} GB` 7 | else if (size > 1e6) return `${toFixed(size / 1e6)} MB` 8 | else if (size > 1e3) return `${toFixed(size / 1e3)} KB` 9 | return `${toFixed(size)} B` 10 | } 11 | 12 | export function formatSpeed(speed) { 13 | return `${formatSize(speed)}/s` 14 | } 15 | 16 | export function formatPercent(percent) { 17 | return Number((percent * 100).toPrecision(3)) 18 | } 19 | 20 | export const formatTag = tag => `[${tag.slice(0, 5)}]` 21 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p2p-chat-core", 3 | "version": "1.4.5", 4 | "license": "MIT", 5 | "private": true, 6 | "description": "a p2p chat app", 7 | "main": "index.js", 8 | "author": "dgeibi (blog.dgeibi.xyz)", 9 | "scripts": { 10 | "lint": "eslint .", 11 | "lint:fix": "eslint --fix ." 12 | }, 13 | "keywords": [ 14 | "p2p", 15 | "socket" 16 | ], 17 | "dependencies": { 18 | "fs-extra": "^5.0.0", 19 | "internal-ip": "^3.0.1", 20 | "node-machine-id": "^1.1.10", 21 | "p2p-chat-logger": "^1.4.5", 22 | "p2p-chat-utils": "^1.4.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/lib/enhanceSocket/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const Parse = require('./Parse') 3 | 4 | const msgSocket = { 5 | send(msg) { 6 | this.write(`${JSON.stringify(msg)}\n`) 7 | }, 8 | } 9 | 10 | const enhance = opts => { 11 | const { socket, mixins } = opts 12 | const parse = new Parse(opts) 13 | socket.on('data', chunk => { 14 | parse.transform(chunk) 15 | }) 16 | const done = () => { 17 | parse.destory() 18 | } 19 | socket.on('close', done) 20 | socket.on('error', done) 21 | Object.assign(socket, msgSocket, mixins) 22 | return socket 23 | } 24 | 25 | module.exports = enhance 26 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/event-replaceable.js: -------------------------------------------------------------------------------- 1 | const EventObservable = require('./EventObservable') 2 | const noop = require('./noop') 3 | 4 | module.exports = ({ emitter, callback, args, disable }) => { 5 | let on 6 | let obs 7 | 8 | if (disable) { 9 | on = (...xx) => emitter.on(...xx) 10 | } else { 11 | obs = EventObservable(emitter) 12 | on = obs.observe 13 | } 14 | 15 | if (args && args.length) { 16 | callback(on, ...args) 17 | } else { 18 | callback(on) 19 | } 20 | 21 | if (disable) return noop 22 | 23 | return newCallback => { 24 | obs.removeAllObservables() 25 | newCallback(on, ...args) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/p2p-chat-logger/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "p2p-chat-logger" { 2 | namespace internal { 3 | export function log(message?: any, ...optionalParams: any[]): void; 4 | export function error(message?: any, ...optionalParams: any[]): void; 5 | export function err(message?: any, ...optionalParams: any[]): void; 6 | export function info(message?: any, ...optionalParams: any[]): void; 7 | export function warn(message?: any, ...optionalParams: any[]): void; 8 | export function verbose(message?: any, ...optionalParams: any[]): void; 9 | export function debug(message?: any, ...optionalParams: any[]): void; 10 | } 11 | export = internal; 12 | } 13 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/index.js: -------------------------------------------------------------------------------- 1 | exports.ipset = require('./ipset') 2 | exports.pickByMap = require('./pickByMap') 3 | exports.pickBy = require('./pickBy') 4 | exports.md5 = require('./md5') 5 | exports.arr2keys = require('./arr2keys') 6 | exports.each = require('./each') 7 | exports.ensureUniqueFilename = require('./ensure-unique-filename') 8 | exports.eventReplaceable = require('./event-replaceable') 9 | exports.EventObservable = require('./EventObservable') 10 | exports.getNewAddress = require('./get-new-address') 11 | exports.getPort = require('./get-port') 12 | exports.ipset = require('./ipset') 13 | exports.isIPLarger = require('./is-ip-larger') 14 | exports.has = require('./has') 15 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Settings/MyInfo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { formatTag } from '../../../utils/format' 5 | import styles from './MyInfo.scss' 6 | 7 | const MyInfo = ({ logined, tag, address, port, username }) => 8 | logined ? ( 9 |

10 | {username} 11 | {formatTag(tag)} 12 |
13 | {address}:{port} 14 |

15 | ) : null 16 | 17 | MyInfo.propTypes = { 18 | logined: PropTypes.bool.isRequired, 19 | tag: PropTypes.string, 20 | address: PropTypes.string, 21 | port: PropTypes.number, 22 | username: PropTypes.string, 23 | } 24 | 25 | export default MyInfo 26 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/pickByMap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | function getValue(object, props) { 4 | if (!object) return null 5 | if (!Array.isArray(props)) return object[props] 6 | const [first, ...rest] = props 7 | if (!first) return object 8 | return getValue(object[first], rest) 9 | } 10 | 11 | module.exports = function pickByMap(object, props, keys) { 12 | if (!keys) keys = Object.keys(object) 13 | return keys.reduce((obj, key) => { 14 | const value = object[key] 15 | if (!value) return obj 16 | obj[key] = {} 17 | Object.keys(props).forEach(propKey => { 18 | obj[key][propKey] = getValue(object[key], props[propKey]) 19 | }) 20 | return obj 21 | }, {}) 22 | } 23 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/global/subscribe.js: -------------------------------------------------------------------------------- 1 | import subscribeDialog from '../components/Chatting/Dialog/subscribe' 2 | import hot from '../utils/hot' 3 | 4 | const subscribes = [subscribeDialog] 5 | 6 | let replace 7 | const makeReplace = (unsubscribe, store) => { 8 | replace = subscribe => { 9 | unsubscribe() 10 | replace = null 11 | subscribe(store) 12 | } 13 | } 14 | 15 | const subscribe = store => { 16 | const listener = () => { 17 | const state = store.getState() 18 | subscribes.forEach(x => x && x(state)) 19 | } 20 | makeReplace(store.subscribe(listener), store) 21 | } 22 | 23 | hot({ 24 | sourceModule: module, 25 | replaceGetter: () => replace, 26 | args: [subscribe], 27 | }) 28 | 29 | export default subscribe 30 | -------------------------------------------------------------------------------- /packages/p2p-chat/main/setContextMenu.js: -------------------------------------------------------------------------------- 1 | import { Menu } from 'electron' 2 | 3 | const selectionMenu = Menu.buildFromTemplate([{ role: 'copy' }]) 4 | 5 | const inputMenu = Menu.buildFromTemplate([ 6 | { role: 'undo' }, 7 | { role: 'redo' }, 8 | { type: 'separator' }, 9 | { role: 'cut' }, 10 | { role: 'copy' }, 11 | { role: 'paste' }, 12 | { type: 'separator' }, 13 | { role: 'selectall' }, 14 | ]) 15 | 16 | export default window => { 17 | window.webContents.on('context-menu', (e, props) => { 18 | const { selectionText, isEditable } = props 19 | if (isEditable) { 20 | inputMenu.popup(window) 21 | } else if (selectionText && selectionText.trim() !== '') { 22 | selectionMenu.popup(window) 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/global/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { connectRouter } from 'connected-react-router' 3 | import settings from '../views/SettingsRedux' 4 | import aside from '../views/AsideRedux' 5 | import chatting from '../views/ChattingRedux' 6 | import modalbtns from '../views/ModalBtnRedux' 7 | 8 | // const initialState = appReducer({}, {}) 9 | 10 | export default history => { 11 | const appReducer = combineReducers({ 12 | settings, 13 | aside, 14 | chatting, 15 | modalbtns, 16 | router: connectRouter(history), 17 | }) 18 | 19 | return (state, action) => { 20 | if (action.type === 'LOGOUT') { 21 | window.location.reload() 22 | } 23 | 24 | return appReducer(state, action) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Settings/validators.js: -------------------------------------------------------------------------------- 1 | import ipRegex from 'ip-regex' 2 | 3 | export const validPort = (rule, value, callback) => { 4 | const port = Number(value) 5 | if (Number.isNaN(port) || !Number.isInteger(port) || port < 2000 || port > 59999) { 6 | callback(Error('Port should be a integer (2000~59999)')) 7 | return 8 | } 9 | callback() 10 | } 11 | 12 | export const validAddress = (rule, value, callback) => { 13 | if (!value || ipRegex({ exact: true }).test(value)) { 14 | callback() 15 | } else { 16 | callback(Error(`${value} is not a regular IP`)) 17 | } 18 | } 19 | 20 | export const validName = (rule, value, callback) => { 21 | if (!/\s/.test(value)) { 22 | callback() 23 | } else { 24 | callback(Error('should not contain white space')) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/p2p-chat-logger/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const chalk = require('chalk') 3 | const util = require('util') 4 | 5 | const colors = { 6 | log: null, 7 | verbose: 'cyan', 8 | info: 'green', 9 | warn: 'yellow', 10 | debug: 'blue', 11 | error: 'red', 12 | } 13 | 14 | const aliases = { 15 | err: 'error', 16 | } 17 | 18 | const mapFn = arg => (typeof arg === 'function' ? String(arg) : arg) 19 | 20 | const log = color => (...args) => { 21 | const str = util.format(...args.map(mapFn)) 22 | if (color !== null) { 23 | console.log(chalk[color](str)) 24 | } else { 25 | console.log(str) 26 | } 27 | } 28 | 29 | const logger = {} 30 | 31 | Object.keys(colors).forEach(x => { 32 | logger[x] = log(colors[x]) 33 | }) 34 | 35 | Object.keys(aliases).forEach(x => { 36 | logger[x] = logger[aliases[x]] 37 | }) 38 | 39 | module.exports = logger 40 | -------------------------------------------------------------------------------- /packages/p2p-chat-core/lib/login.js: -------------------------------------------------------------------------------- 1 | const internalIP = require('internal-ip') 2 | const getPort = require('p2p-chat-utils/get-port') 3 | const getTag = require('./getTag') 4 | 5 | function getInternalIP() { 6 | return internalIP.v4.sync() || internalIP.v6.sync() 7 | } 8 | 9 | /** 10 | * get tag, address, port 11 | * @param {{port: number, host: string, username: string}} opts 12 | * @param {function(?Error, ?{port: number, address: string, tag: string})} callback 13 | */ 14 | function login(opts, callback) { 15 | getPort({ start: opts.port }, (e, port) => { 16 | if (!e) { 17 | const id = Object.assign({}, opts) 18 | id.port = port 19 | id.address = opts.host || getInternalIP() // lan ip address 20 | id.tag = getTag(port, opts.username) 21 | callback(null, id) 22 | } else { 23 | callback(e) 24 | } 25 | }) 26 | } 27 | 28 | module.exports = login 29 | -------------------------------------------------------------------------------- /packages/p2p-chat/build/build-win-portable.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const builder = require('electron-builder') 4 | const commonConfig = require('../package').build 5 | 6 | const { Platform } = builder 7 | 8 | const createConfig = arch => 9 | Object.assign({}, commonConfig, { 10 | win: { 11 | target: [ 12 | { 13 | arch, 14 | target: 'portable', 15 | }, 16 | { 17 | arch, 18 | target: 'nsis', 19 | }, 20 | ], 21 | }, 22 | portable: { 23 | artifactName: `\${productName}-portable-win-\${version}-${arch}.\${ext}`, 24 | }, 25 | nsis: { 26 | artifactName: `\${productName}-win-installer-\${version}-${arch}.\${ext}`, 27 | }, 28 | }) 29 | 30 | const arch = process.argv[2] || 'ia32' 31 | 32 | builder.build({ 33 | targets: Platform.WINDOWS.createTarget(), 34 | config: createConfig(arch), 35 | }) 36 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/views/ModalBtn.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { modalBtnActions, selectVisible } from './ModalBtnRedux' 5 | 6 | @connect( 7 | (state, ownProp) => ({ 8 | visible: selectVisible(state.modalbtns, ownProp), 9 | }), 10 | (dispatch, ownProp) => ({ 11 | show: () => dispatch(modalBtnActions.show(ownProp.id)), 12 | hide: () => dispatch(modalBtnActions.hide(ownProp.id)), 13 | }) 14 | ) 15 | class ModalBtn extends Component { 16 | static propTypes = { 17 | show: PropTypes.func.isRequired, 18 | hide: PropTypes.func.isRequired, 19 | visible: PropTypes.bool.isRequired, 20 | children: PropTypes.func.isRequired, 21 | } 22 | 23 | render() { 24 | const { show, hide, visible, children } = this.props 25 | return children({ show, hide, visible }) 26 | } 27 | } 28 | 29 | export default ModalBtn 30 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/views/ModalBtnRedux.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | import constants from '../utils/constants' 3 | 4 | const initalState = {} 5 | 6 | const TYPES = { 7 | SHOW: '', 8 | HIDE: '', 9 | } 10 | constants(TYPES, 'modalBtn') 11 | 12 | export default (state = initalState, action) => { 13 | switch (action.type) { 14 | case TYPES.SHOW: 15 | return { 16 | ...state, 17 | [action.payload]: true, 18 | } 19 | case TYPES.HIDE: 20 | return { 21 | ...state, 22 | [action.payload]: false, 23 | } 24 | default: 25 | return state 26 | } 27 | } 28 | 29 | export const modalBtnActions = { 30 | show: createAction(TYPES.SHOW), 31 | hide: createAction(TYPES.HIDE), 32 | } 33 | 34 | export function selectVisible(state, ownProp) { 35 | if (state[ownProp.id] === undefined) return !!ownProp.visibleDefault 36 | return !!state[ownProp.id] 37 | } 38 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/Messages/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import dateFormat from 'dateformat' 3 | import PropTypes from 'prop-types' 4 | 5 | import { formatTag } from '../../../utils/format' 6 | 7 | import styles from './Text.scss' 8 | 9 | const Text = ({ username, text, myName, self, date, tag }) => ( 10 |
11 |
12 | {self ? myName : username + formatTag(tag)} 13 |
14 |
{text}
15 |
16 | {dateFormat(date, 'yyyy-mm-dd HH:MM')} 17 |
18 |
19 | ) 20 | 21 | Text.propTypes = { 22 | text: PropTypes.string.isRequired, 23 | date: PropTypes.number.isRequired, 24 | username: PropTypes.string, 25 | self: PropTypes.bool, 26 | myName: PropTypes.string, 27 | tag: PropTypes.string, 28 | } 29 | 30 | export default Text 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p2p-chat-bootstrap", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "description": "a P2P LAN chatting and file sharing tool", 6 | "author": "dgeibi", 7 | "private": true, 8 | "scripts": { 9 | "app": "cd packages/p2p-chat && yarn run", 10 | "lint": "eslint .", 11 | "fix": "eslint . --fix", 12 | "format": "prettier --write '**/*.{js,scss}'" 13 | }, 14 | "lint-staged": { 15 | "*.{scss,js}": [ 16 | "prettier --write", 17 | "git add" 18 | ] 19 | }, 20 | "husky": { 21 | "hooks": { 22 | "precommit": "lint-staged" 23 | } 24 | }, 25 | "devDependencies": { 26 | "eslint": "^5.11.0", 27 | "eslint-config-dgeibi": "^5.1.1", 28 | "husky": "^1.2.1", 29 | "lint-staged": "^8.1.0", 30 | "prettier": "^1.15.3" 31 | }, 32 | "workspaces": [ 33 | "packages/*" 34 | ], 35 | "prettier": { 36 | "semi": false, 37 | "singleQuote": true, 38 | "trailingComma": "es5", 39 | "printWidth": 90 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/global/redux.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import { routerMiddleware as createRouterMiddleware } from 'connected-react-router' 3 | import createHistory from 'history/createHashHistory' 4 | import makeReducer from './reducer' 5 | 6 | const history = createHistory() 7 | const RouterMiddleware = createRouterMiddleware(history) 8 | 9 | const middlewares = [ 10 | applyMiddleware(RouterMiddleware), 11 | process.env.NODE_ENV !== 'production' && require('./devToolsMiddleware').default, 12 | ].filter(Boolean) 13 | 14 | const finalCreateStore = compose(...middlewares)(createStore) 15 | 16 | const configureStore = initialState => { 17 | const store = finalCreateStore(makeReducer(history), initialState) 18 | 19 | if (process.env.NODE_ENV !== 'production' && module.hot) { 20 | module.hot.accept('./reducer', () => { 21 | store.replaceReducer(makeReducer(history)) 22 | }) 23 | } 24 | 25 | return store 26 | } 27 | 28 | export { history, configureStore } 29 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/get-port.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | /* eslint-disable no-param-reassign, no-bitwise */ 3 | /* base-on get-port getport */ 4 | 5 | function getport(opts, callback) { 6 | if (!callback) throw TypeError('callback required') 7 | opts = opts || { start: 2000, end: 50000 } 8 | opts.start |= 0 9 | opts.end |= 0 10 | opts.start = opts.start < 2000 ? 2000 : opts.start 11 | opts.end = opts.end < 2000 ? 50000 : opts.end 12 | 13 | const { start, end } = opts 14 | 15 | if (start >= end) { 16 | callback(Error('out of ports')) 17 | return 18 | } 19 | 20 | const server = net.createServer() 21 | 22 | server.unref() 23 | server.on('error', () => { 24 | opts.start += 1 25 | getport(opts, callback) 26 | }) 27 | 28 | server.listen(start, () => { 29 | if (server.address().port !== start) { 30 | opts.start += 1 31 | getport(opts, callback) 32 | server.close() 33 | return 34 | } 35 | server.close(() => { 36 | callback(null, start) 37 | }) 38 | }) 39 | } 40 | 41 | module.exports = getport 42 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/md5.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const crypto = require('crypto') 3 | 4 | /** 5 | * Get md5sum of file 6 | * @param {string} filepath full path of file 7 | * @param {boolean} strict whether check `file` is a file 8 | * @param {function(?Error, string)} callback 9 | */ 10 | const file = (filepath, strict, callback) => { 11 | if (strict) { 12 | let stats 13 | try { 14 | stats = fs.statSync(filepath) 15 | } catch (err) { 16 | callback(err) 17 | return 18 | } 19 | if (!stats.isFile()) { 20 | callback(Error(`${filepath} is not a file`)) 21 | return 22 | } 23 | } 24 | 25 | fs.createReadStream(filepath) 26 | .pipe(crypto.createHash('md5').setEncoding('hex')) 27 | .on('finish', function finished() { 28 | callback(null, this.read()) 29 | }) 30 | } 31 | 32 | /** 33 | * Get md5 checksum of data 34 | * @param {(string|Buffer)} data 35 | * @returns {string} 36 | */ 37 | const dataSync = data => 38 | crypto 39 | .createHash('md5') 40 | .update(data) 41 | .digest('hex') 42 | 43 | module.exports = { 44 | dataSync, 45 | file, 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 dgeibi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Settings/Login/redux.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | import { push } from 'connected-react-router' 3 | 4 | import createReducer from '../../../utils/createReducer' 5 | import getConstants from '../../../utils/constants' 6 | 7 | const TYPES = { 8 | UPDATE_SETTINGS: '', 9 | LOGOUT: '', 10 | } 11 | 12 | getConstants(TYPES, 'LOGIN') 13 | const initialState = { 14 | username: 'anonymous', 15 | port: 8087, 16 | logined: false, 17 | } 18 | 19 | const reducerMap = { 20 | [TYPES.UPDATE_SETTINGS](state, action) { 21 | return { 22 | ...state, 23 | ...action.payload, 24 | logined: true, 25 | } 26 | }, 27 | [TYPES.LOGOUT](state) { 28 | return { 29 | ...state, 30 | logined: false, 31 | } 32 | }, 33 | } 34 | 35 | export default createReducer(reducerMap, initialState) 36 | 37 | export const logout = () => { 38 | ipcRenderer.send('logout') 39 | return { 40 | type: TYPES.LOGOUT, 41 | } 42 | } 43 | 44 | export const updateSettings = id => ({ 45 | type: TYPES.UPDATE_SETTINGS, 46 | payload: id, 47 | }) 48 | 49 | export const backToRoot = () => push('/') 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | dist/ 61 | assets/ 62 | -------------------------------------------------------------------------------- /packages/p2p-chat/config/devServer.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process') 2 | const webpack = require('webpack') 3 | const logger = require('p2p-chat-logger') 4 | 5 | class StartElectron { 6 | apply(compiler) { 7 | compiler.hooks.done.tap('selfdone', () => { 8 | if (this.started) return 9 | this.started = true 10 | spawn('electron .', { 11 | shell: true, 12 | env: Object.assign( 13 | { 14 | DEV_PORT: compiler.options.devServer.port, 15 | }, 16 | process.env 17 | ), 18 | stdio: 'inherit', 19 | }) 20 | .on('close', () => process.exit(0)) 21 | .on('error', spawnError => logger.error(spawnError)) 22 | }) 23 | } 24 | } 25 | 26 | module.exports = options => wtf => { 27 | wtf.plugin(webpack.HotModuleReplacementPlugin) 28 | wtf.plugin(StartElectron) 29 | // eslint-disable-next-line no-param-reassign 30 | wtf.config.devServer = Object.assign( 31 | wtf.config.devServer, 32 | { 33 | disableHostCheck: true, 34 | hot: true, 35 | stats: { 36 | colors: true, 37 | chunks: false, 38 | children: false, 39 | }, 40 | }, 41 | options 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /packages/p2p-chat-utils/ensure-unique-filename.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition, no-param-reassign */ 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | /** 6 | * Get Unique filename that do not exist in disk 7 | * @return {string} 8 | * @param {string} filepath 9 | */ 10 | const ensureUnique = filepath => { 11 | do { 12 | if (fs.existsSync(filepath)) { 13 | const dirname = path.dirname(filepath) 14 | const basename = path.basename(filepath) 15 | 16 | const arr = basename.split('.') 17 | 18 | if (arr.length === 1) { 19 | arr.push(1) 20 | } else if (arr.length === 2) { 21 | const n = Number(arr[1]) 22 | if (Number.isNaN(n)) { 23 | arr.splice(1, 0, '1') 24 | } else { 25 | arr[1] = n + 1 26 | } 27 | } else { 28 | let idx = arr.length - 2 29 | let n = Number(arr[idx]) 30 | if (!Number.isNaN(n)) { 31 | arr[idx] = n + 1 32 | } else { 33 | idx += 1 34 | n = Number(arr[idx]) 35 | if (!Number.isNaN(n)) { 36 | arr[idx] = n + 1 37 | } else { 38 | arr.splice(arr.length - 1, 0, '1') 39 | } 40 | } 41 | } 42 | filepath = path.join(dirname, arr.join('.')) 43 | } else { 44 | break 45 | } 46 | } while (true) 47 | return filepath 48 | } 49 | 50 | module.exports = ensureUnique 51 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/components/Chatting/FilePanel/FileInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'antd' 3 | import PropTypes from 'prop-types' 4 | 5 | import Card from '../../Common/CustomCard' 6 | import { formatSize } from '../../../utils/format' 7 | import styles from './FileReceive.scss' 8 | import { fileLoadStates } from './constants' 9 | 10 | const FileInfo = ({ filename, accept, ignore, size, username, status }) => ( 11 | 12 |
13 |
14 | {filename} 15 |
16 |
17 | {formatSize(size)} by {username} 18 |
19 | {fileLoadStates.waitting === status ? ( 20 | 'waiting to receive..' 21 | ) : ( 22 |
23 |
24 | {' '} 27 | 30 |
31 | )} 32 |
33 |
34 | ) 35 | 36 | FileInfo.propTypes = { 37 | username: PropTypes.string.isRequired, 38 | size: PropTypes.number.isRequired, 39 | filename: PropTypes.string.isRequired, 40 | status: PropTypes.string, 41 | accept: PropTypes.func, 42 | ignore: PropTypes.func, 43 | } 44 | 45 | export default FileInfo 46 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/selectors/chatInfo.js: -------------------------------------------------------------------------------- 1 | import { createSelector, defaultMemoize } from 'reselect' 2 | import pickBy from 'p2p-chat-utils/pickBy' 3 | import has from 'p2p-chat-utils/has' 4 | 5 | const pickProps = (stub, src) => pickBy(src, (value, key) => has(stub, key)) 6 | 7 | const getChannelOnlineMembers = (members, users) => 8 | Object.values(users ? pickProps(members, users) : members).filter(x => x.online) 9 | 10 | const getChannelInfo = defaultMemoize((channels, users, key) => { 11 | const channel = { ...channels[key] } 12 | channel.users = pickProps(channel.users, users) 13 | channel.onlineCount = getChannelOnlineMembers(channel.users).length 14 | channel.online = channel.onlineCount > 0 15 | channel.totalCount = Object.keys(channel.users).length 16 | return channel 17 | }) 18 | 19 | const getInfo = (users, channels, type, key) => { 20 | if (type === 'user') return users[key] 21 | if (type === 'channel') return getChannelInfo(channels, users, key) 22 | return null 23 | } 24 | 25 | const usersSelector = state => state.aside.chatList.users 26 | const channelsSelector = state => state.aside.chatList.channels 27 | const typeSelector = (state, ownProps) => ownProps.match.params.type 28 | const keySelector = (state, ownProps) => ownProps.match.params.key 29 | 30 | const selectInfo = createSelector( 31 | [usersSelector, channelsSelector, typeSelector, keySelector], 32 | getInfo 33 | ) 34 | 35 | export { getInfo, selectInfo } 36 | -------------------------------------------------------------------------------- /packages/p2p-chat/src/layouts/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import { Row, Col } from 'antd' 5 | import { Route } from 'react-router-dom' 6 | import { ConnectedRouter as Router } from 'connected-react-router' 7 | import { hot } from 'react-hot-loader' 8 | 9 | import SettingNav from '../views/Settings' 10 | import Aside from '../views/Aside' 11 | import Chatting from '../views/Chatting' 12 | import styles from './global.scss' 13 | 14 | function getDevTool() { 15 | if (process.env.NODE_ENV !== 'production') { 16 | const DevTools = require('./DevTools').default 17 | return 18 | } 19 | return null 20 | } 21 | 22 | const App = ({ store, history }) => ( 23 | 24 | 25 | 26 | 27 | 28 |