├── server ├── src │ ├── main.ts │ ├── app │ │ ├── context │ │ │ ├── index.ts │ │ │ └── ServicesContext.ts │ │ ├── routes │ │ │ ├── index.ts │ │ │ ├── api.routes.ts │ │ │ └── app.routes.ts │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── register.controller.ts │ │ │ ├── login.controller.ts │ │ │ └── githubOAuth.controller.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── group.service.ts │ │ │ ├── chat.service.ts │ │ │ ├── groupChat.service.ts │ │ │ └── user.service.ts │ │ ├── middlewares │ │ │ ├── verify.ts │ │ │ └── requestFrequency.ts │ │ ├── utils │ │ │ ├── qiniu.ts │ │ │ ├── db.ts │ │ │ └── Logger.ts │ │ ├── index.ts │ │ ├── server.ts │ │ └── socket │ │ │ └── message.socket.ts │ └── configs │ │ ├── configs.prod.ts │ │ ├── configs.common.ts │ │ └── configs.dev.ts ├── .DS_Store ├── nodemon.json ├── .gitignore ├── init │ ├── util │ │ ├── getSQLMap.ts │ │ ├── walkFile.ts │ │ └── getSQLConentMap.ts │ ├── db.ts │ ├── index.ts │ └── sql │ │ └── ghchat.sql ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── package.json ├── .DS_Store ├── src ├── components │ ├── Linkify │ │ ├── decorators │ │ │ ├── defaultHrefDecorator.js │ │ │ ├── defaultTextDecorator.js │ │ │ ├── defaultMatchDecorator.js │ │ │ └── defaultComponentDecorator.js │ │ └── index.js │ ├── MyInfo │ │ ├── styles.scss │ │ └── index.js │ ├── GroupChat │ │ └── styles.scss │ ├── Spinner │ │ ├── index.js │ │ └── index.scss │ ├── GroupAvatar │ │ ├── styles.scss │ │ └── index.js │ ├── ChatContentList │ │ ├── styles.scss │ │ └── index.js │ ├── Tabs │ │ ├── style.scss │ │ ├── help.js │ │ └── index.js │ ├── Button │ │ ├── styles.scss │ │ └── index.js │ ├── UserAvatar │ │ ├── style.scss │ │ └── index.js │ ├── Notification │ │ ├── style.scss │ │ └── index.js │ ├── PersonalInfo │ │ ├── styles.scss │ │ └── index.js │ ├── NotFound │ │ ├── index.js │ │ └── styles.scss │ ├── ModalBase │ │ ├── index.js │ │ └── styles.scss │ ├── ChatHeader │ │ ├── style.scss │ │ └── index.js │ ├── HomePageList │ │ └── index.scss │ ├── Header │ │ ├── style.scss │ │ └── index.js │ ├── SearchBox │ │ ├── styles.scss │ │ └── index.js │ ├── ShareModal │ │ ├── styles.scss │ │ └── index.js │ ├── ShareChatCard │ │ └── index.js │ ├── Setting │ │ ├── styles.scss │ │ └── index.js │ ├── CreateGroupModal │ │ ├── styles.scss │ │ └── index.js │ ├── Modal │ │ ├── index.js │ │ └── style.scss │ ├── GroupChatInfo │ │ ├── styles.scss │ │ └── index.js │ ├── InputArea │ │ └── style.scss │ ├── Robot │ │ └── index.js │ ├── ChatItem │ │ └── style.scss │ ├── ListItems │ │ ├── styles.scss │ │ └── index.js │ └── SignInSignUp │ │ ├── index.scss │ │ └── index.js ├── utils │ ├── sleep.js │ ├── setStateAsync.js │ ├── debounce.js │ ├── qiniu.js │ ├── request.js │ └── transformTime.js ├── redux │ ├── actions │ │ ├── shareAction.js │ │ └── initAppAction.js │ ├── reducers │ │ ├── shareReducer.js │ │ └── initAppReducer.js │ ├── store.js │ └── reducers.js ├── containers │ ├── WelcomePage │ │ ├── styles.scss │ │ └── index.js │ ├── SettingPage │ │ ├── settingAction.js │ │ ├── index.js │ │ └── settingReducer.js │ ├── RegisterPage │ │ ├── index.scss │ │ └── index.js │ ├── LogInPage │ │ ├── index.scss │ │ └── index.js │ ├── Tabs │ │ └── index.js │ ├── RobotPage │ │ ├── index.js │ │ ├── robotAction.js │ │ └── robotReducer.js │ ├── PrivateChatPage │ │ ├── privateChatReducer.js │ │ ├── index.js │ │ └── privateChatAction.js │ ├── GroupChatPage │ │ ├── groupChatReducer.js │ │ ├── index.js │ │ └── groupChatAction.js │ ├── Header │ │ └── index.js │ └── HomePageList │ │ ├── index.js │ │ ├── homePageListReducer.js │ │ └── homePageListAction.js ├── assets │ ├── base.scss │ └── chat.scss ├── manifest.json ├── index.js ├── service-worker.js ├── index.html ├── modules │ ├── BrowserNotification │ │ └── index.js │ └── Chat │ │ └── index.js ├── router │ └── index.js └── index.scss ├── .eslintignore ├── postcss.config.js ├── .prettierrc ├── .babelrc ├── .prettierignore ├── .vscode └── settings.json ├── .gitignore ├── webpack.common.config.js ├── LICENSE ├── webpack.dev.config.js ├── webpack.prod.config.js ├── .eslintrc.js └── package.json /server/src/main.ts: -------------------------------------------------------------------------------- 1 | export { App } from './app'; 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aermin/ghChat/HEAD/.DS_Store -------------------------------------------------------------------------------- /server/src/app/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ServicesContext'; 2 | -------------------------------------------------------------------------------- /server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aermin/ghChat/HEAD/server/.DS_Store -------------------------------------------------------------------------------- /src/components/Linkify/decorators/defaultHrefDecorator.js: -------------------------------------------------------------------------------- 1 | export default href => href; 2 | -------------------------------------------------------------------------------- /src/components/Linkify/decorators/defaultTextDecorator.js: -------------------------------------------------------------------------------- 1 | export default text => text; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/build 4 | *.min.js 5 | service-worker.js 6 | -------------------------------------------------------------------------------- /src/components/MyInfo/styles.scss: -------------------------------------------------------------------------------- 1 | .myInfo { 2 | display: none; 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /server/src/app/routes/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Generated by cli, don't modify manually 3 | */ 4 | export * from './app.routes'; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // eslint-disable-next-line global-require 3 | plugins: [require('autoprefixer')], 4 | }; 5 | -------------------------------------------------------------------------------- /src/utils/sleep.js: -------------------------------------------------------------------------------- 1 | export default function sleep(ms = 0) { 2 | return new Promise(resolve => { 3 | setTimeout(resolve, ms); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/setStateAsync.js: -------------------------------------------------------------------------------- 1 | export default function setStateAsync(state) { 2 | return new Promise(resolve => { 3 | this.setState(state, resolve); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["dist"], 3 | "ext": "js", 4 | "ignore": ["dist/**/*.spec.ts"], 5 | "exec": "node --inspect -r dotenv/config dist/index.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/redux/actions/shareAction.js: -------------------------------------------------------------------------------- 1 | const SHARE = 'SHARE'; 2 | 3 | const shareAction = (data = null) => ({ 4 | type: SHARE, 5 | data, 6 | }); 7 | 8 | export { SHARE, shareAction }; 9 | -------------------------------------------------------------------------------- /src/redux/actions/initAppAction.js: -------------------------------------------------------------------------------- 1 | const INIT_APP = 'INIT_APP'; 2 | 3 | const initAppAction = (status = false) => ({ 4 | type: INIT_APP, 5 | data: status, 6 | }); 7 | 8 | export { INIT_APP, initAppAction }; 9 | -------------------------------------------------------------------------------- /server/src/app/controllers/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Generated by cli, don't modify manually 3 | */ 4 | export * from './githubOAuth.controller'; 5 | export * from './login.controller'; 6 | export * from './register.controller'; 7 | -------------------------------------------------------------------------------- /src/containers/WelcomePage/styles.scss: -------------------------------------------------------------------------------- 1 | .welcomeWrapper { 2 | text-align: center; 3 | .title { 4 | font-size: 18px; 5 | padding-bottom: 40px; 6 | } 7 | .content { 8 | font-size: 14px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/src/configs/configs.prod.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | import commonConfigs from './configs.common'; 3 | 4 | export default { 5 | production: true, 6 | ...commonConfigs, 7 | ...require('../../secrets'), 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Linkify/decorators/defaultMatchDecorator.js: -------------------------------------------------------------------------------- 1 | import LinkifyIt from 'linkify-it'; 2 | import tlds from 'tlds'; 3 | 4 | const linkify = new LinkifyIt(); 5 | linkify.tlds(tlds); 6 | 7 | export default text => linkify.match(text); 8 | -------------------------------------------------------------------------------- /server/src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Generated by cli, don't modify manually 3 | */ 4 | export * from './chat.service'; 5 | export * from './group.service'; 6 | export * from './groupChat.service'; 7 | export * from './user.service'; 8 | -------------------------------------------------------------------------------- /src/components/GroupChat/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/chat.scss"; 2 | 3 | .groupChatInfoMask { 4 | position: fixed; 5 | width: 100%; 6 | height: 100%; 7 | top: 0; 8 | left: 0; 9 | background-color: #fff; 10 | opacity: 0; 11 | z-index: 98; 12 | } -------------------------------------------------------------------------------- /src/components/Linkify/decorators/defaultComponentDecorator.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default (decoratedHref, decoratedText, key, target = '_blank') => ( 4 | 5 | {decoratedText} 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/Spinner/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.scss'; 3 | 4 | export default function Spinner() { 5 | return ( 6 |
7 |
Loading...
8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "eslintIntegration": true, 5 | "printWidth": 100, 6 | "overrides": [ 7 | { 8 | "files": ".prettierrc", 9 | "options": { 10 | "parser": "json" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/containers/SettingPage/settingAction.js: -------------------------------------------------------------------------------- 1 | const SET_GLOBAL_SETTINGS = 'SET_GLOBAL_SETTINGS'; 2 | 3 | const setGlobalSettingsAction = (globalSettings = {}) => ({ 4 | type: SET_GLOBAL_SETTINGS, 5 | data: globalSettings, 6 | }); 7 | 8 | export { SET_GLOBAL_SETTINGS, setGlobalSettingsAction }; 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | "react-hot-loader/babel", 9 | "transform-runtime", //将babel辅助函数“搬”到一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。 10 | "syntax-dynamic-import" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/base.scss: -------------------------------------------------------------------------------- 1 | $blue: #66b3ef; 2 | $gray:#555; 3 | $title:#676767; 4 | $lightGray: #b4b8b9d3; 5 | $grayTab:rgb(243, 243, 243); 6 | $border:rgb(243, 238, 238) ; 7 | $backgroundColor:#f6f7f9; 8 | $otherMsgColor: #f3f3f3; 9 | $myMsgColor: #daf4fe; 10 | $warnRed: #e44545; 11 | $lightBruce: #f3f8ff; 12 | -------------------------------------------------------------------------------- /src/containers/RegisterPage/index.scss: -------------------------------------------------------------------------------- 1 | .register{ 2 | height: 100vh; 3 | display: flex; 4 | align-items: center; 5 | flex-direction: column; 6 | justify-content: center; 7 | width: 100%; 8 | *:focus { 9 | outline: none; 10 | } 11 | 12 | #icon { 13 | width: 60%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/GroupAvatar/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | .groupAvatar { 3 | display: inline-flex; 4 | flex-wrap: wrap; 5 | justify-content:center; 6 | align-items: center; 7 | width: 46px; 8 | height: 46px; 9 | overflow: hidden; 10 | border-radius: 50%; 11 | background-color: $lightBruce; 12 | } -------------------------------------------------------------------------------- /src/redux/reducers/shareReducer.js: -------------------------------------------------------------------------------- 1 | import { SHARE } from '../actions/shareAction'; 2 | 3 | const shareReducer = (previousState = null, action) => { 4 | switch (action.type) { 5 | case SHARE: 6 | return action.data; 7 | default: 8 | return previousState; 9 | } 10 | }; 11 | 12 | export { shareReducer }; 13 | -------------------------------------------------------------------------------- /src/redux/reducers/initAppReducer.js: -------------------------------------------------------------------------------- 1 | import { INIT_APP } from '../actions/initAppAction'; 2 | 3 | const initAppReducer = (previousState = false, action) => { 4 | switch (action.type) { 5 | case INIT_APP: 6 | return action.data; 7 | default: 8 | return previousState; 9 | } 10 | }; 11 | 12 | export { initAppReducer }; 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.png 4 | **/*.ejs 5 | **/*.html 6 | build 7 | .cache 8 | .idea 9 | .vscode 10 | .history 11 | node_modules 12 | package.json 13 | package-lock.json 14 | .DS_Store 15 | .eslintignore 16 | .editorconfig 17 | .gitignore 18 | .prettierignore 19 | LICENSE 20 | .eslintcache 21 | *.lock 22 | yarn-error.log 23 | -------------------------------------------------------------------------------- /src/components/ChatContentList/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | @import '~react-viewer/dist/index.css'; 3 | 4 | .tip { 5 | font-size: 12px; 6 | color: $gray; 7 | text-align: center; 8 | margin: 10px 0; 9 | } 10 | 11 | .react-viewer-toolbar :nth-child(3), 12 | .react-viewer-toolbar :nth-child(5){ 13 | display: none; 14 | } -------------------------------------------------------------------------------- /server/src/app/routes/api.routes.ts: -------------------------------------------------------------------------------- 1 | import * as Router from 'koa-router'; 2 | 3 | import { githubOAuthController, loginController, registerController } from '../controllers'; 4 | 5 | export const apiRoutes = new Router() 6 | .post('/register', registerController) // 注册 7 | .post('/login', loginController) // 登录 8 | .post('/github_oauth', githubOAuthController); 9 | -------------------------------------------------------------------------------- /server/src/configs/configs.common.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | const rootUrl = path.join(process.cwd(), 'dist'); 4 | 5 | export default { 6 | rootUrl: path.join(process.cwd(), 'dist'), 7 | staticPath: path.join(rootUrl, '../build'), 8 | logger: { 9 | debug: 'app*', 10 | console: { 11 | level: 'error', 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/containers/LogInPage/index.scss: -------------------------------------------------------------------------------- 1 | .login{ 2 | height: 100vh; 3 | display: flex; 4 | align-items: center; 5 | flex-direction: column; 6 | justify-content: center; 7 | width: 100%; 8 | *:focus { 9 | outline: none; 10 | } 11 | 12 | .content { 13 | padding:30px 0px 10px; 14 | } 15 | 16 | #icon { 17 | width: 60%; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Tabs/style.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | .tabs-wrapper{ 3 | background-color: $grayTab; 4 | align-items: center; 5 | text-align: center; 6 | z-index: 99; 7 | .tab .icon{ 8 | font-size: 44px; 9 | padding: 6px; 10 | } 11 | .userAvatar { 12 | font-size: 14px; 13 | text-align: center; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import combineReducers from './reducers.js'; 5 | 6 | const store = createStore(combineReducers, composeWithDevTools(applyMiddleware(thunkMiddleware))); // 第二个参数为thunk中间件 用来处理函数类型的action 7 | 8 | export default store; 9 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.sw[mnpcod] 3 | *.log 4 | *.tmp 5 | *.tmp.* 6 | log.txt 7 | *.sublime-project 8 | *.sublime-workspace 9 | .vscode/ 10 | npm-debug.log* 11 | .idea/ 12 | .sourcemaps/ 13 | .sass-cache/ 14 | .tmp/ 15 | .versions/ 16 | coverage/ 17 | dist/ 18 | dist.1/ 19 | dist.2/ 20 | frontEnd/ 21 | node_modules/ 22 | tmp/ 23 | temp/ 24 | .DS_Store 25 | Thumbs.db 26 | .nyc_output 27 | .awcache 28 | /secrets.* 29 | -------------------------------------------------------------------------------- /src/components/Button/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | 3 | .baseButton { 4 | background-color: $blue; 5 | border: none; 6 | color: white; 7 | padding: 10px 40px; 8 | text-align: center; 9 | border-radius: 4px 4px 4px 4px; 10 | display: inline-block; 11 | cursor: pointer; 12 | } 13 | 14 | .disable { 15 | background-color: $backgroundColor; 16 | color: $gray; 17 | cursor: not-allowed; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/UserAvatar/style.scss: -------------------------------------------------------------------------------- 1 | .gray { 2 | -webkit-filter: grayscale(100%); 3 | -moz-filter: grayscale(100%); 4 | -ms-filter: grayscale(100%); 5 | -o-filter: grayscale(100%); 6 | filter: grayscale(100%); 7 | filter: gray; 8 | } 9 | 10 | .viaGithub { 11 | font-size: 10px; 12 | position: absolute; 13 | top: -6px; 14 | left: 34px; 15 | } 16 | 17 | .userAvatar { 18 | position: relative; 19 | } 20 | -------------------------------------------------------------------------------- /server/src/app/middlewares/verify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 处理验证的中间件 3 | */ 4 | 5 | import * as jwt from 'jsonwebtoken'; 6 | import configs from '@configs'; 7 | 8 | export const authVerify = token => { 9 | try { 10 | // 解码取出之前存在payload的user_id 11 | const payload = jwt.verify(token, configs.jwt_secret); 12 | return payload; 13 | } catch (err) { 14 | // ctx.throw(401, err); 15 | console.error(err); 16 | return false; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "javascript.validate.enable": false, 4 | "editor.rulers": [100], 5 | "editor.tabSize": 2, 6 | "search.exclude": { 7 | "**/.git": true, 8 | "**/.svn": true, 9 | "**/.hg": true, 10 | "**/CVS": true, 11 | "**/.DS_Store": true, 12 | "build/**": true 13 | }, 14 | "eslint.autoFixOnSave": true, 15 | "files.eol": "\n", 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/WelcomePage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './styles.scss'; 3 | 4 | export default class welcome extends Component { 5 | constructor() { 6 | super(); 7 | this.state = {}; 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

欢迎ヾ(=・ω・=)o

14 |

选个群组/用户开始聊天吧ε==(づ′▽`)づ

15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/containers/Tabs/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Tabs from '../../components/Tabs'; 3 | import { initAppAction } from '../../redux/actions/initAppAction'; 4 | 5 | const mapStateToProps = state => ({ 6 | initializedApp: state.initAppState, 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | initApp(arg) { 11 | dispatch(initAppAction(arg)); 12 | }, 13 | }); 14 | 15 | export default connect(mapStateToProps, mapDispatchToProps)(Tabs); 16 | -------------------------------------------------------------------------------- /src/components/Tabs/help.js: -------------------------------------------------------------------------------- 1 | import InitApp from '../../modules/InitApp'; 2 | 3 | function initAppOnce(props) { 4 | if (Object.prototype.toString.call(props) !== '[object Object]') { 5 | throw new Error('please input props for init function'); 6 | } 7 | if (!props.initializedApp && props.initApp) { 8 | const InitAppInstance = new InitApp({ history: props.history }); 9 | InitAppInstance.init().then(() => { 10 | props.initApp(true); 11 | }); 12 | } 13 | } 14 | 15 | export { initAppOnce }; 16 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghChat", 3 | "short_name": "ghChat", 4 | "display": "standalone", 5 | "start_url": "/", 6 | "theme_color": "#60b0f4", 7 | "background_color": "#fff", 8 | "icons": [ 9 | { 10 | "src": "https://cdn.aermin.top/ghChatIcon192%2A192.png", 11 | "sizes": "192x192", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "https://cdn.aermin.top/ghChatIcon512%2A512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /server/src/app/utils/qiniu.ts: -------------------------------------------------------------------------------- 1 | import * as qiniu from 'qiniu'; 2 | import configs from '@configs'; 3 | 4 | export function getUploadToken() { 5 | const { accessKey, secretKey, bucket } = configs.qiniu; 6 | const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); 7 | 8 | const options = { 9 | scope: bucket, 10 | }; 11 | 12 | const putPolicy = new qiniu.rs.PutPolicy(options); 13 | const uploadToken = putPolicy.uploadToken(mac); 14 | 15 | console.log('uploadToken', uploadToken); 16 | return uploadToken; 17 | } 18 | -------------------------------------------------------------------------------- /server/init/util/getSQLMap.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | import { walkFile } from './walkFile'; 3 | 4 | /** 5 | * 获取sql目录下的文件目录数据 6 | * @return {object} 7 | */ 8 | export function getSqlMap(): object { 9 | let basePath = __dirname; 10 | basePath = basePath.replace(/\\/g, '/'); 11 | 12 | let pathArr = basePath.split('/'); 13 | pathArr = pathArr.splice(0, pathArr.length - 1); 14 | basePath = `${pathArr.join('/')}/sql/`; 15 | 16 | const fileList = walkFile(basePath, 'sql'); 17 | return fileList; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Notification/style.scss: -------------------------------------------------------------------------------- 1 | 2 | .rc-notification { 3 | position: fixed; 4 | top: 14px; 5 | left: 50%; 6 | font-size: 12px; 7 | transform: translate(-50%, 0); 8 | z-index: 9999; 9 | .all-icon { 10 | font-size: 14px; 11 | pointer-events: all; 12 | padding: 10px 12px; 13 | border-radius: 6px; 14 | box-shadow: 0 1px 6px rgba(0, 0, 0, .2); 15 | background: #fff; 16 | color: #495060; 17 | .icon{ 18 | margin-right:4px; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/assets/chat.scss: -------------------------------------------------------------------------------- 1 | @import "./base.scss"; 2 | .chat-wrapper { 3 | height: 100%; 4 | width: 100%; 5 | position: relative; 6 | .chat-content-list { 7 | height: calc(100% - 100px); 8 | padding: 20px 0; 9 | display: flex; 10 | flex-direction: column; 11 | list-style: none; 12 | overflow-y: auto; 13 | li { 14 | padding: 0; 15 | } 16 | } 17 | .button { 18 | position: absolute; 19 | left: 50%; 20 | transform: translateX(-50%); 21 | } 22 | } -------------------------------------------------------------------------------- /src/containers/RobotPage/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { getRobotMsgAction, insertMsgAction } from './robotAction'; 3 | import Robot from '../../components/Robot'; 4 | 5 | const mapStateToProps = state => ({ 6 | robotState: state.robotState, 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | insertMsg(data) { 11 | dispatch(insertMsgAction(data)); 12 | }, 13 | async getRobotMsg(data) { 14 | dispatch(await getRobotMsgAction(data)); 15 | }, 16 | }); 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(Robot); 19 | -------------------------------------------------------------------------------- /server/src/app/middlewares/requestFrequency.ts: -------------------------------------------------------------------------------- 1 | let timeStamp = Date.parse(new Date().toString()); 2 | let limitCount = {}; 3 | 4 | export const requestFrequency = socketId => { 5 | const nowTimeStamp = Date.parse(new Date().toString()); 6 | if (nowTimeStamp - timeStamp > 60000) { 7 | // more than 60 seconds 8 | limitCount = {}; 9 | timeStamp = nowTimeStamp; 10 | return false; 11 | } // less than 60 seconds 12 | if (limitCount[socketId] > 30) { 13 | return true; 14 | } 15 | limitCount[socketId] = (limitCount[socketId] || 0) + 1; 16 | return false; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/PersonalInfo/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | 3 | .userInfo { 4 | padding: 20px 20px; 5 | p { 6 | font-size: 12px; 7 | padding: 10px 0px; 8 | } 9 | .name { 10 | font-size: 16px; 11 | padding-top: 10px; 12 | } 13 | .icon { 14 | font-size: 40px; 15 | padding-top: 10px; 16 | cursor: pointer; 17 | } 18 | .website, .github { 19 | cursor: pointer; 20 | } 21 | .personalInfoBtn { 22 | margin: 20px 10px 0px; 23 | padding: 8px 30px !important; 24 | } 25 | .deleteBtn { 26 | background-color: $warnRed; 27 | } 28 | } -------------------------------------------------------------------------------- /server/src/configs/configs.dev.ts: -------------------------------------------------------------------------------- 1 | import commonConfigs from './configs.common'; 2 | 3 | export default { 4 | production: false, 5 | ...commonConfigs, 6 | port: '3000', 7 | dbConnection: { 8 | host: '127.0.0.1', // 数据库IP 9 | port: 3306, // 数据库端口 10 | database: 'ghchat', // 数据库名称 11 | user: 'root', // 数据库用户名 12 | password: 'ghchat@123', // 数据库密码 13 | }, 14 | client_secret: '', 15 | jwt_secret: 'chat-sec', 16 | qiniu: { 17 | accessKey: '', 18 | secretKey: '', 19 | bucket: '', 20 | }, 21 | robot_key: '', // 机器人聊天用到的key => 请自己申请http://www.tuling123.com/ 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/GroupAvatar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles.scss'; 3 | import UserAvatar from '../UserAvatar'; 4 | 5 | function GroupAvatar({ members }) { 6 | if (!members.length) return ; 7 | const willRenderMembers = members.slice(0, 4); 8 | const avatarRender = willRenderMembers.map(e => { 9 | const size = `${46 / 2}`; 10 | return ( 11 | 12 | ); 13 | }); 14 | 15 | return
{avatarRender}
; 16 | } 17 | 18 | export default GroupAvatar; 19 | -------------------------------------------------------------------------------- /src/components/NotFound/index.js: -------------------------------------------------------------------------------- 1 | // thanks => https://codepen.io/Navedkhan012/pen/vrWQMY 2 | 3 | import React from 'react'; 4 | import { Link } from 'react-router-dom'; 5 | import './styles.scss'; 6 | 7 | export default function NotFound() { 8 | return ( 9 |
10 |
11 |
12 |

404

13 |
14 |
15 |

你所访问的页面不存在

16 | 17 | 返回首页 18 | 19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export default function debounce(func, threshold = 500, immediate = false) { 2 | if (typeof func !== 'function') { 3 | throw new Error('First argument of debounce function should be a function'); 4 | } 5 | let timer = null; 6 | return function debounced(...args) { 7 | const context = this; 8 | const callNow = immediate && !timer; 9 | const later = () => { 10 | timer = null; 11 | if (!immediate) func.apply(context, args); 12 | }; 13 | console.log('please wait'); 14 | clearTimeout(timer); 15 | timer = setTimeout(later, threshold); 16 | if (callNow) func.apply(context, args); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /server/src/app/controllers/register.controller.ts: -------------------------------------------------------------------------------- 1 | import * as md5 from 'md5'; 2 | import { ServicesContext } from '../context'; 3 | 4 | export const registerController = async (ctx, next) => { 5 | const { userService } = ServicesContext.getInstance(); 6 | 7 | const { name, password } = ctx.request.body; 8 | const result = await userService.findDataByName(name); 9 | if (result.length) { 10 | ctx.body = { 11 | success: false, 12 | message: '用户名已存在', 13 | }; 14 | } else { 15 | ctx.body = { 16 | success: true, 17 | message: '注册成功!', 18 | }; 19 | console.log('注册成功'); 20 | userService.insertData([name, md5(password)]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Spinner/index.scss: -------------------------------------------------------------------------------- 1 | .spinnerWrapper{ 2 | background-color: #000; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | z-index: 998; 7 | width: 100%; 8 | height: 100%; 9 | opacity: 0.8; 10 | font-size: 20px; 11 | .spinner { 12 | position: absolute; 13 | color: #fff; 14 | z-index: 999; 15 | top: 50%; 16 | left: 50%; 17 | transform: translate(-50%, -50%); 18 | -moz-transform: translate(-50%, -50%); 19 | //Mozilla内核浏览器:firefox3.5+ 20 | -webkit-transform: translate(-50%, -50%); //Webkit内核浏览器:Safari and Chrome 21 | -o-transform: translate(-50%, -50%); 22 | //Opera 23 | -ms-transform: translate(-50%, -50%); 24 | } 25 | } -------------------------------------------------------------------------------- /src/containers/PrivateChatPage/privateChatReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ALL_PRIVATE_CHATS, 3 | ADD_PRIVATE_CHAT_MESSAGES, 4 | ADD_PRIVATE_INFO, 5 | ADD_PRIVATE_CHAT_MESSAGE_AND_INFO, 6 | DELETE_PRIVATE_CHAT, 7 | } from './privateChatAction'; 8 | 9 | const fetchAllPrivateChatsReducer = (previousState = new Map(), action) => { 10 | switch (action.type) { 11 | case SET_ALL_PRIVATE_CHATS: 12 | case ADD_PRIVATE_CHAT_MESSAGES: 13 | case ADD_PRIVATE_INFO: 14 | case ADD_PRIVATE_CHAT_MESSAGE_AND_INFO: 15 | case DELETE_PRIVATE_CHAT: 16 | return action.data; 17 | default: 18 | return previousState; 19 | } 20 | }; 21 | 22 | export { fetchAllPrivateChatsReducer }; 23 | -------------------------------------------------------------------------------- /server/src/app/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { createPool } from 'mysql'; 2 | import configs from '@configs'; 3 | 4 | const pool = createPool(configs.dbConnection); 5 | 6 | export const query = (sql, values?): Promise => 7 | new Promise((resolve, reject) => { 8 | pool.getConnection((err, connection) => { 9 | if (err) { 10 | console.log('query connec error!', err); 11 | // resolve(err); 12 | } else { 13 | connection.query(sql, values, (err, rows) => { 14 | if (err) { 15 | reject(err); 16 | } else { 17 | resolve(rows); 18 | } 19 | connection.release(); 20 | }); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /server/init/util/walkFile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | /** 4 | * 遍历目录下的文件目录 5 | * @param {string} pathResolve 需进行遍历的目录路径 6 | * @param {string} mime 遍历文件的后缀名 7 | * @return {object} 返回遍历后的目录结果 8 | */ 9 | export const walkFile = (pathResolve: string, mime: string): object => { 10 | const files = fs.readdirSync(pathResolve); 11 | const fileList = {}; 12 | for (const [i, item] of files.entries()) { 13 | const itemArr = item.split('.'); 14 | 15 | const itemMime = itemArr.length > 1 ? itemArr[itemArr.length - 1] : 'undefined'; 16 | if (mime === itemMime) { 17 | fileList[item] = pathResolve + item; 18 | } 19 | } 20 | 21 | return fileList; 22 | }; 23 | -------------------------------------------------------------------------------- /src/containers/GroupChatPage/groupChatReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ALL_GROUP_CHATS, 3 | ADD_GROUP_MESSAGES, 4 | DELETE_GROUP_CHAT, 5 | ADD_GROUP_INFO, 6 | ADD_GROUP_MESSAGE_AND_INFO, 7 | UPDATE_GROUP_TITLE_NOTICE, 8 | } from './groupChatAction'; 9 | 10 | const fetchAllGroupChatsReducer = (previousState = new Map(), action) => { 11 | switch (action.type) { 12 | case SET_ALL_GROUP_CHATS: 13 | case ADD_GROUP_MESSAGES: 14 | case DELETE_GROUP_CHAT: 15 | case ADD_GROUP_INFO: 16 | case ADD_GROUP_MESSAGE_AND_INFO: 17 | case UPDATE_GROUP_TITLE_NOTICE: 18 | return action.data; 19 | default: 20 | return previousState; 21 | } 22 | }; 23 | 24 | export { fetchAllGroupChatsReducer }; 25 | -------------------------------------------------------------------------------- /src/containers/SettingPage/index.js: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'react-router-dom'; 2 | import { connect } from 'react-redux'; 3 | import Setting from '../../components/Setting'; 4 | import { initAppAction } from '../../redux/actions/initAppAction'; 5 | import { setGlobalSettingsAction } from './settingAction'; 6 | 7 | const mapStateToProps = state => ({ 8 | globalSettings: state.globalSettingsState, 9 | }); 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | initApp(arg) { 13 | dispatch(initAppAction(arg)); 14 | }, 15 | setGlobalSettings(arg) { 16 | dispatch(setGlobalSettingsAction(arg)); 17 | }, 18 | }); 19 | 20 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Setting)); 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Production 9 | build 10 | 11 | # Dependency directories 12 | node_modules/ 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional eslint cache 18 | .eslintcache 19 | 20 | # Optional REPL history 21 | .node_repl_history 22 | 23 | # Output of 'npm pack' 24 | *.tgz 25 | 26 | # dotenv environment variable files 27 | .env* 28 | 29 | # Mac files 30 | .DS_Store 31 | 32 | # Yarn 33 | yarn.lock 34 | yarn-error.log 35 | .pnp/ 36 | .pnp.js 37 | # Yarn Integrity file 38 | .yarn-integrity 39 | 40 | # idea 41 | .idea 42 | 43 | # VSCode 44 | .history 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import { Provider } from 'react-redux'; // 让所有容器组件都可以访问store,而不必显示地传递它。只需要在渲染根组件时使用即可。 4 | import store from './redux/store'; 5 | import App from './router'; 6 | import AxiosHandle from './utils/request'; 7 | import './index.scss'; 8 | 9 | if (window.location.protocol === 'https:' && navigator.serviceWorker) { 10 | window.addEventListener('load', () => { 11 | const sw = '/service-worker.js'; 12 | navigator.serviceWorker.register(sw); 13 | }); 14 | } 15 | console.log(process.env.NODE_ENV); 16 | 17 | ReactDom.render( 18 | 19 | 20 | , 21 | document.getElementById('app'), 22 | ); 23 | 24 | AxiosHandle.axiosConfigInit(); 25 | -------------------------------------------------------------------------------- /src/containers/RobotPage/robotAction.js: -------------------------------------------------------------------------------- 1 | import request from '../../utils/request'; 2 | import notification from '../../components/Notification'; 3 | 4 | export const GET_ROBOT_MSG = 'robot/GET_ROBOT_MSG'; 5 | export const INSERT_MSG = 'robot/INSERT_MSG'; 6 | 7 | export const insertMsgAction = data => ({ 8 | type: INSERT_MSG, 9 | data, 10 | }); 11 | 12 | export const getRobotMsgAction = async data => { 13 | const response = await request.socketEmitAndGetResponse('robotChat', data, error => { 14 | notification('消息发送失败', 'error', 2); 15 | }); 16 | const { text, code, url } = response; 17 | return { 18 | type: INSERT_MSG, 19 | data: { 20 | message: code === 200000 ? text + url : text, 21 | user: '机器人小R', 22 | }, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/containers/RobotPage/robotReducer.js: -------------------------------------------------------------------------------- 1 | import { INSERT_MSG } from './robotAction'; 2 | 3 | const GROUP_CHAT_ID = 'ddbffd80-3663-11e9-a580-d119b23ef62e'; 4 | 5 | const initState = { 6 | robotMsg: [ 7 | // 机器人首语 8 | { 9 | message: 'hi, 我是机器人,欢迎与我聊天哦!也欢迎点击加入ghChat交流群进行交流 :grinning:', 10 | user: '机器人小R', 11 | }, 12 | { 13 | message: '::share::{"name":"ghChat","to_group_id":"ddbffd80-3663-11e9-a580-d119b23ef62e"}', 14 | user: '机器人小R', 15 | }, 16 | ], 17 | }; 18 | 19 | export default function RobotReducer(state = initState.robotMsg, action) { 20 | switch (action.type) { 21 | case INSERT_MSG: 22 | state.push(action.data); 23 | return [...state]; 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/app/routes/app.routes.ts: -------------------------------------------------------------------------------- 1 | import configs from '@configs'; 2 | import * as fs from 'fs'; 3 | import * as Router from 'koa-router'; 4 | import * as statics from 'koa-static'; 5 | import * as path from 'path'; 6 | 7 | import { apiRoutes } from './api.routes'; 8 | 9 | export const appRoutes = new Router() 10 | .get('/alive', (ctx, next) => { 11 | ctx.body = { 12 | message: 'server alive', 13 | time: new Date(), 14 | }; 15 | ctx.status = 200; 16 | }) 17 | .use('/api/v1', apiRoutes.routes(), apiRoutes.allowedMethods()) 18 | .get('*.*', statics(configs.staticPath)) 19 | .get('/*', (ctx, next) => { 20 | ctx.type = 'html'; 21 | ctx.body = fs.createReadStream(path.join(configs.staticPath, '/index.html')); 22 | next(); 23 | }); 24 | -------------------------------------------------------------------------------- /server/init/util/getSQLConentMap.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { getSqlMap } from './getSQLMap'; 3 | 4 | const sqlContentMap = {}; 5 | 6 | /** 7 | * 读取sql文件内容 8 | * @param {string} fileName 文件名称 9 | * @param {string} path 文件所在的路径 10 | * @return {string} 脚本文件内容 11 | */ 12 | function getSqlContent(fileName: string, path: string) { 13 | const content = fs.readFileSync(path, 'binary'); 14 | sqlContentMap[fileName] = content; 15 | } 16 | 17 | /** 18 | * 封装所有sql文件脚本内容 19 | * @return {object} 20 | */ 21 | export function getSqlContentMap(): object { 22 | const sqlMap = getSqlMap(); 23 | // eslint-disable-next-line guard-for-in 24 | for (const key in sqlMap) { 25 | getSqlContent(key, sqlMap[key]); 26 | } 27 | 28 | return sqlContentMap; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ModalBase/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles.scss'; 3 | import classnames from 'classnames'; 4 | 5 | function ModalBase(Comp) { 6 | return props => { 7 | const { visible = false, cancel, modalWrapperClassName } = props; 8 | return ( 9 |
10 |
11 |
12 | {cancel && ( 13 | 14 | x 15 | 16 | )} 17 | 18 |
19 |
20 | ); 21 | }; 22 | } 23 | 24 | export default ModalBase; 25 | -------------------------------------------------------------------------------- /server/init/db.ts: -------------------------------------------------------------------------------- 1 | import { createPool } from 'mysql'; 2 | import configs from '../src/configs/configs.dev'; 3 | // import configs from '../src/configs/configs.prod'; //if use in production 4 | 5 | const pool = createPool(configs.dbConnection); 6 | 7 | export const query = (sql, values?): Promise => 8 | new Promise((resolve, reject) => { 9 | pool.getConnection((err, connection) => { 10 | if (err) { 11 | console.log('query connec error!', err); 12 | // resolve(err); 13 | } else { 14 | connection.query(sql, values, (err, rows) => { 15 | if (err) { 16 | reject(err); 17 | } else { 18 | resolve(rows); 19 | } 20 | connection.release(); 21 | }); 22 | } 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import './styles.scss'; 5 | 6 | export default function Button({ clickFn, value, className, disable }) { 7 | return ( 8 | 15 | ); 16 | } 17 | 18 | Button.propTypes = { 19 | clickFn: PropTypes.func, 20 | value: PropTypes.string, 21 | className: PropTypes.string, 22 | disable: PropTypes.bool, 23 | }; 24 | 25 | Button.defaultProps = { 26 | clickFn: undefined, 27 | value: '', 28 | className: undefined, 29 | disable: false, 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/ChatHeader/style.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | .chat-header-wrapper{ 3 | border-bottom: 1px solid $border; 4 | height: 50px; 5 | background-color: #fff; 6 | line-height: 50px; 7 | overflow: hidden; 8 | display: flex; 9 | align-items: center; 10 | font-size: 26px; 11 | .chat-title{ 12 | padding-left: 30px; 13 | font-size: 18px; 14 | } 15 | .back-icon { 16 | color: $blue; 17 | display: none; 18 | } 19 | .icon { 20 | position: absolute; 21 | cursor: pointer; 22 | z-index: 99; 23 | } 24 | .information-icon { 25 | right: 20px; 26 | font-size: 24px; 27 | } 28 | .shareIcon { 29 | right: 50px; 30 | font-size: 28px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/HomePageList/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | .home-page-list-wrapper { 3 | height: 100%; 4 | .home-page-list-content { 5 | overflow-y: auto; 6 | .searchResult { 7 | .searchResultTitle { 8 | padding-left: 10px; 9 | } 10 | p { 11 | color: $title; 12 | font-size: 14px; 13 | padding: 10px 0; 14 | } 15 | .clickToSearch { 16 | text-align: right; 17 | padding-right: 10px; 18 | margin-bottom: 10px; 19 | color: $blue; 20 | cursor:pointer; 21 | } 22 | .search-none { 23 | text-align: center; 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/containers/Header/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { updateHomePageListAction } from '../HomePageList/homePageListAction'; 4 | import { addGroupMessageAndInfoAction } from '../GroupChatPage/groupChatAction'; 5 | import Header from '../../components/Header'; 6 | 7 | const mapStateToProps = state => ({ 8 | allGroupChats: state.allGroupChatsState, 9 | homePageList: state.homePageListState, 10 | }); 11 | 12 | const mapDispatchToProps = dispatch => ({ 13 | addGroupMessageAndInfo(arg = {}) { 14 | dispatch(addGroupMessageAndInfoAction({ ...arg })); 15 | }, 16 | updateHomePageList(arg = {}) { 17 | dispatch(updateHomePageListAction({ ...arg })); 18 | }, 19 | }); 20 | 21 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)); 22 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | // 用workbox-sw替代之前的实现方案,感谢碎碎酱@yinxin630的建议和方案 2 | 3 | importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js'); 4 | 5 | if (workbox) { 6 | workbox.setConfig({ modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/' }); 7 | 8 | workbox.precaching.precache(['/', '/index.html']); 9 | 10 | workbox.routing.registerRoute( 11 | new RegExp('^https?://im.aermin.top/?$'), 12 | workbox.strategies.networkFirst(), 13 | ); 14 | 15 | workbox.routing.registerRoute(new RegExp('.*.html'), workbox.strategies.networkFirst()); 16 | 17 | workbox.routing.registerRoute( 18 | new RegExp('.*.(?:js|css)'), 19 | workbox.strategies.staleWhileRevalidate(), 20 | ); 21 | 22 | workbox.routing.registerRoute( 23 | new RegExp('https://cdn.aremin.top/'), 24 | workbox.strategies.cacheFirst(), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ModalBase/styles.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | .mask { 3 | position: fixed; 4 | width: 100%; 5 | height: 100%; 6 | top: 0; 7 | left: 0; 8 | background-color: #000; 9 | opacity: 0.3; 10 | z-index: 998; 11 | } 12 | 13 | .modalWrapper { 14 | position: fixed; 15 | min-width: 30%; 16 | top: 50%; 17 | left: 50%; 18 | transform: translate(-50%, -50%); 19 | background-color: white; 20 | border-radius: 8px; 21 | border-top: 1px solid white; 22 | text-align: center; 23 | z-index: 999; 24 | .xIcon{ 25 | font-size: 18px; 26 | line-height: 18px; 27 | padding: 6px 12px; 28 | cursor: pointer; 29 | position: absolute; 30 | right: 0px; 31 | } 32 | } 33 | } 34 | 35 | .showModalBase { 36 | position: absolute; 37 | } 38 | 39 | .hideModalBase { 40 | display: none; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Header/style.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | .header-wrapper { 3 | width: 100%; 4 | background-color: #fff; 5 | border-bottom: 1px solid $border; 6 | display: flex; 7 | justify-content: space-between; 8 | height: 50px; 9 | align-items: center; 10 | .myInfo { 11 | visibility: hidden; 12 | display: inline-block; 13 | } 14 | .add { 15 | font-size: 22px; 16 | height: 50px; 17 | line-height: 50px; 18 | cursor: pointer; 19 | margin-right: 10px; 20 | } 21 | .dialogRender { 22 | /*在IE10+浏览器中, 使用css即可隐藏input文本输入框右侧的叉号*/ 23 | ::-ms-reveal, 24 | input[type=text]::-ms-clear { 25 | display: none; 26 | } 27 | ::-ms-reveal, 28 | input::-ms-clear { 29 | display: none; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/SearchBox/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | 3 | .searchBox { 4 | border-radius: 4px; 5 | background-color: #fff; 6 | .icon { 7 | font-size: 26px; 8 | position: absolute; 9 | } 10 | /*在IE10+浏览器中, 使用css即可隐藏input文本输入框右侧的叉号*/ 11 | ::-ms-reveal,input[type=text]::-ms-clear { 12 | display: none; 13 | } 14 | ::-ms-reveal,input::-ms-clear { 15 | display: none; 16 | } 17 | input { 18 | /*去除点击出现轮廓颜色*/ 19 | border-radius: 4px; 20 | outline: none; 21 | border: 1px solid #fff; 22 | background-color: $backgroundColor; 23 | padding-left: 35px; 24 | height: 26px; 25 | -webkit-appearance: none; 26 | /*去除系统默认的样式,苹果手机上的阴影*/ 27 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 28 | } 29 | } -------------------------------------------------------------------------------- /src/containers/HomePageList/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import HomePageList from '../../components/HomePageList'; 3 | import { showCallMeTipAction } from './homePageListAction'; 4 | 5 | const mapStateToProps = state => { 6 | const userInfo = JSON.parse(localStorage.getItem('userInfo')); 7 | const homePageListStorage = 8 | userInfo && 9 | userInfo.user_id && 10 | JSON.parse(localStorage.getItem(`homePageList-${userInfo.user_id}`)); 11 | return { 12 | homePageList: homePageListStorage || state.homePageListState, 13 | allGroupChats: state.allGroupChatsState, 14 | allPrivateChats: state.allPrivateChatsState, 15 | initializedApp: state.initAppState, 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = dispatch => ({ 20 | showCallMeTip(arg = {}) { 21 | dispatch(showCallMeTipAction({ ...arg })); 22 | }, 23 | }); 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(HomePageList); 26 | -------------------------------------------------------------------------------- /src/components/NotFound/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | 3 | .page404 { 4 | width: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | text-align: center; 9 | .container { 10 | width: 50%; 11 | } 12 | img{ 13 | width:100%; 14 | } 15 | 16 | .fourZeroFourBg { 17 | background-image: url(https://cdn.aermin.top/ghchat404.gif); 18 | height: 400px; 19 | background-position: center; 20 | } 21 | 22 | 23 | .fourZeroFourBg h1 { 24 | font-size:80px; 25 | } 26 | 27 | .fourZeroFourBg h3 { 28 | font-size:80px; 29 | } 30 | 31 | .link{ 32 | color: #fff; 33 | padding: 10px 20px; 34 | background: $blue; 35 | margin: 20px 0; 36 | display: inline-block; 37 | text-decoration:none 38 | } 39 | .contentBox { 40 | margin-top:-50px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/qiniu.js: -------------------------------------------------------------------------------- 1 | import * as qiniu from 'qiniu-js'; 2 | 3 | export default async function upload(file, uploadToken, completeEvent) { 4 | // subscription.unsubscribe(); // 上传取消 5 | const observer = { 6 | next(res) { 7 | // console.log('qiniu observer next', res); 8 | }, 9 | error(err) { 10 | // console.log('qiniu observer err', err); 11 | return err; 12 | }, 13 | complete(res) { 14 | // console.log('qiniu observer complete', res); 15 | const fileUrl = `https://cdn.aermin.top/${res.key}`; 16 | completeEvent(fileUrl); 17 | }, 18 | }; 19 | 20 | const config = { useCdnDomain: true }; 21 | const putExtra = {}; 22 | const { user_id } = JSON.parse(localStorage.getItem('userInfo')); 23 | const key = `${user_id}_${new Date().getTime()}_${file.name}`; 24 | const observable = qiniu.upload(file, key, uploadToken, putExtra, config); 25 | const subscription = observable.subscribe(observer); // 上传开始 26 | } 27 | -------------------------------------------------------------------------------- /src/containers/SettingPage/settingReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_GLOBAL_SETTINGS } from './settingAction'; 2 | 3 | const GLOBAL_SETTINGS = { 4 | NOTIFICATION: 'notification', 5 | }; 6 | 7 | const initialSettings = { 8 | [GLOBAL_SETTINGS.NOTIFICATION]: true, 9 | }; 10 | 11 | const userInfo = JSON.parse(localStorage.getItem('userInfo')); 12 | const previousSettings = 13 | userInfo && JSON.parse(localStorage.getItem(`settings-${userInfo.user_id}`)); 14 | 15 | const setGlobalSettingsReducer = (previousState = previousSettings || initialSettings, action) => { 16 | switch (action.type) { 17 | case SET_GLOBAL_SETTINGS: 18 | if (userInfo) { 19 | localStorage.setItem( 20 | `settings-${userInfo.user_id}`, 21 | JSON.stringify({ ...previousState, ...action.data }), 22 | ); 23 | } 24 | return { ...previousState, ...action.data }; 25 | default: 26 | return previousState; 27 | } 28 | }; 29 | 30 | export { setGlobalSettingsReducer, GLOBAL_SETTINGS }; 31 | -------------------------------------------------------------------------------- /src/redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import robotReducer from '../containers/RobotPage/robotReducer'; 4 | import { 5 | getHomePageListReducer, 6 | relatedCurrentChatReducer, 7 | } from '../containers/HomePageList/homePageListReducer'; 8 | import { initAppReducer } from './reducers/initAppReducer'; 9 | import { shareReducer } from './reducers/shareReducer'; 10 | import { fetchAllGroupChatsReducer } from '../containers/GroupChatPage/groupChatReducer'; 11 | import { fetchAllPrivateChatsReducer } from '../containers/PrivateChatPage/privateChatReducer'; 12 | import { setGlobalSettingsReducer } from '../containers/SettingPage/settingReducer'; 13 | 14 | export default combineReducers({ 15 | robotState: robotReducer, 16 | homePageListState: getHomePageListReducer, 17 | allGroupChatsState: fetchAllGroupChatsReducer, 18 | allPrivateChatsState: fetchAllPrivateChatsReducer, 19 | relatedCurrentChat: relatedCurrentChatReducer, 20 | initAppState: initAppReducer, 21 | shareState: shareReducer, 22 | globalSettingsState: setGlobalSettingsReducer, 23 | }); 24 | -------------------------------------------------------------------------------- /webpack.common.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | output: { 7 | chunkFilename: '[name].[chunkhash].js', 8 | publicPath: '/', 9 | }, 10 | /* src文件夹下面的以.js结尾的文件,要使用babel解析 */ 11 | /* cacheDirectory是用来缓存编译结果,下次编译加速 */ 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | use: ['babel-loader?cacheDirectory=true'], 17 | include: path.resolve(__dirname, 'src'), 18 | }, 19 | { 20 | test: /\.(png|jpg|gif|svg)$/, 21 | use: [ 22 | { 23 | loader: 'url-loader', 24 | options: { 25 | limit: 8192, // 小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求。 26 | }, 27 | }, 28 | ], 29 | }, 30 | ], 31 | }, 32 | plugins: [ 33 | new HtmlWebpackPlugin({ 34 | // 每次自动把js插入到模板index.html里面去 35 | filename: 'index.html', 36 | template: path.resolve(__dirname, 'src/index.html'), 37 | }), 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /server/src/app/index.ts: -------------------------------------------------------------------------------- 1 | import configs from '@configs'; 2 | import * as bodyParser from 'koa-bodyparser'; 3 | import * as compress from 'koa-compress'; 4 | import * as cors from '@koa/cors'; 5 | 6 | import { ServicesContext } from './context'; 7 | import { appRoutes } from './routes'; 8 | import { Server } from './server'; 9 | import { ChatService, GroupChatService, GroupService, UserService } from './services'; 10 | 11 | const corsArgs = configs.production ? { origin: "https://im.aermin.top" } : {}; 12 | 13 | export const App = Server.init(app => { 14 | app 15 | .use(compress()) 16 | .use(cors(corsArgs)) 17 | .use(bodyParser()) 18 | .use(appRoutes.routes()) 19 | .use(appRoutes.allowedMethods()); 20 | }) 21 | .createServer() 22 | .createConnection() 23 | .then(() => { 24 | ServicesContext.getInstance() 25 | .setuserService(new UserService()) 26 | .setGroupService(new GroupService()) 27 | .setChatService(new ChatService()) 28 | .setgroupChatService(new GroupChatService()); 29 | 30 | Server.run(configs.port); 31 | }); 32 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "esModuleInterop": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "noImplicitAny": false, 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "baseUrl": "./", 13 | "paths": { 14 | "@configs": ["src/configs/configs.dev"] 15 | }, 16 | "lib": ["es2015", "es6", "esnext.asynciterable", "dom"] 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules"], 20 | "awesomeTypescriptLoaderOptions": { 21 | "forceIsolatedModules": true, 22 | "useBabel": true, 23 | // "babelOptions": { 24 | // "babelrc": false, 25 | // /* Important line */ 26 | // "presets": [ 27 | // ["@babel/preset-env", { 28 | // "targets": "last 2 versions, ie 11", 29 | // "modules": false 30 | // }] 31 | // ] 32 | // }, 33 | "babelCore": "@babel/core", 34 | "useCache": true, 35 | "reportFiles": ["src/**/*.{ts,tsx}"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Aermin Huang 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 | -------------------------------------------------------------------------------- /server/init/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable guard-for-in */ 2 | import { getSqlContentMap } from './util/getSQLConentMap'; 3 | import { query } from './db'; 4 | 5 | // 打印脚本执行日志 6 | const eventLog = (err, sqlFile, index) => { 7 | if (err) { 8 | console.log(`[ERROR] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行失败 o(╯□╰)o !`); 9 | } else { 10 | console.log(`[SUCCESS] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行成功 O(∩_∩)O !`); 11 | } 12 | }; 13 | 14 | // 获取所有sql脚本内容 15 | const sqlContentMap = getSqlContentMap(); 16 | 17 | // 执行建表sql脚本 18 | const createAllTables = async () => { 19 | for (const key in sqlContentMap) { 20 | const sqlShell = sqlContentMap[key]; 21 | const sqlShellList = sqlShell.split(';'); 22 | 23 | for (const [i, shell] of sqlShellList.entries()) { 24 | if (shell.trim()) { 25 | const result = await query(shell); 26 | if (result.serverStatus * 1 === 2) { 27 | eventLog(null, key, i); 28 | } else { 29 | eventLog(true, key, i); 30 | } 31 | } 32 | } 33 | } 34 | console.log('sql脚本执行结束!'); 35 | console.log('请按 ctrl + c 键退出!'); 36 | }; 37 | 38 | createAllTables(); 39 | -------------------------------------------------------------------------------- /src/containers/HomePageList/homePageListReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_HOME_PAGE_LIST, 3 | UPDATE_HOME_PAGE_LIST, 4 | CLEAR_UNREAD, 5 | DELETE_CHAT_FROM_LIST, 6 | SHOW_CALL_ME_TIP, 7 | RELATED_CURRENT_CHAT, 8 | UPDATE_LIST_GROUP_NAME, 9 | } from './homePageListAction'; 10 | 11 | const userInfo = JSON.parse(localStorage.getItem('userInfo')); 12 | const getHomePageListReducer = (previousState = [], action) => { 13 | switch (action.type) { 14 | case SET_HOME_PAGE_LIST: 15 | case UPDATE_HOME_PAGE_LIST: 16 | case CLEAR_UNREAD: 17 | case DELETE_CHAT_FROM_LIST: 18 | case SHOW_CALL_ME_TIP: 19 | case UPDATE_LIST_GROUP_NAME: 20 | if (userInfo) { 21 | localStorage.setItem(`homePageList-${userInfo.user_id}`, JSON.stringify(action.data)); 22 | } 23 | return [...action.data]; 24 | default: 25 | return previousState; 26 | } 27 | }; 28 | 29 | const relatedCurrentChatReducer = (previousState = true, action) => { 30 | switch (action.type) { 31 | case RELATED_CURRENT_CHAT: 32 | return action.data; 33 | default: 34 | return previousState; 35 | } 36 | }; 37 | 38 | export { getHomePageListReducer, relatedCurrentChatReducer }; 39 | -------------------------------------------------------------------------------- /src/components/Notification/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Notification from 'rc-notification'; 3 | import './style.scss'; 4 | 5 | function addIcon(msg, type) { 6 | let content; 7 | if (type === 'success') { 8 | content = ( 9 |
10 | {' '} 13 | {msg} 14 |
15 | ); 16 | } else if (type === 'warn') { 17 | content = ( 18 |
19 | {' '} 22 | {msg} 23 |
24 | ); 25 | } else if (type === 'error') { 26 | content = ( 27 |
28 | {' '} 31 | {msg} 32 |
33 | ); 34 | } 35 | return content; 36 | } 37 | 38 | export default function notification(msg, type, duration) { 39 | const content = addIcon(msg, type); 40 | Notification.newInstance({}, notification => { 41 | notification.notice({ 42 | content, 43 | duration, 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "indent": [true, "spaces"], 6 | "one-line": [true, "check-open-brace", "check-whitespace"], 7 | "no-var-keyword": true, 8 | "quotemark": [true, "double", "avoid-escape"], 9 | "semicolon": [true, "always", "ignore-bound-class-methods"], 10 | "whitespace": [ 11 | true, 12 | "check-branch", 13 | "check-decl", 14 | "check-operator", 15 | "check-module", 16 | "check-separator", 17 | "check-type" 18 | ], 19 | "typedef-whitespace": [ 20 | true, 21 | { 22 | "call-signature": "nospace", 23 | "index-signature": "nospace", 24 | "parameter": "nospace", 25 | "property-declaration": "nospace", 26 | "variable-declaration": "nospace" 27 | }, 28 | { 29 | "call-signature": "onespace", 30 | "index-signature": "onespace", 31 | "parameter": "onespace", 32 | "property-declaration": "onespace", 33 | "variable-declaration": "onespace" 34 | } 35 | ], 36 | "no-internal-module": true, 37 | "no-trailing-whitespace": true, 38 | "no-null-keyword": true, 39 | "prefer-const": true, 40 | "jsdoc-format": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/SearchBox/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './styles.scss'; 4 | 5 | export default class SearchBox extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | searchField: '', 10 | }; 11 | } 12 | 13 | _searchFieldChange = event => { 14 | const { name, value } = event.target; 15 | this.setState({ [name]: value }); 16 | const { searchFieldChange } = this.props; 17 | searchFieldChange(value); 18 | }; 19 | 20 | render() { 21 | const { searchField } = this.state; 22 | const { isSearching } = this.props; 23 | return ( 24 |
25 | 28 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | SearchBox.propTypes = { 41 | searchFieldChange: PropTypes.func, 42 | isSearching: PropTypes.bool, 43 | }; 44 | 45 | SearchBox.defaultProps = { 46 | searchFieldChange: undefined, 47 | isSearching: false, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/ShareModal/styles.scss: -------------------------------------------------------------------------------- 1 | .shareModalWrapper { 2 | height: 50%; 3 | .dialogRender { 4 | height: 100%; 5 | .searchBox { 6 | padding: 10px 0; 7 | } 8 | .homePageList { 9 | height: calc(100% - 130px); 10 | overflow-y: auto; 11 | li{ 12 | &:hover { 13 | background-color: #f5f5f5; 14 | } 15 | padding: 6px 0 6px 20px !important; 16 | .title p { 17 | margin-bottom: 0 !important; 18 | } 19 | .groupAvatar { 20 | width: 28px; 21 | height: 28px; 22 | .userAvatar{ 23 | width: 14px !important; 24 | height: 14px !important; 25 | line-height: 14px !important; 26 | } 27 | } 28 | .userAvatar { 29 | width: 28px !important; 30 | height: 28px !important; 31 | line-height: 28px !important; 32 | img { 33 | width: 28px !important; 34 | height: 28px !important; 35 | } 36 | } 37 | } 38 | } 39 | .shareShareLink { 40 | font-size: 14px; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | padding: 10px; 45 | cursor: pointer; 46 | .shareIcon { 47 | font-size: 24px; 48 | } 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 5 | const commonConfig = require('./webpack.common.config.js'); 6 | 7 | module.exports = merge(commonConfig, { 8 | mode: 'development', 9 | optimization: { 10 | usedExports: true, 11 | }, 12 | devtool: 'inline-source-map', 13 | entry: { 14 | app: ['babel-polyfill', 'react-hot-loader/patch', path.resolve(__dirname, 'src/index.js')], 15 | }, 16 | output: { 17 | /* 这里本来应该是[chunkhash]的,但是由于[chunkhash]和react-hot-loader不兼容。只能妥协 */ 18 | filename: '[name].[hash].js', 19 | path: path.resolve(__dirname, './build'), 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(js|jsx)$/, 25 | use: 'react-hot-loader/webpack', 26 | include: /node_modules/, 27 | }, 28 | { 29 | test: /\.scss$/, 30 | use: ['style-loader', 'css-loader', 'sass-loader', 'postcss-loader'], 31 | }, 32 | ], 33 | }, 34 | devServer: { 35 | contentBase: path.resolve(__dirname, './src'), // 让WEB服务器运行静态资源(index.html) 36 | hot: true, 37 | historyApiFallback: true, 38 | compress: true, 39 | stats: 'errors-only', // 只在发生错误时输出 40 | }, 41 | plugins: [new webpack.HotModuleReplacementPlugin(), new ProgressBarPlugin()], 42 | }); 43 | -------------------------------------------------------------------------------- /server/src/app/services/group.service.ts: -------------------------------------------------------------------------------- 1 | import { query } from '../utils/db'; 2 | 3 | export class GroupService { 4 | // 模糊匹配用户 5 | fuzzyMatchGroups(link) { 6 | const _sql = ` 7 | SELECT * FROM group_info WHERE name LIKE ?; 8 | `; 9 | return query(_sql, link); 10 | } 11 | 12 | // 加入群 13 | joinGroup(user_id, to_group_id) { 14 | const _sql = 'INSERT INTO group_user_relation(user_id,to_group_id) VALUES(?,?);'; 15 | return query(_sql, [user_id, to_group_id]); 16 | } 17 | 18 | // 查看某个用户是否在某个群中 19 | isInGroup(user_id, to_group_id) { 20 | const _sql = 'SELECT * FROM group_user_relation WHERE user_id = ? AND to_group_id = ?;'; 21 | return query(_sql, [user_id, to_group_id]); 22 | } 23 | 24 | // 建群 25 | createGroup(arr) { 26 | const _sql = 27 | 'INSERT INTO group_info (to_group_id,name,group_notice,creator_id,create_time) VALUES (?,?,?,?,?)'; 28 | return query(_sql, arr); 29 | } 30 | 31 | // 更新群信息 32 | 33 | updateGroupInfo({ name, group_notice, to_group_id }) { 34 | const _sql = 'UPDATE group_info SET name = ?, group_notice = ? WHERE to_group_id= ? limit 1 ; '; 35 | return query(_sql, [name, group_notice, to_group_id]); 36 | } 37 | 38 | // 退群 39 | leaveGroup(user_id, to_group_id) { 40 | const _sql = 'DELETE FROM group_user_relation WHERE user_id = ? AND to_group_id = ? ;'; 41 | return query(_sql, [user_id, to_group_id]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/containers/PrivateChatPage/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { 4 | updateHomePageListAction, 5 | deleteHomePageListAction, 6 | } from '../HomePageList/homePageListAction'; 7 | import { 8 | addPrivateChatMessagesAction, 9 | addPrivateChatInfoAction, 10 | deletePrivateChatAction, 11 | } from './privateChatAction'; 12 | import PrivateChat from '../../components/PrivateChat'; 13 | 14 | const mapStateToProps = state => ({ 15 | allPrivateChats: state.allPrivateChatsState, 16 | homePageList: state.homePageListState, 17 | relatedCurrentChat: state.relatedCurrentChat, 18 | shareData: state.shareState, 19 | allGroupChats: state.allGroupChatsState, 20 | initApp: state.initAppState, 21 | }); 22 | 23 | const mapDispatchToProps = dispatch => ({ 24 | addPrivateChatMessages(arg = {}) { 25 | dispatch(addPrivateChatMessagesAction({ ...arg })); 26 | }, 27 | addPrivateChatInfo(arg = {}) { 28 | dispatch(addPrivateChatInfoAction({ ...arg })); 29 | }, 30 | updateHomePageList(arg = {}) { 31 | dispatch(updateHomePageListAction({ ...arg })); 32 | }, 33 | deleteHomePageList(arg = {}) { 34 | dispatch(deleteHomePageListAction({ ...arg })); 35 | }, 36 | deletePrivateChat(arg = {}) { 37 | dispatch(deletePrivateChatAction({ ...arg })); 38 | }, 39 | }); 40 | 41 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(PrivateChat)); 42 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-sequences */ 2 | import axios from 'axios'; 3 | 4 | export default class Request { 5 | static axiosConfigInit() { 6 | if (process.env.NODE_ENV !== 'production') { 7 | axios.defaults.baseURL = 'http://localhost:3000'; 8 | } 9 | } 10 | 11 | static async axios(method = 'get', url, params) { 12 | const handleMethod = method === 'get' && params ? { params } : params; 13 | return new Promise((resolve, reject) => { 14 | // eslint-disable-next-line no-unused-expressions 15 | axios[method](url, handleMethod) 16 | .then(res => { 17 | const response = typeof res.data === 'object' ? res.data : JSON.parse(res.data); 18 | resolve(response); 19 | }) 20 | .catch(error => { 21 | reject(error.response ? error.response.data : error); 22 | }); 23 | }); 24 | } 25 | 26 | static socketEmit(emitName, data, onError) { 27 | try { 28 | window.socket.emit(emitName, data); 29 | } catch (error) { 30 | if (onError) { 31 | onError(error); 32 | } 33 | } 34 | } 35 | 36 | static socketEmitAndGetResponse(emitName, data, onError) { 37 | return new Promise((resolve, reject) => { 38 | try { 39 | window.socket.emit(emitName, data, response => { 40 | resolve(response); 41 | }); 42 | } catch (error) { 43 | if (onError) { 44 | onError(error); 45 | } 46 | reject(error); 47 | } 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/transformTime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param 时间戳 4 | * @return yyyy-MM-dd hh:mm 格式的时间 5 | */ 6 | 7 | function addZero(data) { 8 | if (data.toString().length === 1) { 9 | return `0${data}`; 10 | } 11 | return data; 12 | } 13 | 14 | function monthHandle(month) { 15 | return `${month + 1 < 10 ? `0${month + 1}` : month + 1}`; 16 | } 17 | 18 | export const toNormalTime = timestamp => { 19 | const dateOfArg = new Date(timestamp * 1000); 20 | const [yearOfArg, monthOfArg, dayOfArg, hourOfArg, minuteOfArg] = [ 21 | dateOfArg.getFullYear(), 22 | dateOfArg.getMonth(), 23 | dateOfArg.getDate(), 24 | dateOfArg.getHours(), 25 | dateOfArg.getMinutes(), 26 | ]; 27 | const date = new Date(); 28 | const [today, yesterday, thisMonth, thisYear] = [ 29 | date.getDate(), 30 | new Date(new Date().setDate(new Date().getDate() - 1)).getDate(), 31 | date.getMonth(), 32 | date.getFullYear(), 33 | ]; 34 | 35 | const isToday = thisYear === yearOfArg && thisMonth === monthOfArg && today === dayOfArg; 36 | if (isToday) { 37 | return `${addZero(hourOfArg)}:${addZero(minuteOfArg)}`; 38 | } 39 | 40 | const isYesterday = thisYear === yearOfArg && thisMonth === monthOfArg && yesterday === dayOfArg; 41 | if (isYesterday) { 42 | return `昨天 ${addZero(hourOfArg)}:${addZero(minuteOfArg)}`; 43 | } 44 | 45 | if (yearOfArg === thisYear) { 46 | return `${monthHandle(monthOfArg)}/${addZero(dayOfArg)}`; 47 | } 48 | 49 | return `${yearOfArg}/${monthHandle(monthOfArg)}/${addZero(dayOfArg)}`; 50 | }; 51 | -------------------------------------------------------------------------------- /server/src/app/context/ServicesContext.ts: -------------------------------------------------------------------------------- 1 | import { ChatService, GroupChatService, GroupService, UserService } from './../services'; 2 | 3 | export class ServicesContext { 4 | static instance: ServicesContext; 5 | 6 | static getInstance(): ServicesContext { 7 | if (!ServicesContext.instance) { 8 | ServicesContext.instance = new ServicesContext(); 9 | } 10 | return ServicesContext.instance; 11 | } 12 | 13 | // user 14 | private _userService: UserService; 15 | public get userService() { 16 | return this._userService; 17 | } 18 | public setuserService(service: UserService): ServicesContext { 19 | this._userService = service; 20 | return this; 21 | } 22 | 23 | // group 24 | private _groupService: GroupService; 25 | public get groupService() { 26 | return this._groupService; 27 | } 28 | public setGroupService(service: GroupService): ServicesContext { 29 | this._groupService = service; 30 | return this; 31 | } 32 | 33 | // chat 34 | private _chatService: ChatService; 35 | public get chatService() { 36 | return this._chatService; 37 | } 38 | public setChatService(service: ChatService): ServicesContext { 39 | this._chatService = service; 40 | return this; 41 | } 42 | 43 | // groupChat 44 | private _groupChatService: GroupChatService; 45 | public get groupChatService() { 46 | return this._groupChatService; 47 | } 48 | public setgroupChatService(service: GroupChatService): ServicesContext { 49 | this._groupChatService = service; 50 | return this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/webpack.config.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | const nodeExternals = require('webpack-node-externals'); 4 | const webpack = require('webpack'); 5 | const path = require('path'); 6 | 7 | // function srcPath(subdir) { 8 | // return path.join(__dirname, subdir); 9 | // } 10 | 11 | const config = { 12 | mode: 'development', 13 | entry: './src/main.ts', 14 | target: 'node', 15 | output: { 16 | // Puts the output at the root of the dist folder 17 | path: path.join(__dirname, 'dist'), 18 | filename: 'index.js', 19 | }, 20 | resolve: { 21 | alias: { 22 | 'react-dom': '@hot-loader/react-dom', 23 | }, 24 | extensions: ['.ts', '.js'], 25 | modules: ['node_modules', 'src'], 26 | }, 27 | plugins: [ 28 | new webpack.LoaderOptionsPlugin({ 29 | options: { 30 | test: /\.ts$/, 31 | ts: { 32 | compiler: 'typescript', 33 | configFileName: 'tsconfig.json', 34 | }, 35 | tslint: { 36 | emitErrors: true, 37 | failOnHint: true, 38 | }, 39 | }, 40 | }), 41 | ], 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.ts$/, 46 | use: 'awesome-typescript-loader', 47 | }, 48 | ], 49 | }, 50 | externals: [nodeExternals()], 51 | }; 52 | 53 | module.exports = (env, argv) => { 54 | if (!argv.prod) { 55 | config.devtool = 'source-map'; 56 | } 57 | config.resolve.alias = { 58 | '@configs': path.join(__dirname, `src/configs/configs.${argv.prod ? 'prod' : 'dev'}.ts`), 59 | }; 60 | return config; 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/ShareChatCard/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ShareModal from '../ShareModal'; 4 | 5 | class ShareChatCard extends Component { 6 | state = { 7 | showShareModal: false, 8 | }; 9 | 10 | _showShareModal = () => { 11 | this.props.hideOtherModal(); 12 | this.setState(state => ({ showShareModal: !state.showShareModal })); 13 | }; 14 | 15 | _closeShareModal = () => { 16 | this.setState({ showShareModal: false }); 17 | }; 18 | 19 | render() { 20 | const { allGroupChats, homePageList, title, chatId } = this.props; 21 | return ( 22 |
23 | 26 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | export default ShareChatCard; 40 | 41 | ShareChatCard.propTypes = { 42 | title: PropTypes.string, 43 | allGroupChats: PropTypes.instanceOf(Map), 44 | homePageList: PropTypes.array, 45 | chatId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 46 | hideOtherModal: PropTypes.func, 47 | }; 48 | 49 | ShareChatCard.defaultProps = { 50 | title: '', 51 | allGroupChats: new Map(), 52 | homePageList: [], 53 | chatId: null, 54 | hideOtherModal() {}, 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/Setting/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | @import '~rc-switch/assets/index.css'; 3 | 4 | .setting{ 5 | width: 100%; 6 | height: 100%; 7 | padding-top: 100px; 8 | text-align: center; 9 | position: relative; 10 | color: $gray; 11 | .notificationConfig { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | .rc-switch-checked { 16 | border: 1px solid $blue; 17 | background-color: $blue; 18 | } 19 | } 20 | .githubStarRender { 21 | color: #fff; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | background-color: #9baec8; 26 | height: 26px; 27 | margin-left: 20px; 28 | position: absolute; 29 | right: 20px; 30 | top: 30px; 31 | cursor: pointer; 32 | .githubIcon { 33 | height: 30px; 34 | font-size: 28px; 35 | padding-right: 4px; 36 | padding-left: 4px; 37 | } 38 | .starTitle{ 39 | font-size: 12px; 40 | height: 26px; 41 | padding-right: 4px; 42 | padding-left: 4px; 43 | line-height: 26px; 44 | color: #fff; 45 | background-color: #5893d4; 46 | } 47 | } 48 | .userInfo p { 49 | font-size: 12px; 50 | padding-top: 20px; 51 | } 52 | .name{ 53 | font-size: 18px; 54 | padding-top: 20px; 55 | } 56 | .baseButton { 57 | position: relative; 58 | font-size: 12px; 59 | margin-top: 80px; 60 | } 61 | .contact, .contact a { 62 | text-decoration:none; 63 | margin: 40px 0; 64 | font-size: 14px; 65 | cursor: pointer; 66 | display: block; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ghChat 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Tabs/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unused-prop-types */ 2 | import React, { Component } from 'react'; 3 | import { withRouter, Link } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | import './style.scss'; 6 | import MyInfo from '../MyInfo'; 7 | import { initAppOnce } from './help'; 8 | 9 | class Tabs extends Component { 10 | constructor(props) { 11 | super(props); 12 | this._userInfo = JSON.parse(localStorage.getItem('userInfo')); 13 | initAppOnce(props); 14 | } 15 | 16 | render() { 17 | const { pathname } = this.props.location; 18 | const showMessageIcon = 19 | pathname === '/' || /\/group_chat|\/private_chat|\/robot_chat/.test(pathname); 20 | return ( 21 |
22 | 23 |
24 | 25 | 28 | 29 |
30 |
31 | 32 | 35 | 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default withRouter(Tabs); 43 | 44 | Tabs.propTypes = { 45 | location: PropTypes.object, 46 | initializedApp: PropTypes.bool, 47 | initApp: PropTypes.func, 48 | }; 49 | 50 | Tabs.defaultProps = { 51 | location: { pathname: '/' }, 52 | initializedApp: false, 53 | initApp() {}, 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/CreateGroupModal/styles.scss: -------------------------------------------------------------------------------- 1 | .groupModalContent { 2 | font-size: 22px; 3 | margin-top: 20px; 4 | text-align: center; 5 | div { 6 | text-align: left; 7 | input, textarea { 8 | font-size: 14px; 9 | outline: none; 10 | resize: none; 11 | padding-left: 10px; 12 | border: 1px solid #fff; 13 | color: #999; 14 | width: 70%; 15 | } 16 | span { 17 | left: 10px; 18 | text-align: right; 19 | float: left; 20 | font-size: 14px; 21 | margin-top: 4px; 22 | width: 30%; 23 | } 24 | textarea { 25 | padding-top: 6px; 26 | width: 70%; 27 | } 28 | input::-webkit-input-placeholder , textarea::-webkit-input-placeholder { 29 | /* WebKit browsers */ 30 | // font-size: 0.24rem; 31 | color: #cccccc; 32 | } 33 | input:-moz-placeholder, textarea:-moz-placeholder { 34 | /* Mozilla Firefox 4 to 18 */ 35 | // font-size: 0.24rem; 36 | color: #cccccc; 37 | } 38 | input::-moz-placeholder, textarea::-moz-placeholder { 39 | /* Mozilla Firefox 19+ */ 40 | // font-size: 0.24rem; 41 | color: #cccccc; 42 | } 43 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 44 | /* Internet Explorer 10+ */ 45 | color: #cccccc; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /server/src/app/controllers/login.controller.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as md5 from 'md5'; 3 | import configs from '@configs'; 4 | import { ServicesContext } from '../context'; 5 | 6 | // 用户名登录系统只涉及非github用户,也就是github用户只能走github授权来登录 7 | export const loginController = async (ctx, next) => { 8 | const { userService } = ServicesContext.getInstance(); 9 | 10 | const { name = '', password = '' } = ctx.request.body; 11 | if (name === '' || password === '') { 12 | ctx.body = { 13 | success: false, 14 | message: '用户名或密码不能为空', 15 | }; 16 | return; 17 | } 18 | const RowDataPacket = await userService.findDataByName(name); 19 | const res = JSON.parse(JSON.stringify(RowDataPacket)); 20 | if (res.length > 0) { 21 | // 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端 22 | if (md5(password) === res[0].password) { 23 | const { id, name, sex, website, github, intro, company, avatar, location, socketId } = res[0]; 24 | const payload = { id }; 25 | const token = jwt.sign(payload, configs.jwt_secret, { 26 | expiresIn: Math.floor(Date.now() / 1000) + 24 * 60 * 60 * 7, // 一周 27 | }); 28 | ctx.body = { 29 | success: true, 30 | message: '登录成功', 31 | userInfo: { 32 | name, 33 | user_id: id, 34 | sex, 35 | website, 36 | github, 37 | intro, 38 | company, 39 | avatar, 40 | location, 41 | socketId, 42 | token, 43 | }, 44 | }; 45 | } else { 46 | ctx.body = { 47 | success: false, 48 | message: '密码错误', 49 | }; 50 | } 51 | } else { 52 | ctx.body = { 53 | success: false, 54 | message: '用户名错误', 55 | }; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './style.scss'; 4 | import ModalBase from '../ModalBase'; 5 | 6 | function confirmCancelRender(props) { 7 | const { hasCancel, hasConfirm, confirm, cancel } = props; 8 | if (hasCancel && hasConfirm) { 9 | return ( 10 |
11 |

取消

12 |

确定

13 |
14 | ); 15 | } 16 | if (hasConfirm || hasCancel) { 17 | return ( 18 |
19 | {hasCancel &&

取消

} 20 | {hasConfirm &&

确定

} 21 |
22 | ); 23 | } 24 | return null; 25 | } 26 | 27 | confirmCancelRender.propTypes = { 28 | hasCancel: PropTypes.bool, 29 | hasConfirm: PropTypes.bool, 30 | cancel: PropTypes.func, // 点击遮罩取消Modal的前提是有传cancel方法 31 | confirm: PropTypes.func, 32 | }; 33 | 34 | confirmCancelRender.defaultProps = { 35 | hasCancel: false, 36 | hasConfirm: false, 37 | cancel: undefined, 38 | confirm: undefined, 39 | }; 40 | 41 | function dialogRender(props) { 42 | const { title, children } = props; 43 | return ( 44 |
45 |

{title}

46 | {children} 47 | {confirmCancelRender({ ...props })} 48 |
49 | ); 50 | } 51 | 52 | dialogRender.propTypes = { 53 | title: PropTypes.string, 54 | children: PropTypes.node, 55 | }; 56 | 57 | dialogRender.defaultProps = { 58 | title: '', 59 | children: undefined, 60 | }; 61 | 62 | const ModalDialogRender = ModalBase(dialogRender); 63 | // TODO: (refactor)take thinner component 64 | export default function Modal(props) { 65 | return ; 66 | } 67 | -------------------------------------------------------------------------------- /server/src/app/services/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { query } from '../utils/db'; 2 | 3 | export class ChatService { 4 | /** 5 | * 获取私聊相关内容 6 | * @param to_user 私聊对象的id 7 | * @param from_user 私聊者自己的id 8 | * @return from_user 此条信息的发送者 9 | * to_user 此条信息的接收者 10 | * message 私聊信息 11 | * time 时间 12 | * avatar 发送者的头像 13 | // * sex 发送者的性别 14 | // * location 发送者居住地 15 | */ 16 | getPrivateDetail(from_user, to_user, start, count) { 17 | const data = [from_user, to_user, to_user, from_user, start, count]; 18 | const _sql = 19 | 'SELECT * FROM ( SELECT p.from_user,p.to_user,p.message,p.attachments,p.time,i.avatar,i.name, i.github_id from private_msg as p inner join user_info as i on p.from_user = i.id where (p.from_user = ? AND p.to_user = ? ) or (p.from_user = ? AND p.to_user = ? ) order by time desc limit ?,? ) as n order by n.time'; 20 | return query(_sql, data); 21 | } 22 | 23 | /** 24 | * 存聊天记录 25 | * @param from_user 发送者id 26 | * @param to_user 接收者id 27 | * @param message 消息 28 | * @param name 用户名 29 | * @param time 时间 30 | * @return 31 | */ 32 | 33 | savePrivateMsg({ from_user, to_user, message, time, attachments }) { 34 | const data = [from_user, to_user, message, time, attachments]; 35 | const _sql = 36 | ' INSERT INTO private_msg(from_user,to_user,message,time,attachments) VALUES(?,?,?,?,?); '; 37 | return query(_sql, data); 38 | } 39 | 40 | getUnreadCount({ sortTime, from_user, to_user }) { 41 | const data = [sortTime, from_user, to_user, to_user, from_user]; 42 | const _sql = 43 | 'SELECT count(time) as unread FROM private_msg AS p WHERE p.time > ? and ((p.from_user = ? and p.to_user= ?) or (p.from_user = ? and p.to_user=?));'; 44 | return query(_sql, data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/BrowserNotification/index.js: -------------------------------------------------------------------------------- 1 | import notification from '../../components/Notification'; 2 | import store from '../../redux/store'; 3 | 4 | export default class BrowserNotification { 5 | constructor() { 6 | this._notificationEnable = false; 7 | this._checkOrRequirePermission(); 8 | } 9 | 10 | _checkOrRequirePermission() { 11 | if (!this.notification) { 12 | // eslint-disable-next-line no-alert 13 | notification('此浏览器不支持浏览器提示', 'warn', 3); 14 | return; 15 | } 16 | if (this.hasPermission) { 17 | this._notificationEnable = true; 18 | return; 19 | } 20 | if (this.permission && this.permission !== 'denied') { 21 | this.notification.requestPermission(status => { 22 | if (this.permission !== status) { 23 | this.permission = status; 24 | } 25 | if (status === 'granted') { 26 | this._notificationEnable = true; 27 | } 28 | }); 29 | } 30 | } 31 | 32 | notify({ title, text, icon, onClick, audio }) { 33 | const { 34 | globalSettingsState: { notification }, 35 | } = store.getState(); 36 | if (!this._notificationEnable || !notification) { 37 | return; 38 | } 39 | const n = new window.Notification(title, { body: text, icon }); 40 | n.onclick = () => { 41 | onClick(); 42 | n.close(); 43 | }; 44 | this._onPlay(audio); 45 | } 46 | 47 | _onPlay(src) { 48 | const audio = document.createElement('audio'); 49 | audio.setAttribute('src', src); 50 | audio.play(); 51 | } 52 | 53 | get permission() { 54 | return this.notification.permission; 55 | } 56 | 57 | set permission(value) { 58 | if (value) { 59 | this.notification.permission = value; 60 | } 61 | } 62 | 63 | get hasPermission() { 64 | return this.permission && this.permission === 'granted'; 65 | } 66 | 67 | get notification() { 68 | return window.Notification; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/containers/GroupChatPage/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { 4 | updateHomePageListAction, 5 | deleteHomePageListAction, 6 | updateListGroupNameAction, 7 | } from '../HomePageList/homePageListAction'; 8 | import { 9 | addGroupMessagesAction, 10 | deleteGroupChatAction, 11 | addGroupInfoAction, 12 | addGroupMessageAndInfoAction, 13 | updateGroupTitleNoticeAction, 14 | } from './groupChatAction'; 15 | import { deletePrivateChatAction } from '../PrivateChatPage/privateChatAction'; 16 | import GroupChat from '../../components/GroupChat'; 17 | 18 | const mapStateToProps = state => ({ 19 | allGroupChats: state.allGroupChatsState, 20 | allPrivateChats: state.allPrivateChatsState, 21 | homePageList: state.homePageListState, 22 | relatedCurrentChat: state.relatedCurrentChat, 23 | initApp: state.initAppState, 24 | shareData: state.shareState, 25 | }); 26 | 27 | const mapDispatchToProps = dispatch => ({ 28 | addGroupMessageAndInfo(arg = {}) { 29 | dispatch(addGroupMessageAndInfoAction({ ...arg })); 30 | }, 31 | addGroupMessages(arg = {}) { 32 | dispatch(addGroupMessagesAction({ ...arg })); 33 | }, 34 | deleteGroupChat(arg = {}) { 35 | dispatch(deleteGroupChatAction({ ...arg })); 36 | }, 37 | addGroupInfo(arg = {}) { 38 | dispatch(addGroupInfoAction({ ...arg })); 39 | }, 40 | updateHomePageList(arg = {}) { 41 | dispatch(updateHomePageListAction({ ...arg })); 42 | }, 43 | deleteHomePageList(arg = {}) { 44 | dispatch(deleteHomePageListAction({ ...arg })); 45 | }, 46 | updateGroupTitleNotice(arg = {}) { 47 | dispatch(updateGroupTitleNoticeAction({ ...arg })); 48 | }, 49 | updateListGroupName(arg = {}) { 50 | dispatch(updateListGroupNameAction({ ...arg })); 51 | }, 52 | deletePrivateChat(arg = {}) { 53 | dispatch(deletePrivateChatAction({ ...arg })); 54 | }, 55 | }); 56 | 57 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(GroupChat)); 58 | -------------------------------------------------------------------------------- /src/components/Modal/style.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | .dialogRender { 3 | h3 { 4 | text-align: center; 5 | font-size: 14px; 6 | padding: 10px 0; 7 | border-bottom: 1px solid rgb(235, 232, 232); 8 | } 9 | p { 10 | font-size: 16px; 11 | color: rgba(0, 0, 0, 0.808); 12 | margin-bottom: 10px; 13 | } 14 | 15 | .twoButton { 16 | height:40px; 17 | p { 18 | background: transparent; 19 | color: $blue; 20 | font-size: 14px; 21 | height:40px; 22 | line-height:40px; 23 | width: 50%; 24 | position: absolute; 25 | cursor: pointer; 26 | } 27 | p:nth-of-type(1) { 28 | border-top: 1px solid rgb(235, 232, 232);; 29 | border-right: 1px solid rgb(235, 232, 232); 30 | left: 0; 31 | border-radius: 0 0 0 8px; 32 | } 33 | p:nth-of-type(2) { 34 | border-top: 1px solid rgb(235, 232, 232); 35 | right: 0; 36 | border-radius: 0 0 8px 0; 37 | } 38 | p:focus, 39 | p:hover { 40 | font-weight: bold; 41 | background: #EFEFEF; 42 | } 43 | p:active { 44 | background: #D6D6D6; 45 | } 46 | } 47 | .oneButton { 48 | height:40px; 49 | p { 50 | background: transparent; 51 | color: $blue; 52 | height:40px; 53 | font-size: 16px; 54 | line-height:40px; 55 | width: 100%; 56 | position: absolute; 57 | text-decoration: none; 58 | border-top: 1px solid rgb(235, 232, 232); 59 | left: 0; 60 | right: 0; 61 | border-radius: 0 0 0 8px; 62 | } 63 | p:focus, 64 | p:hover { 65 | font-weight: bold; 66 | background: #EFEFEF; 67 | } 68 | p:active { 69 | background: #D6D6D6; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/GroupChatInfo/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/base.scss"; 2 | .chatInformation { 3 | height: calc(100% - 50px); 4 | width: 200px; 5 | position: absolute; 6 | top: 50px; 7 | right: 0; 8 | background-color: $backgroundColor; 9 | opacity: 0.97; 10 | z-index: 99; 11 | .info { 12 | padding: 10px 16px; 13 | .noticeTitle { 14 | color: $title; 15 | padding-bottom: 10px; 16 | font-size: 14px; 17 | position: relative; 18 | .iconEditor { 19 | position: absolute; 20 | right: 10px; 21 | font-size: 18px; 22 | cursor: pointer; 23 | } 24 | } 25 | .noticeContent { 26 | word-wrap: break-word; 27 | height: 60px; 28 | overflow: hidden; 29 | overflow-y: auto; 30 | font-size: 12px; 31 | } 32 | .memberTitle { 33 | font-size: 14px; 34 | padding-top: 10px; 35 | color: $title; 36 | display: flex; 37 | justify-content: space-between; 38 | .showAllMember { 39 | color: $blue; 40 | cursor: pointer; 41 | } 42 | } 43 | } 44 | .members{ 45 | list-style: none; 46 | overflow-y: auto; 47 | font-size: 18px; 48 | height: calc(100% - 190px); 49 | .member { 50 | &:hover { 51 | background-color: #fff; 52 | } 53 | padding: 6px 10px; 54 | display: flex; 55 | align-items: center; 56 | cursor: pointer; 57 | .memberName { 58 | font-size: 14px; 59 | overflow: hidden; 60 | text-overflow: ellipsis; 61 | white-space: nowrap; 62 | width: 120px; 63 | padding-left: 10px; 64 | } 65 | } 66 | } 67 | .leave { 68 | position: absolute; 69 | bottom: 0; 70 | border-top: 1px solid $border; 71 | width: 100%; 72 | text-align: center; 73 | padding: 10px 0; 74 | font-size: 14px; 75 | color: $warnRed; 76 | cursor: pointer; 77 | } 78 | } -------------------------------------------------------------------------------- /src/components/ChatHeader/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import './style.scss'; 5 | 6 | class ChatHeader extends Component { 7 | clickToBack = () => { 8 | this.props.history.push('/'); 9 | }; 10 | 11 | _clickChatInfo = () => { 12 | const { showGroupChatInfo, showPersonalInfo, chatType, hasShowed } = this.props; 13 | if (chatType === 'group') { 14 | showGroupChatInfo(!hasShowed); 15 | } else if (chatType === 'private') { 16 | showPersonalInfo(); 17 | } 18 | }; 19 | 20 | _showShareModal = () => { 21 | this.props.showShareModal(); 22 | }; 23 | 24 | render() { 25 | const { title, chatType, showShareIcon } = this.props; 26 | const icon = chatType === 'group' ? '#icon-group' : '#icon-people'; 27 | const isRobotChat = chatType === 'robot'; 28 | return ( 29 |
30 | 33 |
{title}
34 | {showShareIcon && ( 35 | 38 | )} 39 | {!isRobotChat && ( 40 | 43 | )} 44 |
45 | ); 46 | } 47 | } 48 | 49 | export default withRouter(ChatHeader); 50 | 51 | ChatHeader.propTypes = { 52 | title: PropTypes.string, 53 | history: PropTypes.object, 54 | chatType: PropTypes.string.isRequired, 55 | showGroupChatInfo: PropTypes.func, 56 | showPersonalInfo: PropTypes.func, 57 | hasShowed: PropTypes.bool, 58 | showShareIcon: PropTypes.bool, 59 | }; 60 | 61 | ChatHeader.defaultProps = { 62 | title: '', 63 | history: undefined, 64 | showGroupChatInfo: undefined, 65 | showPersonalInfo: undefined, 66 | hasShowed: false, 67 | showShareIcon: false, 68 | }; 69 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const webpack = require('webpack'); 5 | const CompressionPlugin = require('compression-webpack-plugin'); 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 8 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 9 | const TerserJSPlugin = require('terser-webpack-plugin'); 10 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 11 | const commonConfig = require('./webpack.common.config.js'); 12 | 13 | module.exports = merge(commonConfig, { 14 | mode: 'production', 15 | entry: { 16 | app: ['babel-polyfill', path.resolve(__dirname, 'src/index.js')], 17 | }, 18 | /* 输出到build文件夹,输出文件名字为[name].[chunkhash].js */ 19 | output: { 20 | filename: '[name].[contenthash].js', 21 | path: path.resolve(__dirname, 'build'), 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.scss$/, 27 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 28 | }, 29 | ], 30 | }, 31 | plugins: [ 32 | new CleanWebpackPlugin(['build/*.*']), 33 | new CompressionPlugin(), 34 | new webpack.HashedModuleIdsPlugin(), 35 | new MiniCssExtractPlugin({ 36 | ignoreOrder: true, 37 | // Options similar to the same options in webpackOptions.output 38 | // both options are optional 39 | filename: '[name].css', 40 | chunkFilename: '[id].css', 41 | }), 42 | new CopyWebpackPlugin([ 43 | { from: 'src/manifest.json', to: 'manifest.json' }, 44 | { from: 'src/service-worker.js', to: 'service-worker.js' }, 45 | ]), 46 | new ProgressBarPlugin(), 47 | ], 48 | optimization: { 49 | minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], 50 | splitChunks: { 51 | cacheGroups: { 52 | vendor: { 53 | test: /node_modules/, 54 | chunks: 'initial', 55 | name: 'vendor', 56 | enforce: true, 57 | }, 58 | }, 59 | }, 60 | runtimeChunk: true, 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/MyInfo/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import UserAvatar from '../UserAvatar'; 3 | import PersonalInfo from '../PersonalInfo'; 4 | import ShareModal from '../ShareModal'; 5 | import store from '../../redux/store'; 6 | import './styles.scss'; 7 | 8 | class MyInfo extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | showShareModal: false, 13 | showPersonalInfo: false, 14 | }; 15 | this._userInfo = JSON.parse(localStorage.getItem('userInfo')); 16 | } 17 | 18 | _showPersonalInfo = () => { 19 | this.setState(state => ({ showPersonalInfo: !state.showPersonalInfo })); 20 | }; 21 | 22 | _showShareModal = () => { 23 | this.setState(state => ({ 24 | showShareModal: !state.showShareModal, 25 | showPersonalInfo: false, 26 | })); 27 | }; 28 | 29 | _closeShareModal = () => { 30 | this.setState({ showShareModal: false }); 31 | }; 32 | 33 | get shareLink() { 34 | return `${window.location.origin}/private_chat/${this._userInfo.user_id}`; 35 | } 36 | 37 | render() { 38 | const { name, avatar, github_id, user_id } = this._userInfo; 39 | const { allGroupChatsState, homePageListState } = store.getState(); 40 | return ( 41 |
42 | 49 | 57 | 67 |
68 | ); 69 | } 70 | } 71 | 72 | export default MyInfo; 73 | -------------------------------------------------------------------------------- /src/containers/LogInPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Request from '../../utils/request'; 3 | import Modal from '../../components/Modal'; 4 | import notification from '../../components/Notification'; 5 | import SignInSignUp from '../../components/SignInSignUp'; 6 | import './index.scss'; 7 | 8 | class LogIn extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | name: '', 14 | password: '', 15 | modal: { 16 | visible: false, 17 | }, 18 | }; 19 | } 20 | 21 | async login() { 22 | const { name, password } = this.state; 23 | if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/.test(name)) { 24 | notification('用户名只能由汉字,数字,字母,下划线组成', 'warn'); 25 | return; 26 | } 27 | if (!/^[A-Za-z0-9]+$/.test(password)) { 28 | notification('密码只能由字母数字组成', 'warn'); 29 | return; 30 | } 31 | try { 32 | const res = await Request.axios('post', '/api/v1/login', { name, password }); 33 | if (res && res.success) { 34 | localStorage.setItem('userInfo', JSON.stringify(res.userInfo)); 35 | // 弹窗 36 | this.setState({ 37 | modal: { 38 | visible: true, 39 | }, 40 | }); 41 | } else { 42 | notification(res.message, 'error'); 43 | } 44 | } catch (error) { 45 | notification(error, 'error'); 46 | } 47 | } 48 | 49 | setValue = value => { 50 | const { name, password } = value; 51 | this.setState({ name, password }, async () => { 52 | await this.login(); 53 | }); 54 | }; 55 | 56 | confirm = () => { 57 | this.setState({ 58 | modal: { 59 | visible: true, 60 | }, 61 | }); 62 | window.location.reload(); 63 | const originalLink = sessionStorage.getItem('originalLink'); 64 | if (originalLink) { 65 | sessionStorage.removeItem('originalLink'); 66 | window.location.href = originalLink; 67 | return; 68 | } 69 | window.location.href = '/'; 70 | }; 71 | 72 | render() { 73 | const { visible } = this.state.modal; 74 | return ( 75 |
76 | 77 |

您已登录成功

78 |
79 | 80 |
81 | ); 82 | } 83 | } 84 | 85 | export default LogIn; 86 | -------------------------------------------------------------------------------- /src/containers/RegisterPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './index.scss'; 3 | import Request from '../../utils/request'; 4 | import Modal from '../../components/Modal'; 5 | import notification from '../../components/Notification'; 6 | import SignInSignUp from '../../components/SignInSignUp'; 7 | 8 | export default class Register extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | name: '', 13 | password: '', 14 | modal: { 15 | visible: false, 16 | }, 17 | }; 18 | } 19 | 20 | register = async () => { 21 | const { name, password } = this.state; 22 | if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/.test(name)) { 23 | notification('用户名只能由汉字,数字,字母,下划线组成', 'warn'); 24 | return; 25 | } 26 | if (!/^[A-Za-z0-9]+$/.test(password)) { 27 | notification('密码只能由字母数字组成', 'warn'); 28 | return; 29 | } 30 | try { 31 | const res = await Request.axios('post', '/api/v1/register', { 32 | name, 33 | password, 34 | }); 35 | if (res && res.success) { 36 | // 弹窗 37 | this.setState({ 38 | modal: { 39 | visible: true, 40 | }, 41 | }); 42 | } else { 43 | notification(res.message, 'error'); 44 | } 45 | } catch (error) { 46 | notification(error, 'error'); 47 | } 48 | }; 49 | 50 | setValue = value => { 51 | const { name, password } = value; 52 | this.setState( 53 | { 54 | name, 55 | password, 56 | }, 57 | async () => { 58 | await this.register(); 59 | }, 60 | ); 61 | }; 62 | 63 | confirm = () => { 64 | this.setState({ 65 | // eslint-disable-next-line react/no-unused-state 66 | visible: false, 67 | }); 68 | 69 | // eslint-disable-next-line react/prop-types 70 | this.props.history.push('/login'); 71 | }; 72 | 73 | render() { 74 | const { visible } = this.state.modal; 75 | return ( 76 |
77 | 78 |

您已注册成功

79 |
80 | {/* */} 81 | 82 |
83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/src/app/server.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as Koa from 'koa'; 3 | import { createServer } from 'http'; 4 | 5 | import { Logger } from './utils/Logger'; 6 | import { appSocket } from './socket/app.socket'; 7 | 8 | const log = Logger('app:core:server'); 9 | 10 | export class Server { 11 | static app: Koa; 12 | static server: http.Server; 13 | 14 | static init(cb: (app: Koa) => any) { 15 | if (!Server.app) { 16 | Server.app = new Koa(); 17 | 18 | if (cb) { 19 | cb(Server.app); 20 | } 21 | } 22 | return Server; 23 | } 24 | 25 | static createServer() { 26 | Server.server = createServer(Server.app.callback()); 27 | return Server; 28 | } 29 | 30 | static run(port: string) { 31 | appSocket(Server.server); 32 | 33 | Server.server.listen(this.normalizePort(port)); 34 | // .on('listening', () => this.onListening(Server.server)) 35 | // .on('error', (error) => this.onError(Server.server, error)); 36 | 37 | // log.debug('Server was started on environment %s', process.env.NODE_ENV); 38 | return Server; 39 | } 40 | 41 | static async createConnection() { 42 | return Server; 43 | } 44 | 45 | private static normalizePort(port: string): number | string | boolean { 46 | const parsedPort = parseInt(port, 10); 47 | if (isNaN(parsedPort)) { 48 | // named pipe 49 | return port; 50 | } 51 | if (parsedPort >= 0) { 52 | // port number 53 | return parsedPort; 54 | } 55 | return false; 56 | } 57 | 58 | private static onListening(server: http.Server): void { 59 | log.debug(`Listening on ${this.bind(server.address())}`); 60 | } 61 | 62 | private static onError(server: http.Server, error: Error): void { 63 | if (error['syscall'] !== 'listen') { 64 | throw error; 65 | } 66 | const addr = server.address(); 67 | // handle specific listen errors with friendly messages 68 | switch (error['code']) { 69 | case 'EACCES': 70 | log.error(`${this.bind(addr)} requires elevated privileges`); 71 | process.exit(1); 72 | break; 73 | case 'EADDRINUSE': 74 | log.error(`${this.bind(addr)} is already in use`); 75 | process.exit(1); 76 | break; 77 | default: 78 | throw error; 79 | } 80 | } 81 | 82 | private static bind(addr: string | any): string { 83 | return typeof addr === 'string' ? `pipe ${addr}` : `port http://localhost:${addr.port}`; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/src/app/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston'; 2 | import configs from '@configs'; 3 | // imports debug moduel 4 | import * as Debug from 'debug'; 5 | 6 | // Imports the Google Cloud client library for Winston 7 | // const { LoggingWinston } = require('@google-cloud/logging-winston'); 8 | // const loggingWinston = new LoggingWinston(); 9 | 10 | /** 11 | * Configures the winston logger. There are also file and remote transports available 12 | */ 13 | const logger = winston.createLogger({ 14 | transports: [ 15 | new winston.transports.Console(), 16 | // new winston.transports.File({ filename: 'error.log', level: 'error' }), 17 | // new winston.transports.File({ filename: 'combined.log' }) 18 | // Add Stackdriver Logging 19 | // loggingWinston, 20 | ], 21 | exitOnError: false, 22 | }); 23 | 24 | const stream = streamFunction => ({ 25 | stream: streamFunction, 26 | }); 27 | 28 | const write = writeFunction => ({ 29 | write: (message: string) => writeFunction(message), 30 | }); 31 | 32 | /** 33 | * Winston logger stream for the morgan plugin 34 | */ 35 | export const winstonStream = stream(write(logger.info)); 36 | 37 | // Configure the debug module 38 | process.env.DEBUG = configs.logger.debug; 39 | 40 | const debug = Debug('app:response'); 41 | 42 | /** 43 | * Debug stream for the morgan plugin 44 | */ 45 | export const debugStream = stream(write(debug)); 46 | 47 | /** 48 | * Exports a wrapper for all the loggers we use in this configuration 49 | */ 50 | const format = (scope: string, message: string): string => `[${scope}] ${message}`; 51 | 52 | const parse = (args: any[]) => (args.length > 0 ? args : ''); 53 | 54 | export const Logger = (scope: string) => { 55 | const scopeDebug = Debug(scope); 56 | return { 57 | debug(message: string, ...args: any[]) { 58 | if (configs.production) { 59 | logger.debug(format(scope, message), parse(args)); 60 | } 61 | scopeDebug(message, parse(args)); 62 | }, 63 | verbose: (message: string, ...args: any[]) => 64 | logger.verbose(format(scope, message), parse(args)), 65 | silly: (message: string, ...args: any[]) => logger.silly(format(scope, message), parse(args)), 66 | info: (message: string, ...args: any[]) => logger.info(format(scope, message), parse(args)), 67 | warn: (message: string, ...args: any[]) => logger.warn(format(scope, message), parse(args)), 68 | error: (message: string, ...args: any[]) => logger.error(format(scope, message), parse(args)), 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /server/src/app/services/groupChat.service.ts: -------------------------------------------------------------------------------- 1 | import { query } from '../utils/db'; 2 | 3 | export class GroupChatService { 4 | /** 5 | * 获取群消息 6 | * @param 群id 7 | * @return message 群消息 8 | * @return time 时间 9 | * @return from_user 发送人id 10 | * @return avatar 发送人头像 11 | */ 12 | getGroupMsg(groupId, start, count) { 13 | const _sql = 14 | 'SELECT * FROM (SELECT g.message,g.attachments,g.time,g.from_user,g.to_group_id, i.avatar ,i.name, i.github_id FROM group_msg As g inner join user_info AS i ON g.from_user = i.id WHERE to_group_id = ? order by time desc limit ?,?) as n order by n.time; '; 15 | return query(_sql, [groupId, start, count]); 16 | } 17 | 18 | /** 19 | * 获取群成员 20 | * @param 群id 21 | * @return group_member_id 群成员id 22 | */ 23 | getGroupMember(groupId) { 24 | const _sql = 25 | 'SELECT g.user_id, u.socketid, u.name, u.avatar, u.github_id, u.github, u.intro, u.company, u.location, u.website FROM group_user_relation AS g inner join user_info AS u ON g.user_id = u.id WHERE to_group_id = ?'; 26 | return query(_sql, groupId); 27 | } 28 | 29 | /** 30 | * 获取群资料 31 | * @param arr 包括 groupId groupName 至少一个 32 | * @return 33 | */ 34 | getGroupInfo(arr) { 35 | const _sql = 36 | 'SELECT to_group_id, name, group_notice, creator_id, create_time FROM group_info WHERE to_group_id = ? OR name = ? ;'; 37 | return query(_sql, arr); 38 | } 39 | 40 | /** 41 | * 存聊天记录 42 | * @param user_id 用户id 43 | * @param groupId 群id 44 | * @param message 消息 45 | * @param name 用户名 46 | * @param time 时间 47 | * @return 48 | */ 49 | 50 | saveGroupMsg({ from_user, to_group_id, message, time, attachments }) { 51 | const data = [from_user, to_group_id, message, time, attachments]; 52 | const _sql = 53 | ' INSERT INTO group_msg(from_user,to_group_id,message ,time, attachments) VALUES(?,?,?,?,?); '; 54 | return query(_sql, data); 55 | } 56 | 57 | /** 58 | * 群添加成员并返回群成员 59 | * @param user_id 用户id 60 | * @param groupId 群id 61 | * @return 62 | */ 63 | addGroupUserRelation(user_id, groupId) { 64 | const data = [groupId, user_id]; 65 | const _sql = ' INSERT INTO group_user_relation(to_group_id,user_id) VALUES(?,?); '; 66 | return query(_sql, data); 67 | } 68 | 69 | getUnreadCount({ sortTime, to_group_id }) { 70 | const data = [sortTime, to_group_id]; 71 | const _sql = 72 | 'SELECT count(time) as unread FROM group_msg as p where p.time > ? and p.to_group_id = ?;'; 73 | return query(_sql, data); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server/src/app/controllers/githubOAuth.controller.ts: -------------------------------------------------------------------------------- 1 | import configs from '@configs'; 2 | import * as jwt from 'jsonwebtoken'; 3 | import * as request from 'request-promise'; 4 | import { ServicesContext } from '../context'; 5 | 6 | async function getAccessToken(ctx) { 7 | try { 8 | const { code, clientId } = ctx.request.body; 9 | const date = { 10 | code, 11 | client_secret: configs.client_secret, 12 | client_id: clientId, 13 | }; 14 | const options = { 15 | method: 'POST', 16 | uri: 'https://github.com/login/oauth/access_token', 17 | body: date, 18 | json: true, // Automatically stringifies the body to JSON 19 | }; 20 | const response = await request(options); 21 | return response.access_token; 22 | } catch (error) { 23 | throw new Error(error); 24 | } 25 | } 26 | 27 | export const githubOAuthController = async (ctx, next) => { 28 | const { userService } = ServicesContext.getInstance(); 29 | 30 | try { 31 | const accessToken = await getAccessToken(ctx); 32 | const options = { 33 | uri: 'https://api.github.com/user', 34 | qs: { 35 | access_token: accessToken, // -> uri + '?access_token=xxxxx%20xxxxx' 36 | }, 37 | headers: { 38 | 'User-Agent': 'Request-Promise', 39 | }, 40 | json: true, 41 | }; 42 | const response = await request(options); 43 | const { avatar_url, html_url, bio, login, location, id, blog, company } = response; 44 | const payload = { id }; 45 | const token = jwt.sign(payload, configs.jwt_secret, { 46 | expiresIn: Math.floor(Date.now() / 1000) + 24 * 60 * 60 * 7, // 一周 47 | }); 48 | const data = { 49 | avatar: avatar_url, 50 | github: html_url, 51 | intro: bio, 52 | name: login, 53 | location, 54 | token, 55 | github_id: id, 56 | website: blog, 57 | company, 58 | user_id: null, 59 | }; 60 | const RowDataPacket = await userService.findGithubUser(id); // judge if this github account exist 61 | let githubUser = JSON.parse(JSON.stringify(RowDataPacket)); 62 | if (githubUser.length > 0) { 63 | await userService.updateGithubUser(data); 64 | } else { 65 | await userService.insertGithubData(data); 66 | const RowDataPacket = await userService.findGithubUser(id); 67 | githubUser = JSON.parse(JSON.stringify(RowDataPacket)); 68 | } 69 | data.user_id = githubUser[0].id; 70 | console.log('github res && ctx.body', response, data); 71 | ctx.body = data; 72 | } catch (error) { 73 | throw new Error(error); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/Linkify/index.js: -------------------------------------------------------------------------------- 1 | // edit from https://github.com/tasti/react-linkify thanks @tasti 2 | import * as React from 'react'; 3 | 4 | import defaultComponentDecorator from './decorators/defaultComponentDecorator'; 5 | import defaultHrefDecorator from './decorators/defaultHrefDecorator'; 6 | import defaultMatchDecorator from './decorators/defaultMatchDecorator'; 7 | import defaultTextDecorator from './decorators/defaultTextDecorator'; 8 | 9 | class LinkifyWithTargetBlank extends React.Component { 10 | static defaultProps = { 11 | componentDecorator: defaultComponentDecorator, 12 | hrefDecorator: defaultHrefDecorator, 13 | matchDecorator: defaultMatchDecorator, 14 | textDecorator: defaultTextDecorator, 15 | }; 16 | 17 | parseString(string) { 18 | if (string === '') { 19 | return string; 20 | } 21 | 22 | const matches = this.props.matchDecorator(string); 23 | if (!matches) { 24 | return string; 25 | } 26 | 27 | const elements = []; 28 | let lastIndex = 0; 29 | matches.forEach((match, i) => { 30 | // Push preceding text if there is any 31 | if (match.index > lastIndex) { 32 | elements.push(string.substring(lastIndex, match.index)); 33 | } 34 | 35 | const decoratedHref = this.props.hrefDecorator(match.url); 36 | const decoratedText = this.props.textDecorator(match.text); 37 | const target = match.schema === 'mailto:' ? '_self' : '_blank'; 38 | const decoratedComponent = this.props.componentDecorator( 39 | decoratedHref, 40 | decoratedText, 41 | i, 42 | target, 43 | ); 44 | elements.push(decoratedComponent); 45 | 46 | lastIndex = match.lastIndex; 47 | }); 48 | 49 | // Push remaining text if there is any 50 | if (string.length > lastIndex) { 51 | elements.push(string.substring(lastIndex)); 52 | } 53 | 54 | return elements.length === 1 ? elements[0] : elements; 55 | } 56 | 57 | parse(children, key = 0) { 58 | if (typeof children === 'string') { 59 | return this.parseString(children); 60 | } 61 | if (React.isValidElement(children) && children.type !== 'a' && children.type !== 'button') { 62 | return React.cloneElement(children, { key }, this.parse(children.props.children)); 63 | } 64 | if (Array.isArray(children)) { 65 | return children.map((child, i) => this.parse(child, i)); 66 | } 67 | 68 | return children; 69 | } 70 | 71 | render() { 72 | return {this.parse(this.props.children)}; 73 | } 74 | } 75 | 76 | export default LinkifyWithTargetBlank; 77 | -------------------------------------------------------------------------------- /src/components/CreateGroupModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Modal from '../Modal'; 4 | import './styles.scss'; 5 | import notification from '../Notification'; 6 | 7 | export default class GroupModal extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | groupName: props.defaultGroupName, 12 | groupNotice: props.defaultGroupNotice, 13 | }; 14 | } 15 | 16 | handleChange = event => { 17 | const { name, value } = event.target; 18 | this.setState({ [name]: value }); 19 | }; 20 | 21 | _confirm = () => { 22 | const { groupName, groupNotice } = this.state; 23 | if (!groupName || !groupNotice) { 24 | notification('你有空行没填哦', 'error'); 25 | return; 26 | } 27 | if (groupName === 'ghChat') { 28 | notification('这个群名仅供项目本身使用啦,请用别的群名', 'error'); 29 | return; 30 | } 31 | this.props.confirm({ groupName, groupNotice }); 32 | }; 33 | 34 | render() { 35 | const { modalVisible, cancel, title } = this.props; 36 | const { groupName, groupNotice } = this.state; 37 | return ( 38 | 46 |
47 |
48 | 群名: 49 | 57 |
58 |
59 | 群公告: 60 |