├── .babelrc
├── .eslintrc
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── README.md
├── chat.gif
├── conf
└── log.conf.js
├── favicon.ico
├── index.html
├── package-lock.json
├── package.json
├── server.js
├── src
├── components
│ ├── ChatInput.tsx
│ ├── ChatRoom.tsx
│ ├── Messages.tsx
│ └── RoomStatus.tsx
├── container
│ └── App.tsx
├── context
│ └── index.tsx
├── imgs
│ ├── bg.jpg
│ └── whaletail.jpg
├── index.tsx
└── style
│ └── index.scss
├── tsconfig.json
├── webpack.config.js
└── webpack.config.prod.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": ["@babel/plugin-proposal-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "sourceType": "module"
5 | },
6 | "extends": ["alloy", "alloy/react", "alloy/typescript"],
7 | "plugins": ["react", "@typescript-eslint"],
8 | "settings": {
9 | "react": {
10 | "version": "detect"
11 | }
12 | },
13 | "rules": {
14 | "no-console": 1,
15 | "react/prop-types": 0,
16 | "no-undef": 0,
17 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"],
18 | "require-jsdoc": 1,
19 | "max-len": [1, { "code": 200 }],
20 | "object-curly-spacing": ["warn", "always"],
21 | "comma-dangle": [1, "never"]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | sourceType: 'module'
5 | },
6 | extends: ['alloy', 'alloy/react', 'alloy/typescript'],
7 | plugins: ['react', '@typescript-eslint'],
8 | settings: {
9 | react: {
10 | version: 'detect'
11 | }
12 | },
13 | rules: {
14 | 'no-console': 1,
15 | 'react/prop-types': 0,
16 | 'no-undef': 0,
17 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
18 | 'require-jsdoc': 1,
19 | 'max-len': [1, { code: 200 }],
20 | 'object-curly-spacing': ['warn', 'always'],
21 | 'comma-dangle': [1, 'never']
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.*
3 | dist/
4 | logs/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "semi": true,
4 | "singleQuote": true,
5 | "printWidth": 200,
6 | "arrowParens": "always"
7 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | os:
4 | - linux
5 | - osx
6 |
7 | node_js:
8 | - 'lts/*'
9 | - '8'
10 |
11 | script:
12 | - npm install
13 | - npm run lint
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/ymyqwe/Websocket-React-Chatroom)
2 |
3 | # Websocket-React-Chatroom
4 |
5 | A Chatroom powered by [`React`](https://reactjs.org/), [`Socket.io`](https://socket.io/)
6 |
7 | ## Features
8 |
9 | - Using [React-Hooks](https://reactjs.org/docs/hooks-intro.html) to manage state
10 | - Using [Socket.io](https://socket.io/) to receive real-time message
11 |
12 | ## Getting Started
13 |
14 | 1. Clone this repo
15 |
16 | 2. Install dependencies
17 |
18 | ```bash
19 | npm install
20 | ```
21 |
22 | 3. Try out
23 |
24 | ```bash
25 | npm start
26 | ```
27 |
28 | ## For WINDOWS
29 |
30 | `npm install` might fail.
31 |
32 | View detail with this [pull request](https://github.com/ymyqwe/Websocket-React-Chatroom/pull/9)
33 |
34 | - Try Out
35 |
36 | ```bash
37 | npm run start:CE
38 | ```
39 |
40 | ## Demo
41 |
42 | [Live Demo](http://chat.yumingyuan.me)
43 |
44 | ## Preview
45 |
46 | 
47 |
48 | ## TODO
49 |
50 | - [x] [Redux](https://github.com/reduxjs/redux)-like style state manage
51 | - [x] [Log4js](https://github.com/log4js-node/log4js-node)
52 | - [ ] [React-router](https://github.com/ReactTraining/react-router)
53 | - [x] [Typescript](https://github.com/Microsoft/TypeScript)
54 | - [x] [Webpack-v4](https://github.com/webpack/webpack)
55 | - [ ] Test
56 |
57 | ## Changelog
58 |
59 | ### [3.1.0] / 2019-12-30
60 |
61 | - Add [cross-env](https://www.npmjs.com/package/cross-env) package to support WINDOWS
62 |
63 | ### [3.0.0] / 2019-11-19
64 |
65 | - Refactor with Typescript
66 |
67 | ### [2.2.1] / 2019-05-10
68 |
69 | - Upgrade tar to 4.4.8 due to github security alerts
70 | - Upgrade [React](https://reactjs.org) and React-Dom to 16.8.6
71 |
72 | ### [2.2.0] / 2019-03-26
73 |
74 | - Upgrade webpack to v4
75 | - Add logs using log4js
76 |
77 | ### [2.1.0] / 2018-12-11
78 |
79 | - Refactor Message.js with `useRef` and `useEffect`
80 |
81 | ### [2.0.0] / 2018-12-03
82 |
83 | - [React Hooks](https://reactjs.org/docs/hooks-intro.html)
84 | - [Eslint](https://github.com/eslint/eslint)
85 | - [Prettier](https://github.com/prettier/prettier)
86 | - [Travis-CI](https://travis-ci.org/)
87 |
88 | ### [1.0.0] / 2017-02-15
89 |
90 | - React state
91 | - [Socket.io](https://socket.io/)
92 | - [Webpack-v2](https://github.com/webpack/webpack)
93 | - [Expressjs](https://github.com/expressjs/express)
94 |
--------------------------------------------------------------------------------
/chat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/chat.gif
--------------------------------------------------------------------------------
/conf/log.conf.js:
--------------------------------------------------------------------------------
1 | // documents https://log4js-node.github.io/log4js-node/dateFile.html
2 | module.exports = {
3 | appenders: {
4 | dateFile: {
5 | type: 'dateFile',
6 | filename: 'logs/chat.log',
7 | pattern: 'yyyy-MM-dd', /* roll log in days */
8 | daysToKeep: 30, /* keep log for 30 days */
9 | layout: { type: 'json', separator: ',' }
10 | }
11 | },
12 | categories: {
13 | default: { appenders: ['dateFile'], level: 'trace' }
14 | }
15 | }
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 在线聊天室
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "websocket-react-chatroom",
3 | "version": "1.0.0",
4 | "description": "A chatroom powered by React and Websocket",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "",
8 | "start": "NODE_ENV=dev node server.js",
9 | "prod": "NODE_ENV=production node server.js",
10 | "start:CE": "cross-env NODE_ENV=dev node server.js",
11 | "prod:CE": "cross-env NODE_ENV=production node server.js",
12 | "build": "webpack -p --config webpack.config.prod.js",
13 | "server": "run-s build prod",
14 | "lint": "eslint src --fix --ext .tsx,.ts"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/ymyqwe/Websocket-React-Chatroom.git"
19 | },
20 | "keywords": [
21 | "React",
22 | "Websocket",
23 | "Chatroom"
24 | ],
25 | "author": "ManiaU",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/ymyqwe/Websocket-React-Chatroom/issues"
29 | },
30 | "homepage": "https://github.com/ymyqwe/Websocket-React-Chatroom#readme",
31 | "dependencies": {
32 | "@babel/core": "^7.0.0",
33 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
34 | "@babel/preset-env": "^7.0.0",
35 | "@babel/preset-react": "^7.0.0",
36 | "@types/react": "^16.9.11",
37 | "@types/react-dom": "^16.9.3",
38 | "awesome-typescript-loader": "^5.2.1",
39 | "babel-core": "7.0.0-bridge.0",
40 | "babel-eslint": "^10.0.1",
41 | "babel-loader": "^8.0.5",
42 | "babel-preset-env": "^1.7.0",
43 | "babel-preset-react": "^6.22.0",
44 | "clean-webpack-plugin": "^2.0.1",
45 | "css-loader": "^3.2.0",
46 | "eslint-config-alloy": "^3.5.0",
47 | "eslint-plugin-react": "^7.11.1",
48 | "express": "^4.14.1",
49 | "file-loader": "^3.0.1",
50 | "html-webpack-plugin": "^3.2.0",
51 | "log4js": "^4.1.0",
52 | "micromatch": "^4.0.2",
53 | "node-sass": "^4.5.0",
54 | "npm-run-all": "^4.1.5",
55 | "react": "^16.8.6",
56 | "react-dom": "^16.8.6",
57 | "sass-loader": "^5.0.1",
58 | "socket.io": "^2.2.0",
59 | "source-map-loader": "^0.2.4",
60 | "style-loader": "^0.13.1",
61 | "typescript": "^3.6.4",
62 | "url-loader": "^1.1.2",
63 | "webpack": "^4.29.6",
64 | "webpack-hot-middleware": "^2.16.1"
65 | },
66 | "devDependencies": {
67 | "@typescript-eslint/eslint-plugin": "^2.7.0",
68 | "@typescript-eslint/parser": "^2.7.0",
69 | "cross-env": "^6.0.3",
70 | "eslint": "^6.8.0",
71 | "eslint-config-google": "^0.11.0",
72 | "open-browsers": "^1.1.1",
73 | "webpack-cli": "^3.3.0",
74 | "webpack-dev-middleware": "^1.12.2",
75 | "webpack-dev-server": "^3.9.0"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var path = require('path');
3 | var express = require('express');
4 | var app = express();
5 | var openBrowsers = require('open-browsers');
6 |
7 | // log
8 | const log4js = require('log4js');
9 | log4js.addLayout(
10 | 'json',
11 | (config) =>
12 | function(logEvent) {
13 | logEvent.data = logEvent.data[0];
14 | return JSON.stringify(logEvent) + config.separator;
15 | }
16 | );
17 | const logConf = require('./conf/log.conf');
18 | log4js.configure(logConf);
19 | const logger = log4js.getLogger('chatLog');
20 |
21 | // 开发模式热更新
22 | if (process.env.NODE_ENV !== 'production') {
23 | var webpack = require('webpack');
24 | var config = require('./webpack.config');
25 | var compiler = webpack(config);
26 | // use in develope mode
27 | app.use(
28 | require('webpack-dev-middleware')(compiler, {
29 | publicPath: config.output.publicPath
30 | })
31 | );
32 | app.use(require('webpack-hot-middleware')(compiler));
33 |
34 | app.get('/', function(req, res) {
35 | const filename = path.join(compiler.outputPath, 'index.html');
36 | compiler.outputFileSystem.readFile(filename, function(err, result) {
37 | res.set('content-type', 'text/html');
38 | res.send(result);
39 | res.end();
40 | });
41 | });
42 | } else {
43 | app.get('/', function(req, res) {
44 | res.sendFile(path.join(__dirname, 'dist/index.html'));
45 | });
46 | }
47 |
48 | var server = require('http').createServer(app);
49 | var io = require('socket.io')(server);
50 |
51 | app.use(express.static(path.join(__dirname, '/')));
52 |
53 | // 在线用户
54 | var onlineUsers = {};
55 | // 在线用户人数
56 | var onlineCount = 0;
57 |
58 | io.on('connection', function(socket) {
59 | // 监听客户端的登陆
60 | socket.on('login', function(obj) {
61 | // 用户id设为socketid
62 | socket.id = obj.uid;
63 |
64 | // 如果没有这个用户,那么在线人数+1,将其添加进在线用户
65 | if (!onlineUsers[obj.uid]) {
66 | onlineUsers[obj.uid] = obj.username;
67 | onlineCount++;
68 | }
69 |
70 | // 向客户端发送登陆事件,同时发送在线用户、在线人数以及登陆用户
71 | io.emit('login', { onlineUsers: onlineUsers, onlineCount: onlineCount, user: obj });
72 | logger.info({ socketId: socket.id, ip: socket.request.connection.remoteAddress, user: obj.username, event: 'in', message: obj.username + '加入了群聊' });
73 | console.log(obj.username + '加入了群聊');
74 | });
75 |
76 | // 监听客户端的断开连接
77 | socket.on('disconnect', function() {
78 | // 如果有这个用户
79 | if (onlineUsers[socket.id]) {
80 | var obj = { uid: socket.id, username: onlineUsers[socket.id] };
81 |
82 | // 删掉这个用户,在线人数-1
83 | delete onlineUsers[socket.id];
84 | onlineCount--;
85 |
86 | // 向客户端发送登出事件,同时发送在线用户、在线人数以及登出用户
87 | io.emit('logout', { onlineUsers: onlineUsers, onlineCount: onlineCount, user: obj });
88 | logger.info({ socketId: socket.id, ip: socket.request.connection.remoteAddress, user: obj.username, event: 'out', message: obj.username + '退出了群聊' });
89 | console.log(obj.username + '退出了群聊');
90 | }
91 | });
92 |
93 | // 监听客户端发送的信息
94 | socket.on('message', function(obj) {
95 | io.emit('message', obj);
96 | logger.info({ socketId: socket.id, ip: socket.request.connection.remoteAddress, user: obj.username, event: 'chat', message: obj.username + '说:' + obj.message });
97 | console.log(obj.username + '说:' + obj.message);
98 | });
99 | });
100 |
101 | server.listen(3300, function(err) {
102 | if (process.env.NODE_ENV !== 'production') {
103 | openBrowsers('http://localhost:3300');
104 | }
105 | console.log('Listening at *:3300');
106 | });
107 |
--------------------------------------------------------------------------------
/src/components/ChatInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | interface MyProps {
4 | socket: any;
5 | myId: string;
6 | myName: string;
7 | }
8 | interface MyState {
9 | socket: any;
10 | message: string;
11 | myId: string;
12 | myName: string;
13 | }
14 | export default class ChatInput extends Component {
15 | public constructor(props: MyProps) {
16 | super(props);
17 | this.state = {
18 | socket: props.socket,
19 | message: '',
20 | myId: props.myId,
21 | myName: props.myName
22 | };
23 | }
24 |
25 | // 监控input变化
26 | public handleChange(e: React.ChangeEvent) {
27 | this.setState({ message: e.target.value });
28 | }
29 |
30 | // 点击提交或按回车
31 |
32 | public handleClick(e: MouseEvent) {
33 | e.preventDefault();
34 | this.sendMessage();
35 | }
36 |
37 | public handleKeyPress(e: KeyboardEvent): boolean {
38 | if (e.key === 'Enter') {
39 | this.sendMessage();
40 | }
41 | return false;
42 | }
43 |
44 | // 发送聊天信息
45 | public sendMessage(): boolean {
46 | const message = this.state.message;
47 | const socket = this.state.socket;
48 | if (message) {
49 | const obj = {
50 | uid: this.state.myId,
51 | username: this.state.myName,
52 | message: message
53 | };
54 | socket.emit('message', obj);
55 | this.setState({ message: '' });
56 | }
57 | return false;
58 | }
59 | public render(): JSX.Element {
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 |
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/ChatRoom.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import Messages from './Messages';
3 | import ChatInput from './ChatInput';
4 | import { Context } from '../context';
5 |
6 | // 生成消息id
7 | const generateMsgId = () => {
8 | return String(new Date().getTime()) + Math.floor(Math.random() * 899 + 100);
9 | };
10 |
11 | // 时间格式
12 | const generateTime = () => {
13 | const hour = new Date().getHours();
14 | const minute = new Date().getMinutes();
15 | const hourText = hour === 0 ? '00' : String(hour);
16 | const minuteText = minute < 10 ? '0' + minute : String(minute);
17 | return hourText + ':' + minuteText;
18 | };
19 |
20 | const ChatRoom = (props) => {
21 | const { state, dispatch } = useContext(Context);
22 | const [init, setInit] = useState(false);
23 | // 更新系统消息
24 | const updateSysMsg = (o, action) => {
25 | const newMsg = { type: 'system', username: o.user.username, uid: o.user.uid, action: action, msgId: generateMsgId(), time: generateTime() };
26 | dispatch({
27 | type: 'UPDATE_SYSTEM_MESSAGE',
28 | payload: {
29 | onlineCount: o.onlineCount,
30 | onlineUsers: o.onlineUsers,
31 | message: newMsg
32 | }
33 | });
34 | };
35 |
36 | // 发送新消息
37 | const updateMsg = (obj) => {
38 | const newMsg = { type: 'chat', username: obj.username, uid: obj.uid, action: obj.message, msgId: generateMsgId(), time: generateTime() };
39 | dispatch({
40 | type: 'UPDATE_USER_MESSAGE',
41 | payload: {
42 | message: newMsg
43 | }
44 | });
45 | };
46 | // 监听消息发送
47 | const ready = () => {
48 | const { socket } = props;
49 | setInit(true);
50 | socket.on('login', (o) => {
51 | updateSysMsg(o, 'login');
52 | });
53 | socket.on('logout', (o) => {
54 | updateSysMsg(o, 'logout');
55 | });
56 | socket.on('message', (obj) => {
57 | updateMsg(obj);
58 | });
59 | };
60 | if (!init) {
61 | ready();
62 | }
63 | const renderUserList = () => {
64 | const users = state.onlineUsers;
65 | let userhtml = '';
66 | let separator = '';
67 | for (const key in users) {
68 | if (users.key) {
69 | userhtml += separator + users[key];
70 | separator = '、';
71 | }
72 | }
73 | return userhtml;
74 | };
75 | return (
76 |
77 |
78 |
79 |
鱼头的聊天室 | {props.username}
80 |
81 |
82 |
83 |
84 |
85 |
86 | 在线人数: {state.onlineCount}, 在线列表: {renderUserList()}
87 |
88 |
89 |
90 |
91 |
92 |
93 | );
94 | };
95 | export default ChatRoom;
96 |
--------------------------------------------------------------------------------
/src/components/Messages.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useRef } from 'react';
2 | import { Context } from '../context';
3 |
4 | const Message = (props) => {
5 | if (props.msgType === 'system') {
6 | return (
7 |
8 | {props.msgUser} {props.action === 'login' ? '进入了聊天室' : '离开了聊天室'} {props.time}
9 |
10 | );
11 | } else {
12 | return (
13 |
14 |
15 | {props.msgUser} {props.time}
16 |
17 |
{props.action}
18 |
19 | );
20 | }
21 | };
22 |
23 | const Messages = (props) => {
24 | const messageList = useRef(null);
25 | // 使用context中的状态,而不是props传值
26 | const { state } = useContext(Context);
27 |
28 | // 使用useFffect取代componentDidUpdate
29 | useEffect(() => {
30 | window.scrollTo(0, messageList.current.clientHeight + 50);
31 | });
32 | const { uid, messages } = state;
33 | return (
34 |
35 | {messages.map((message) => (
36 |
37 | ))}
38 |
39 | );
40 | };
41 |
42 | export default Messages;
43 |
--------------------------------------------------------------------------------
/src/components/RoomStatus.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const RoomStatus = (props) => {
4 | return (
5 |
6 | 在线人数: {props.onlineCount}, 在线列表: {props.userhtml}
7 |
8 | );
9 | };
10 | export default RoomStatus;
11 |
--------------------------------------------------------------------------------
/src/container/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import ChatRoom from '../components/ChatRoom';
3 | import '../style/index.scss';
4 | import { Context } from '../context/index';
5 |
6 | const userState = (username) => {
7 | const [user, setUsername] = useState(username);
8 | return [user, setUsername];
9 | };
10 |
11 | const generateUid = () => {
12 | return String(new Date().getTime()) + Math.floor(Math.random() * 999 + 1);
13 | };
14 |
15 | const App = (props) => {
16 | // 获取context中的数据
17 | const { state, dispatch } = useContext(Context);
18 | // 输入输出用户名
19 | const [user, setUsername] = userState(null);
20 | const handleLogin = () => {
21 | const uid = generateUid();
22 | const username = user ? user : `游客${uid}`;
23 | dispatch({ type: 'login', payload: { uid, username } });
24 | state.socket.emit('login', { uid, username });
25 | };
26 | const handleKeyPress = (e) => {
27 | if (e.key === 'Enter') {
28 | handleLogin();
29 | }
30 | return false;
31 | };
32 | return (
33 |
34 | {state.uid ? (
35 | // 已登录
36 |
37 | ) : (
38 | // 登录界面
39 |
40 |
登 陆
41 |
42 | setUsername(e.target.value)} onKeyPress={handleKeyPress} />
43 |
44 |
45 |
48 |
49 |
50 | )}
51 |
52 | );
53 | };
54 | export default App;
55 |
--------------------------------------------------------------------------------
/src/context/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useReducer } from 'react';
2 | import io from 'socket.io-client';
3 |
4 | const Context = createContext(null);
5 |
6 | interface StateType {
7 | username: string;
8 | uid: string;
9 | socket: any;
10 | messages: [];
11 | onlineUsers: { [key: string]: string };
12 | onlineCount: number;
13 | userhtml: string;
14 | }
15 |
16 | const initValue: StateType = {
17 | username: '',
18 | uid: '',
19 | socket: io(),
20 | messages: [],
21 | onlineUsers: {},
22 | onlineCount: 0,
23 | userhtml: ''
24 | };
25 | interface Login {
26 | uid: string;
27 | username: string;
28 | }
29 |
30 | const login = (info: Login): object => {
31 | return info;
32 | };
33 |
34 | interface SystemMessage {
35 | onlineCount: number;
36 | onlineUsers: object;
37 | message: Message;
38 | }
39 |
40 | const systemMessage = (sysMsg: SystemMessage, state): object => {
41 | return {
42 | messages: state.messages.concat(sysMsg.message),
43 | onlineUsers: sysMsg.onlineUsers,
44 | onlineCount: sysMsg.onlineCount
45 | };
46 | };
47 |
48 | interface Message {
49 | type: number;
50 | username: string;
51 | uid: string;
52 | action: string;
53 | msgId: string;
54 | time: string;
55 | }
56 | interface UserMessage {
57 | message: Message;
58 | }
59 | const userMessage = (usrMsg: UserMessage, state): object => {
60 | return {
61 | messages: state.messages.concat(usrMsg.message)
62 | };
63 | };
64 | interface Payload extends UserMessage, SystemMessage, Login {}
65 | interface ActionType {
66 | type: string;
67 | payload: Payload;
68 | }
69 |
70 | const reducer = (state: StateType, action: ActionType): StateType => {
71 | // console.log(state, action);
72 | switch (action.type) {
73 | case 'login':
74 | return { ...state, ...login(action.payload) };
75 | case 'UPDATE_SYSTEM_MESSAGE':
76 | return { ...state, ...systemMessage(action.payload, state) };
77 | case 'UPDATE_USER_MESSAGE':
78 | return { ...state, ...userMessage(action.payload, state) };
79 | default:
80 | return state;
81 | }
82 | };
83 |
84 | const ContextProvider = (props) => {
85 | const [state, dispatch] = useReducer(reducer, initValue);
86 | return {props.children};
87 | };
88 |
89 | const ContextConsumer = Context.Consumer;
90 |
91 | export { Context, ContextProvider, ContextConsumer };
92 |
--------------------------------------------------------------------------------
/src/imgs/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/src/imgs/bg.jpg
--------------------------------------------------------------------------------
/src/imgs/whaletail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/src/imgs/whaletail.jpg
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './container/App';
4 | import { ContextProvider } from './context/index';
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById('app')
10 | );
11 |
--------------------------------------------------------------------------------
/src/style/index.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: '';
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 | $breakpoint-tablet: 768px;
9 | * {
10 | margin: 0;
11 | padding: 0;
12 | border: none;
13 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
14 | box-sizing: border-box;
15 | }
16 | body {
17 | background: url('../imgs/whaletail.jpg') center center;
18 | background-size: cover;
19 | background-attachment: fixed;
20 | font-size: 15px;
21 | }
22 | @media (max-width: $breakpoint-tablet) {
23 | body {
24 | background: url('../imgs/bg.jpg') center center;
25 | background-size: cover;
26 | background-attachment: fixed;
27 | }
28 | }
29 | .login-box {
30 | position: absolute;
31 | display: block;
32 | top: 50%;
33 | left: 50%;
34 | width: 100%;
35 | text-align: center;
36 | height: 200px;
37 | width: 300px;
38 | margin-left: -150px;
39 | margin-top: -100px;
40 | background: rgba(255, 255, 255, 0.8);
41 | border-radius: 10px;
42 | padding: 20px;
43 | color: rgb(51, 51, 51);
44 | input {
45 | height: 30px;
46 | font-size: 12px;
47 | border: none;
48 | border-radius: 2px;
49 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08);
50 | transition: box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
51 | margin-top: 30px;
52 | padding: 10px;
53 | outline: none;
54 | &:hover,
55 | &:focus {
56 | box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08);
57 | }
58 | }
59 | button {
60 | border-radius: 2px;
61 | color: #757575;
62 | cursor: default;
63 | font-family: arial, sans-serif;
64 | font-size: 13px;
65 | font-weight: bold;
66 | margin: 11px 4px;
67 | min-width: 54px;
68 | padding: 0 16px;
69 | text-align: center;
70 | height: 27px;
71 | line-height: 18px;
72 | &:hover {
73 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f8f8f8), to(#f1f1f1));
74 | background-image: -webkit-linear-gradient(top, #f8f8f8, #f1f1f1);
75 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
76 | background-color: #f8f8f8;
77 | background-image: linear-gradient(top, #f8f8f8, #f1f1f1);
78 | background-image: -o-linear-gradient(top, #f8f8f8, #f1f1f1);
79 | border: 1px solid #c6c6c6;
80 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
81 | color: #222;
82 | }
83 | }
84 | }
85 | .chat-room {
86 | color: #fff;
87 | max-width: 1200px;
88 | margin: 0 auto;
89 | .welcome {
90 | @include clearfix;
91 | width: 100%;
92 | color: rgba(0, 0, 0, 0.870588);
93 | background: #fff;
94 | padding: 10px 0;
95 | position: fixed;
96 | left: 0;
97 | right: 0;
98 | top: 0;
99 | line-height: 24px;
100 | .room-action {
101 | max-width: 1200px;
102 | margin: 0 auto;
103 | }
104 | .room-name {
105 | float: left;
106 | margin-left: 10px;
107 | }
108 | }
109 | }
110 | .room-status {
111 | margin-top: 50px;
112 | text-align: center;
113 | font-size: 13px;
114 | }
115 | .messages {
116 | padding: 5px 10px 60px;
117 | font-size: 15px;
118 | .one-message.system-message {
119 | font-size: 13px;
120 | color: #e0e0e0;
121 | flex-direction: row;
122 | justify-content: center;
123 | align-items: baseline;
124 | }
125 | .one-message {
126 | padding-bottom: 10px;
127 | display: flex;
128 | flex-direction: column;
129 | justify-content: flex-start;
130 | align-items: flex-start;
131 | p {
132 | margin-bottom: 5px;
133 | }
134 | .time {
135 | color: #e0e0e0;
136 | }
137 | .message-content {
138 | color: rgba(0, 0, 0, 0.870588);
139 | padding: 5px 10px;
140 | margin: 0;
141 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.117647);
142 | word-break: break-all;
143 | border-radius: 10px;
144 | display: inline-block;
145 | background-color: #f1f1f1;
146 | }
147 | }
148 | .one-message.me {
149 | flex-direction: column;
150 | justify-content: flex-start;
151 | align-items: flex-end;
152 | .message-content {
153 | background: #a2e563;
154 | }
155 | }
156 | }
157 | .bottom-area {
158 | position: fixed;
159 | bottom: 0;
160 | left: 0;
161 | right: 0;
162 | width: 100%;
163 | background: #fff;
164 | padding: 10px 0;
165 | font-size: 14px;
166 | .input-box {
167 | display: flex;
168 | max-width: 1200px;
169 | margin: 0 auto;
170 | .input {
171 | flex: 1;
172 | margin: 0 10px;
173 | input {
174 | width: 100%;
175 | outline: none;
176 | color: rgba(0, 0, 0, 0.870588);
177 | font-size: 100%;
178 | line-height: 24px;
179 | border-bottom-width: 2px;
180 | transition: border-color 0.2s ease;
181 | -webkit-transition: border-color 0.2s ease;
182 | background: 0;
183 | border: 0;
184 | border-bottom: 2px solid rgb(224, 224, 224);
185 | }
186 | }
187 | }
188 | }
189 |
190 | .button {
191 | float: right;
192 | margin-right: 10px;
193 | }
194 | button {
195 | background-color: rgb(255, 255, 255);
196 | transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
197 | box-sizing: border-box;
198 | font-family: Roboto, sans-serif;
199 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
200 | box-shadow: rgba(0, 0, 0, 0.117647) 0px 1px 6px, rgba(0, 0, 0, 0.117647) 0px 1px 4px;
201 | border-radius: 2px;
202 | display: inline-block;
203 | padding: 3px 15px;
204 | font-size: 13px;
205 | letter-spacing: 0px;
206 | font-weight: 500;
207 | margin: 0px;
208 | color: rgba(0, 0, 0, 0.870588);
209 | min-width: 65px;
210 | outline: 0;
211 | }
212 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./built",
4 | "sourceMap": true,
5 | "allowJs": true,
6 | "module": "commonjs",
7 | "esModuleInterop": true,
8 | "jsx": "react",
9 | "target": "es5"
10 | },
11 | "include": ["./src/**/*"]
12 | }
13 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const webpack = require('webpack');
4 |
5 | module.exports = {
6 | mode: 'development',
7 | entry: ['./src/index', 'webpack-hot-middleware/client?reload=true'],
8 | devtool: 'source-map',
9 | resolve: {
10 | extensions: ['.ts', '.js', '.tsx']
11 | },
12 | output: {
13 | path: path.join(__dirname),
14 | filename: 'bundle.js',
15 | publicPath: '/'
16 | },
17 | plugins: [
18 | new HtmlWebpackPlugin({
19 | inject: true,
20 | template: 'index.html'
21 | }),
22 | new webpack.HotModuleReplacementPlugin(),
23 | // Use NoErrorsPlugin for webpack 1.x
24 | new webpack.NoEmitOnErrorsPlugin()
25 | ],
26 | module: {
27 | rules: [
28 | { test: /\.tsx?$/, loader: 'awesome-typescript-loader' },
29 | {
30 | test: /\.js$/,
31 | exclude: /node_modules/,
32 | use: ['babel-loader']
33 | },
34 | {
35 | test: /\.(png|jpg)$/,
36 | use: [
37 | {
38 | loader: 'url-loader',
39 | options: {
40 | limit: 8192
41 | }
42 | }
43 | ]
44 | },
45 | {
46 | test: /\.scss$/,
47 | use: ['style-loader', 'css-loader', 'sass-loader']
48 | }
49 | ]
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | entry: ['./src/index'],
7 | devtool: false,
8 | output: {
9 | path: path.join(__dirname, 'dist'),
10 | filename: 'bundle.[hash].js',
11 | publicPath: '/dist/'
12 | },
13 | resolve: {
14 | extensions: ['.ts', '.js', '.tsx']
15 | },
16 | plugins: [
17 | new HtmlWebpackPlugin({
18 | inject: true,
19 | template: 'index.html'
20 | })
21 | ],
22 | module: {
23 | rules: [
24 | { test: /\.tsx?$/, loader: 'awesome-typescript-loader' },
25 | {
26 | test: /\.js$/,
27 | exclude: /node_modules/,
28 | use: ['babel-loader']
29 | },
30 | {
31 | test: /\.(png|jpg)$/,
32 | use: [
33 | {
34 | loader: 'url-loader',
35 | options: {
36 | limit: 8192
37 | }
38 | }
39 | ]
40 | },
41 | {
42 | test: /\.scss$/,
43 | use: ['style-loader', 'css-loader', 'sass-loader']
44 | }
45 | ]
46 | }
47 | };
48 |
--------------------------------------------------------------------------------