diff --git a/README.md b/README.md
index 0a57b59..9ee9941 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,64 @@
-该仓库是go-chat的前端界面。
-后端是使用go开发的,基于WebSocket的聊天程序。
-后端仓库地址:
-https://github.com/kone-net/go-chat
+## go-chat
+使用Go基于WebSocket的通讯聊天软件。
+### 功能列表:
+* 登录注册
+* 修改头像
+* 群聊天
+* 群好友列表
+* 单人聊天
+* 添加好友
+* 添加群组
+* 文本消息
+* 剪切板图片
+* 图片消息
+* 文件发送
+* 语音消息
+* 视频消息
+* 屏幕共享(基于图片)
+* 视频通话(基于webrtc的p2p视频通话)
+
+## 后端
+[代码仓库](https://github.com/kone-net/go-chat)
+go中协程是非常轻量级的。在每个client接入的时候,为每一个client开启一个协程,能够在单机实现更大的并发。同时go的channel,可以非常完美的解耦client接入和消息的转发等操作。
+
+通过go-chat,可以掌握channel的和Select的配合使用,ORM框架的使用,web框架Gin的使用,配置管理,日志操作,还包括proto buffer协议的使用,等一些列项目中常用的技术。
+
+
+### 后端技术和框架
+* web框架Gin
+* 长连接WebSocket
+* 日志框架Uber的zap
+* 配置管理viper
+* ORM框架gorm
+* 通讯协议Google的proto buffer
+* makefile 的编写
+* 数据库MySQL
+* 图片文件二进制操作
+
+## 前端
+基于react,UI和基本组件是使用ant design。可以很方便搭建前端界面。
+
+界面选择单页框架可以更加方便写聊天界面,比如像消息提醒,可以在一个界面接受到消息进行提醒,不会因为换页面或者查看其他内容影响消息接受。
+[前端代码仓库](https://github.com/kone-net/go-chat-web):
+https://github.com/kone-net/go-chat-web
+
+
+### 前端技术和框架
+* React
+* Redux状态管理
+* AntDesign
+* proto buffer的使用
+* WebSocket
+* 剪切板的文件读取和操作
+* 聊天框发送文字显示底部
+* FileReader对文件操作
+* ArrayBuffer,Blob,Uint8Array之间的转换
+* 获取摄像头视频(mediaDevices)
+* 获取麦克风音频(Recorder)
+* 获取屏幕共享(mediaDevices)
+* WebRTC的p2p视频通话
+
+
+### 截图
+
diff --git a/package-lock.json b/package-lock.json
index ce0c6e4..bee9ef8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,8 +17,10 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-infinite-scroll-component": "^6.1.0",
+ "react-redux": "^7.2.6",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
+ "redux-thunk": "^2.4.0",
"socket.io-client": "^4.3.2",
"web-vitals": "^1.0.1"
}
@@ -3095,6 +3097,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/download/@types/hoist-non-react-statics-3.3.1.tgz?cache=0&sync_timestamp=1637265727153&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fhoist-non-react-statics%2Fdownload%2F%40types%2Fhoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha1-ESSq/lEYy1kZd66xzqrtEHDrA58=",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/html-minifier-terser": {
"version": "5.1.2",
"resolved": "https://registry.nlark.com/@types/html-minifier-terser/download/@types/html-minifier-terser-5.1.2.tgz?cache=0&sync_timestamp=1631043806613&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fhtml-minifier-terser%2Fdownload%2F%40types%2Fhtml-minifier-terser-5.1.2.tgz",
@@ -3273,12 +3284,39 @@
"integrity": "sha1-4TAwSNU4lWPhMPW92J03qZrLdes=",
"license": "MIT"
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.4",
+ "resolved": "https://registry.npmmirror.com/@types/prop-types/download/@types/prop-types-15.7.4.tgz",
+ "integrity": "sha1-/PcgXCXf95Xuea8eMNosl5CAjxE="
+ },
"node_modules/@types/q": {
"version": "1.5.5",
"resolved": "https://registry.nlark.com/@types/q/download/@types/q-1.5.5.tgz",
"integrity": "sha1-daKo59irSyMEFFBdkjNdHctTpt8=",
"license": "MIT"
},
+ "node_modules/@types/react": {
+ "version": "17.0.36",
+ "resolved": "https://registry.npmmirror.com/@types/react/download/@types/react-17.0.36.tgz?cache=0&sync_timestamp=1637514386557&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Freact%2Fdownload%2F%40types%2Freact-17.0.36.tgz",
+ "integrity": "sha512-CUFUp01OdfbpN/76v4koqgcpcRGT3sYOq3U3N6q0ZVGcyeP40NUdVU+EWe3hs34RNaTefiYyBzOpxBBidCc5zw==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-redux": {
+ "version": "7.1.20",
+ "resolved": "https://registry.npmmirror.com/@types/react-redux/download/@types/react-redux-7.1.20.tgz",
+ "integrity": "sha1-QvDmGrq7Yh4SxmyW3alMWEI7198=",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
+ }
+ },
"node_modules/@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.nlark.com/@types/resolve/download/@types/resolve-0.0.8.tgz?cache=0&sync_timestamp=1629709391127&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fresolve%2Fdownload%2F%40types%2Fresolve-0.0.8.tgz",
@@ -3288,6 +3326,11 @@
"@types/node": "*"
}
},
+ "node_modules/@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmmirror.com/@types/scheduler/download/@types/scheduler-0.16.2.tgz?cache=0&sync_timestamp=1637270013832&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fscheduler%2Fdownload%2F%40types%2Fscheduler-0.16.2.tgz",
+ "integrity": "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="
+ },
"node_modules/@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.nlark.com/@types/source-list-map/download/@types/source-list-map-0.1.2.tgz",
@@ -7544,6 +7587,11 @@
"integrity": "sha1-nxJ29bK0Y/IRTT8sdSUK+MGjb0o=",
"license": "MIT"
},
+ "node_modules/csstype": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmmirror.com/csstype/download/csstype-3.0.10.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcsstype%2Fdownload%2Fcsstype-3.0.10.tgz",
+ "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
+ },
"node_modules/cyclist": {
"version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/cyclist/download/cyclist-1.0.1.tgz",
@@ -16966,6 +17014,36 @@
"integrity": "sha1-eJcppNw23imZ3BVt1sHZwYzqVqQ=",
"license": "MIT"
},
+ "node_modules/react-redux": {
+ "version": "7.2.6",
+ "resolved": "https://registry.npmmirror.com/react-redux/download/react-redux-7.2.6.tgz?cache=0&sync_timestamp=1637424929776&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-redux%2Fdownload%2Freact-redux-7.2.6.tgz",
+ "integrity": "sha1-SWM6JP5VK1+cr1j+uKE4k23f6ao=",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.15.4",
+ "@types/react-redux": "^7.1.20",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.3 || ^17"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-redux/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmmirror.com/react-is/download/react-is-17.0.2.tgz?cache=0&sync_timestamp=1637338596901&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-17.0.2.tgz",
+ "integrity": "sha1-5pHUqOnHiTZWVVOas3J2Kw77VPA="
+ },
"node_modules/react-refresh": {
"version": "0.8.3",
"resolved": "https://registry.npmmirror.com/react-refresh/download/react-refresh-0.8.3.tgz",
@@ -17583,6 +17661,24 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/redux/download/redux-4.1.2.tgz",
+ "integrity": "sha1-FA81Qm2Zu0cpr3YK/PeeqqxAcQQ=",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/redux-thunk": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmmirror.com/redux-thunk/download/redux-thunk-2.4.0.tgz?cache=0&sync_timestamp=1635214108732&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fredux-thunk%2Fdownload%2Fredux-thunk-2.4.0.tgz",
+ "integrity": "sha1-rInh1rm9ue5JzmmmkHG+QbvYLWc=",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^4"
+ }
+ },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.nlark.com/regenerate/download/regenerate-1.4.2.tgz",
@@ -24082,6 +24178,15 @@
"@types/node": "*"
}
},
+ "@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/download/@types/hoist-non-react-statics-3.3.1.tgz?cache=0&sync_timestamp=1637265727153&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fhoist-non-react-statics%2Fdownload%2F%40types%2Fhoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha1-ESSq/lEYy1kZd66xzqrtEHDrA58=",
+ "requires": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"@types/html-minifier-terser": {
"version": "5.1.2",
"resolved": "https://registry.nlark.com/@types/html-minifier-terser/download/@types/html-minifier-terser-5.1.2.tgz?cache=0&sync_timestamp=1631043806613&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fhtml-minifier-terser%2Fdownload%2F%40types%2Fhtml-minifier-terser-5.1.2.tgz",
@@ -24221,11 +24326,37 @@
"resolved": "https://registry.npmmirror.com/@types/prettier/download/@types/prettier-2.4.1.tgz",
"integrity": "sha1-4TAwSNU4lWPhMPW92J03qZrLdes="
},
+ "@types/prop-types": {
+ "version": "15.7.4",
+ "resolved": "https://registry.npmmirror.com/@types/prop-types/download/@types/prop-types-15.7.4.tgz",
+ "integrity": "sha1-/PcgXCXf95Xuea8eMNosl5CAjxE="
+ },
"@types/q": {
"version": "1.5.5",
"resolved": "https://registry.nlark.com/@types/q/download/@types/q-1.5.5.tgz",
"integrity": "sha1-daKo59irSyMEFFBdkjNdHctTpt8="
},
+ "@types/react": {
+ "version": "17.0.36",
+ "resolved": "https://registry.npmmirror.com/@types/react/download/@types/react-17.0.36.tgz?cache=0&sync_timestamp=1637514386557&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Freact%2Fdownload%2F%40types%2Freact-17.0.36.tgz",
+ "integrity": "sha512-CUFUp01OdfbpN/76v4koqgcpcRGT3sYOq3U3N6q0ZVGcyeP40NUdVU+EWe3hs34RNaTefiYyBzOpxBBidCc5zw==",
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-redux": {
+ "version": "7.1.20",
+ "resolved": "https://registry.npmmirror.com/@types/react-redux/download/@types/react-redux-7.1.20.tgz",
+ "integrity": "sha1-QvDmGrq7Yh4SxmyW3alMWEI7198=",
+ "requires": {
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
+ }
+ },
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.nlark.com/@types/resolve/download/@types/resolve-0.0.8.tgz?cache=0&sync_timestamp=1629709391127&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fresolve%2Fdownload%2F%40types%2Fresolve-0.0.8.tgz",
@@ -24234,6 +24365,11 @@
"@types/node": "*"
}
},
+ "@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmmirror.com/@types/scheduler/download/@types/scheduler-0.16.2.tgz?cache=0&sync_timestamp=1637270013832&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fscheduler%2Fdownload%2F%40types%2Fscheduler-0.16.2.tgz",
+ "integrity": "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="
+ },
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.nlark.com/@types/source-list-map/download/@types/source-list-map-0.1.2.tgz",
@@ -27329,6 +27465,11 @@
}
}
},
+ "csstype": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmmirror.com/csstype/download/csstype-3.0.10.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcsstype%2Fdownload%2Fcsstype-3.0.10.tgz",
+ "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
+ },
"cyclist": {
"version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/cyclist/download/cyclist-1.0.1.tgz",
@@ -33947,6 +34088,26 @@
"resolved": "https://registry.npmmirror.com/react-is/download/react-is-16.13.1.tgz?cache=0&sync_timestamp=1634573849595&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-16.13.1.tgz",
"integrity": "sha1-eJcppNw23imZ3BVt1sHZwYzqVqQ="
},
+ "react-redux": {
+ "version": "7.2.6",
+ "resolved": "https://registry.npmmirror.com/react-redux/download/react-redux-7.2.6.tgz?cache=0&sync_timestamp=1637424929776&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-redux%2Fdownload%2Freact-redux-7.2.6.tgz",
+ "integrity": "sha1-SWM6JP5VK1+cr1j+uKE4k23f6ao=",
+ "requires": {
+ "@babel/runtime": "^7.15.4",
+ "@types/react-redux": "^7.1.20",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmmirror.com/react-is/download/react-is-17.0.2.tgz?cache=0&sync_timestamp=1637338596901&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-17.0.2.tgz",
+ "integrity": "sha1-5pHUqOnHiTZWVVOas3J2Kw77VPA="
+ }
+ }
+ },
"react-refresh": {
"version": "0.8.3",
"resolved": "https://registry.npmmirror.com/react-refresh/download/react-refresh-0.8.3.tgz",
@@ -34392,6 +34553,20 @@
"strip-indent": "^3.0.0"
}
},
+ "redux": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/redux/download/redux-4.1.2.tgz",
+ "integrity": "sha1-FA81Qm2Zu0cpr3YK/PeeqqxAcQQ=",
+ "requires": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "redux-thunk": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmmirror.com/redux-thunk/download/redux-thunk-2.4.0.tgz?cache=0&sync_timestamp=1635214108732&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fredux-thunk%2Fdownload%2Fredux-thunk-2.4.0.tgz",
+ "integrity": "sha1-rInh1rm9ue5JzmmmkHG+QbvYLWc=",
+ "requires": {}
+ },
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.nlark.com/regenerate/download/regenerate-1.4.2.tgz",
diff --git a/package.json b/package.json
index dac76a2..48910a0 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,10 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-infinite-scroll-component": "^6.1.0",
+ "react-redux": "^7.2.6",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
+ "redux-thunk": "^2.4.0",
"socket.io-client": "^4.3.2",
"web-vitals": "^1.0.1"
},
diff --git a/public/screenshot/go-chat-panel.jpeg b/public/screenshot/go-chat-panel.jpeg
new file mode 100644
index 0000000..8948d2d
Binary files /dev/null and b/public/screenshot/go-chat-panel.jpeg differ
diff --git a/src/chat/Login.jsx b/src/chat/Login.jsx
index 7fa6b23..7b5cc4f 100755
--- a/src/chat/Login.jsx
+++ b/src/chat/Login.jsx
@@ -3,6 +3,7 @@ import {
Button,
Form,
Input,
+ Drawer,
message
} from 'antd';
import { axiosPostBody } from './util/Request';
@@ -12,6 +13,7 @@ class Login extends React.Component {
constructor(props) {
super(props)
this.state = {
+ registerDrawerVisible: false
}
}
@@ -37,6 +39,32 @@ class Login extends React.Component {
console.log('Failed:', errorInfo);
};
+ showRegister = () => {
+ this.setState({
+ registerDrawerVisible: true
+ })
+ }
+
+ registerDrawerOnClose = () => {
+ this.setState({
+ registerDrawerVisible: false
+ })
+ }
+
+ onRegister = (values) => {
+ let data = {
+ ...values
+ }
+
+ axiosPostBody(Params.REGISTER_URL, data)
+ .then(_response => {
+ message.success("注册成功!");
+ this.setState({
+ registerDrawerVisible: false
+ })
+ });
+ }
+
render() {
return (
@@ -51,7 +79,7 @@ class Login extends React.Component {
style={{ marginTop: 150 }}
>
@@ -59,7 +87,7 @@ class Login extends React.Component {
@@ -68,10 +96,65 @@ class Login extends React.Component {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/chat/Panel.jsx b/src/chat/Panel.jsx
index 44b0e7c..ef5f8e7 100755
--- a/src/chat/Panel.jsx
+++ b/src/chat/Panel.jsx
@@ -7,7 +7,7 @@ import {
Drawer,
Tag,
Popover,
- Tooltip
+ Tooltip,
} from 'antd';
import {
UserOutlined,
@@ -21,17 +21,22 @@ import {
PhoneOutlined,
VideoCameraOutlined,
UngroupOutlined,
- DesktopOutlined
+ DesktopOutlined,
+ FileOutlined
} from '@ant-design/icons';
import InfiniteScroll from 'react-infinite-scroll-component';
import moment from 'moment';
import { axiosGet, axiosPostBody } from './util/Request';
import * as Params from './common/param/Params'
+import UserInfo from './component/UserInfo'
import protobuf from './proto/proto'
import Recorder from 'js-audio-recorder';
+import { connect } from 'react-redux'
+import { actions } from './redux/module/userInfo'
-var socket = null
+var socket = null;
+var peer = null;
const { TextArea } = Input;
@@ -105,7 +110,6 @@ var heartCheck = {
}
}
-
class Panel extends React.Component {
constructor(props) {
super(props)
@@ -137,12 +141,13 @@ class Panel extends React.Component {
},
share: {
height: 540,
- width: 960
+ width: 750
},
currentScreen: {
height: 0,
width: 0
- }
+ },
+ rtcType: 'answer',
}
}
@@ -194,34 +199,54 @@ class Panel extends React.Component {
const messagePB = message.create(data)
socket.send(message.encode(messagePB).finish())
- // 将ArrayBuffer转换为base64进行展示
- const str = String.fromCharCode(...new Uint8Array(imgData));
- let base64String = `data:image/jpeg;base64,${window.btoa(str)}`;
-
- this.setState({
- comments: [
- ...this.state.comments,
- {
- author: localStorage.username,
- avatar: this.state.user.avatar,
- content: 
,
- datetime: moment().fromNow(),
- },
- ],
- })
+ this.appendImgToPanel(imgData)
})
}, false)
}
+ /**
+ * 本地上传后,将图片追加到聊天框
+ * @param {Arraybuffer类型图片}} imgData
+ */
+ appendImgToPanel(imgData) {
+ // 将ArrayBuffer转换为base64进行展示
+ var binary = '';
+ var bytes = new Uint8Array(imgData);
+ var len = bytes.byteLength;
+ for (var i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ let base64String = `data:image/jpeg;base64,${window.btoa(binary)}`;
+
+ this.setState({
+ comments: [
+ ...this.state.comments,
+ {
+ author: localStorage.username,
+ avatar: this.state.user.avatar,
+ content: 
,
+ datetime: moment().fromNow(),
+ },
+ ],
+ }, () => {
+ setTimeout(this.scrollToBottom(), 3000)
+ })
+ }
+
/**
* 获取用户详情
*/
fetchUserDetails = () => {
axiosGet(Params.USER_URL + this.state.fromUser)
.then(response => {
+ let user = {
+ ...response.data,
+ avatar: Params.HOST + "/file/" + response.data.avatar
+ }
+ this.props.setUser(user)
this.setState({
- user: response.data,
+ user: user,
})
});
}
@@ -230,39 +255,15 @@ class Panel extends React.Component {
* websocket连接
*/
connection = () => {
+ console.log("to connect...")
+ peer = new RTCPeerConnection();
var image = document.getElementById('receiver');
-
- let arr = []
- var video = document.getElementById('preview1')
- let i = 0
-
- // let flag = false
- // let sourceBuffer
- // let mediaSource = new MediaSource()
- // var video = document.getElementById('preview1')
- // video.src = URL.createObjectURL(mediaSource)
- // mediaSource.addEventListener('sourceopen', sourceOpen);
-
- // function sourceOpen(e) {
- // console.log("sourceOpen", "mediaSource ready state: ", mediaSource.readyState)
- // // var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
- // var mime = 'video/webm; codecs="opus, vp9"';
- // // 新建一个 sourceBuffer
- // sourceBuffer = mediaSource.addSourceBuffer(mime);
-
- // sourceBuffer.addEventListener('updateend', function (_) {
- // console.log(mediaSource.readyState); // ended
- // // sourceBuffer.appendBuffer(arr)
- // });
- // }
-
-
- console.log("to connection")
socket = new WebSocket("ws://" + Params.IP_PORT + "/socket.io?user=" + this.props.match.params.user)
socket.onopen = () => {
heartCheck.start()
console.log("connected")
+ this.webrtcConnection()
}
socket.onmessage = (message) => {
heartCheck.start()
@@ -277,6 +278,7 @@ class Panel extends React.Component {
return;
}
+ // 视频图像
if (messagePB.contentType === 8) {
let currentScreen = {
width: this.state.video.width,
@@ -289,6 +291,7 @@ class Panel extends React.Component {
return;
}
+ // 屏幕共享
if (messagePB.contentType === 9) {
let currentScreen = {
width: this.state.share.width,
@@ -301,28 +304,9 @@ class Panel extends React.Component {
return;
}
- // 接受语音电话或者视频电话
- if (messagePB.contentType === 6 || messagePB.contentType === 7) {
- i++
- console.log(i)
- // arr.push(messagePB.file)
-
- if (i % 5 === 0) {
- // let fileReader = new FileReader();
- let recordedBlob = new Blob(arr, { type: "video/webm" }); //video/x-matroska;codecs=avc1,opus video/webm
- // fileReader.readAsDataURL(recordedBlob)
- // fileReader.onload = (e) => {
- // console.log(e.target.result)
- // video.src = e.target.result;
- // this.setState({
- // url: e.target.result
- // })
- // }
- console.log(recordedBlob)
- video.src = URL.createObjectURL(recordedBlob);
- }
- // sourceBuffer.appendBuffer(messagePB.file.buffer)
-
+ // 接受语音电话或者视频电话 webrtc
+ if (messagePB.type === "webrtc") {
+ this.dealWebRtcMessage(messagePB);
return;
}
@@ -331,18 +315,20 @@ class Panel extends React.Component {
avatar = messagePB.avatar
}
+ // 文件内容,录制的视频,语音内容
+ let content = this.getContentByType(messagePB.contentType, messagePB.url, messagePB.content)
this.setState({
comments: [
...this.state.comments,
{
author: messagePB.fromUsername,
avatar: avatar,
- content: {(messagePB.contentType === 2 || messagePB.contentType === 3) ?
: messagePB.content}
,
+ content: {content}
,
datetime: moment().fromNow(),
},
],
}, () => {
- this.scrollToBottom()
+ setTimeout(this.scrollToBottom(), 3000)
})
})
}
@@ -360,6 +346,98 @@ class Panel extends React.Component {
}
}
+ /**
+ * webrtc 绑定事件
+ */
+ webrtcConnection = () => {
+ /**
+ * 对等方收到ice信息后,通过调用 addIceCandidate 将接收的候选者信息传递给浏览器的ICE代理。
+ * @param {候选人信息} e
+ */
+ peer.onicecandidate = (e) => {
+ if (e.candidate) {
+ // rtcType参数默认是对端值为answer,如果是发起端,会将值设置为offer
+ let candidate = {
+ type: this.state.rtcType + '_ice',
+ iceCandidate: e.candidate
+ }
+
+ let data = {
+ fromUsername: localStorage.username,
+ from: this.state.fromUser,
+ to: this.state.toUser,
+ messageType: this.state.messageType,
+ content: JSON.stringify(candidate),
+ type: "webrtc",
+ }
+ let message = protobuf.lookup("protocol.Message")
+ const messagePB = message.create(data)
+ socket.send(message.encode(messagePB).finish())
+ }
+
+ };
+
+ /**
+ * 当连接成功后,从里面获取语音视频流
+ * @param {包含语音视频流} e
+ */
+ peer.ontrack = (e) => {
+ if (e && e.streams) {
+ let remoteVideo = document.getElementById("remoteVideo");
+ remoteVideo.srcObject = e.streams[0];
+ }
+ };
+ }
+
+ /**
+ * 处理webrtc消息,包括获取请求方的offer,回应answer等
+ * @param {消息内容}} messagePB
+ */
+ dealWebRtcMessage = (messagePB) => {
+ const { type, sdp, iceCandidate } = JSON.parse(messagePB.content);
+
+ if (type === "answer") {
+ const offerSdp = new RTCSessionDescription({ type, sdp });
+ peer.setRemoteDescription(offerSdp)
+ } else if (type === "answer_ice") {
+ peer.addIceCandidate(iceCandidate)
+ } else if (type === "offer_ice") {
+ peer.addIceCandidate(iceCandidate)
+ } else if (type === "offer") {
+ let preview = document.getElementById("preview1");
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ video: true,
+ }).then((stream) => {
+ preview.srcObject = stream;
+ stream.getTracks().forEach(track => {
+ peer.addTrack(track, stream);
+ });
+
+ // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行answer创建。不然不能获取到流,从而不能播放视频。
+ const offerSdp = new RTCSessionDescription({ type, sdp });
+ peer.setRemoteDescription(offerSdp)
+ .then(() => {
+ peer.createAnswer().then(answer => {
+ peer.setLocalDescription(answer)
+ let data = {
+ fromUsername: localStorage.username,
+ from: this.state.fromUser,
+ to: this.state.toUser,
+ messageType: this.state.messageType,
+ content: JSON.stringify(answer),
+ type: "webrtc",
+ }
+ let message = protobuf.lookup("protocol.Message")
+ const messagePB = message.create(data)
+ socket.send(message.encode(messagePB).finish())
+ })
+ });
+ });
+ }
+ }
+
/**
* 断开连接后重新连接
*/
@@ -397,7 +475,7 @@ class Panel extends React.Component {
username: users[index].username,
uuid: users[index].uuid,
messageType: 1,
- avatar: users[index].avatar,
+ avatar: Params.HOST + "/file/" + users[index].avatar,
}
data.push(d)
}
@@ -534,16 +612,22 @@ class Panel extends React.Component {
return
}
- axiosGet(Params.USER_URL + value)
+ let data = {
+ name: value
+ }
+ axiosGet(Params.USER_NAME_URL, data)
.then(response => {
- console.log(response)
- if (response.data.username === 0) {
- message.error("无此用户")
+ let data = response.data
+ if (data.user.username === "" && data.group.name === "") {
+ message.error("未查找到群或者用户")
return
}
let queryUser = {
- username: response.data.username,
- nickname: response.data.nickname,
+ username: data.user.username,
+ nickname: data.user.nickname,
+
+ groupUuid: data.group.uuid,
+ groupName: data.group.name,
}
this.setState({
hasUser: true,
@@ -573,6 +657,18 @@ class Panel extends React.Component {
});
};
+ joinGroup = () => {
+ // /group/join/:userUid/:groupUuid
+ axiosPostBody(Params.GROUP_JOIN_URL + this.state.fromUser + "/" + this.state.queryUser.groupUuid)
+ .then(_response => {
+ message.success("添加成功")
+ this.fetchUserList()
+ this.setState({
+ hasUser: false
+ });
+ });
+ }
+
handleCancel = () => {
this.setState({
hasUser: false
@@ -601,21 +697,11 @@ class Panel extends React.Component {
}
for (var i = 0; i < data.length; i++) {
let contentType = data[i].contentType
- let content = data[i].content;
-
- if (contentType === 2) {
-
- } else if (contentType === 3) {
- content =
- } else if (contentType === 4) {
- content =
- } else if (contentType === 5) {
- content =
- }
+ let content = this.getContentByType(contentType, data[i].url, data[i].content)
let comment = {
author: data[i].fromUsername,
- avatar: data[i].avatar,
+ avatar: Params.HOST + "/file/" + data[i].avatar,
content: {content}
,
datetime: moment(data[i].createAt).fromNow(),
}
@@ -626,17 +712,40 @@ class Panel extends React.Component {
comments: comments
}, () => {
this.scrollToBottom()
+ setTimeout(this.scrollToBottom(), 5000)
})
});
}
+ /**
+ * 根据文件类型渲染对应的标签,比如视频,图片等。
+ * @param {文件类型} type
+ * @param {文件地址} url
+ * @returns
+ */
+ getContentByType = (type, url, content) => {
+ if (type === 2) {
+ content =
+ } else if (type === 3) {
+ content =
+ } else if (type === 4) {
+ content =
+ } else if (type === 5) {
+ content =
+ }
+
+ return content;
+ }
+
/**
* 获取群聊信息,群成员列表
*/
chatDetails = () => {
axiosGet(Params.GROUP_USER_URL + this.state.toUser)
.then(response => {
- console.log(response)
+ if (null == response.data) {
+ return;
+ }
this.setState({
drawerVisible: true,
groupUsers: response.data
@@ -660,6 +769,17 @@ class Panel extends React.Component {
if (!files || !files[0]) {
return;
}
+ let fileName = files[0].name
+ if (null == fileName) {
+ message.error("文件无名称")
+ return
+ }
+ let index = fileName.lastIndexOf('.');
+ let fileSuffix = null;
+ if (index >= 0) {
+ fileSuffix = fileName.substring(index + 1);
+ }
+
let reader = new FileReader()
reader.onload = ((event) => {
@@ -676,15 +796,38 @@ class Panel extends React.Component {
messageType: this.state.messageType,
content: this.state.value,
contentType: 3,
+ fileSuffix: fileSuffix,
file: u8
}
const messagePB = message.create(data)
-
socket.send(message.encode(messagePB).finish())
+
+ if (["jpeg", "jpg", "png", "gif", "tif", "bmp", "dwg"].indexOf(fileSuffix) !== -1) {
+ this.appendImgToPanel(file)
+ } else {
+ this.appendFile()
+ }
+
})
reader.readAsArrayBuffer(files[0])
}
+ appendFile = () => {
+ this.setState({
+ comments: [
+ ...this.state.comments,
+ {
+ author: localStorage.username,
+ avatar: this.state.user.avatar,
+ content:
,
+ datetime: moment().fromNow(),
+ },
+ ],
+ }, () => {
+ this.scrollToBottom()
+ })
+ }
+
/**
* 开始录制音频
*/
@@ -854,7 +997,8 @@ class Panel extends React.Component {
let preview = document.getElementById("preview1");
this.setState({
- isRecord: true
+ isRecord: true,
+ rtcType: 'offer'
})
navigator.mediaDevices
@@ -863,65 +1007,35 @@ class Panel extends React.Component {
video: true,
}).then((stream) => {
preview.srcObject = stream;
+ stream.getTracks().forEach(track => {
+ peer.addTrack(track, stream);
+ });
- //传输视频流
- // this.recorder = new MediaRecorder(stream);
- // this.recorder.ondataavailable = (event) => {
- // let data = event.data;
- // let reader = new FileReader()
- // reader.readAsArrayBuffer(data)
-
- // reader.onload = ((e) => {
- // let fileData = e.target.result
-
- // // 上传文件必须将ArrayBuffer转换为Uint8Array
- // let data = {
- // fromUsername: localStorage.username,
- // from: this.state.fromUser,
- // to: this.state.toUser,
- // messageType: this.state.messageType,
- // content: this.state.value,
- // contentType: 6,
- // file: new Uint8Array(fileData)
- // }
- // let message = protobuf.lookup("protocol.Message")
- // const messagePB = message.create(data)
- // socket.send(message.encode(messagePB).finish())
- // })
- // };
- // this.recorder.start(1000);
+ // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行offer创建。不然不能获取到流,从而不能播放视频。
+ peer.createOffer()
+ .then(offer => {
+ peer.setLocalDescription(offer);
+ let data = {
+ fromUsername: localStorage.username,
+ from: this.state.fromUser,
+ to: this.state.toUser,
+ messageType: this.state.messageType,
+ content: JSON.stringify(offer),
+ type: "webrtc",
+ }
+ let message = protobuf.lookup("protocol.Message")
+ const messagePB = message.create(data)
+ socket.send(message.encode(messagePB).finish())
+ });
});
-
- var canvas = document.getElementById("canvas");
- var ctx = canvas.getContext('2d');
- this.interval = window.setInterval(() => {
- let width = this.state.video.width
- let height = this.state.video.height
- let currentScreen = {
- width: width,
- height: height
- }
- this.setState({
- currentScreen: currentScreen
- })
- ctx.drawImage(preview, 0, 0, width, height);
- let data = {
- fromUsername: localStorage.username,
- from: this.state.fromUser,
- to: this.state.toUser,
- messageType: this.state.messageType,
- content: canvas.toDataURL("image/jpeg", 0.5),
- contentType: 8,
- }
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
- }, 60);
+ this.setState({
+ mediaPanelDrawerVisible: true
+ })
}
/**
- * 停止视频电话
+ * 停止视频电话,屏幕共享
*/
stopVideoOnline = () => {
this.setState({
@@ -1024,7 +1138,6 @@ class Panel extends React.Component {
}
render() {
-
const { comments, submitting, value, toUser } = this.state;
return (
@@ -1032,9 +1145,7 @@ class Panel extends React.Component {
-
-
-
+
} size="large" type='link' disabled={this.state.menuType === 1} onClick={this.fetchUserList}>
@@ -1133,7 +1244,9 @@ class Panel extends React.Component {
shape="circle"
onClick={this.startVideoOnline}
style={{ marginRight: 10 }}
- icon={} disabled={toUser === ''}
+ icon={}
+ // disabled={toUser === ''}
+ disabled
/>
@@ -1189,9 +1302,14 @@ class Panel extends React.Component {
-
+
用户名:{this.state.queryUser.username}
昵称:{this.state.queryUser.nickname}
+
+
+
+ 群信息:{this.state.queryUser.groupName}
+
@@ -1202,7 +1320,7 @@ class Panel extends React.Component {
}
+ avatar={}
title={item.username}
description=""
/>
@@ -1211,9 +1329,20 @@ class Panel extends React.Component {
/>
-
-
-
+
+
+ }
+ />
+
+
+
+
+
+
>
@@ -1221,4 +1350,18 @@ class Panel extends React.Component {
}
}
-export default Panel;
\ No newline at end of file
+function mapStateToProps(state) {
+ return {
+ user: state.userInfoReducer.user,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setUser: (data) => dispatch(actions.setUser(data)),
+ }
+}
+
+Panel = connect(mapStateToProps, mapDispatchToProps)(Panel)
+
+export default Panel
\ No newline at end of file
diff --git a/src/chat/common/param/Params.jsx b/src/chat/common/param/Params.jsx
index 8e65b63..69faa77 100755
--- a/src/chat/common/param/Params.jsx
+++ b/src/chat/common/param/Params.jsx
@@ -1,12 +1,14 @@
export const API_VERSION = "/api/v1/";
const PROTOCOL = "http://"
-export const IP_PORT = "127.0.0.1:8888";
+export const IP_PORT = "localhost:8888";
//local
export const HOST = PROTOCOL + IP_PORT;
export const LOGIN_URL = HOST + '/user/login'
+export const REGISTER_URL = HOST + '/user/register'
export const USER_URL = HOST + '/user/'
+export const USER_NAME_URL = HOST + '/user/name'
export const USER_LIST_URL = HOST + '/user'
export const USER_FRIEND_URL = HOST + '/friend'
@@ -15,6 +17,9 @@ export const MESSAGE_URL = HOST + '/message'
export const GROUP_LIST_URL = HOST + '/group'
export const GROUP_USER_URL = HOST + '/group/user/'
+export const GROUP_JOIN_URL = HOST + '/group/join/'
+
+export const FILE_URL = HOST + '/file'
diff --git a/src/chat/component/UserInfo.jsx b/src/chat/component/UserInfo.jsx
new file mode 100644
index 0000000..4819714
--- /dev/null
+++ b/src/chat/component/UserInfo.jsx
@@ -0,0 +1,159 @@
+import React from 'react';
+import {
+ Avatar,
+ Button,
+ Dropdown,
+ Menu,
+ Modal,
+ Upload,
+ message
+} from 'antd';
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
+
+import { connect } from 'react-redux'
+import { actions } from '../redux/module/userInfo'
+import * as Params from '../common/param/Params'
+
+function getBase64(img, callback) {
+ const reader = new FileReader();
+ reader.addEventListener('load', () => callback(reader.result));
+ reader.readAsDataURL(img);
+}
+
+function beforeUpload(file) {
+ const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+ if (!isJpgOrPng) {
+ message.error('You can only upload JPG/PNG file!');
+ }
+ const isLt2M = file.size / 1024 / 1024 < 2;
+ if (!isLt2M) {
+ message.error('Image must smaller than 2MB!');
+ }
+ return isJpgOrPng && isLt2M;
+}
+
+class UserInfo extends React.Component {
+ constructor(props) {
+ super(props)
+ let user = {}
+ if (props.user) {
+ user = props.user
+ }
+ this.state = {
+ user: user,
+ isModalVisible: false,
+ loading: false,
+ imageUrl: ''
+ }
+ }
+
+ componentDidMount() {
+
+ }
+
+ modifyAvatar = () => {
+ this.setState({
+ isModalVisible: true
+ })
+ }
+
+ handleCancel = () => {
+ this.setState({
+ isModalVisible: false
+ })
+ }
+
+ loginout = () => {
+ this.props.history.push("/login")
+ }
+
+ handleChange = info => {
+ if (info.file.status === 'uploading') {
+ this.setState({ loading: true });
+ return;
+ }
+ if (info.file.status === 'done') {
+ let response = info.file.response
+ if (response.code !== 0) {
+ message.error(info.file.response.msg)
+ }
+
+ let user = {
+ ...this.props.user,
+ avatar: Params.HOST + "/file/" + info.file.response.data
+ }
+ this.props.setUser(user)
+ // Get this url from response in real world.
+ getBase64(info.file.originFileObj, imageUrl =>
+ this.setState({
+ imageUrl,
+ loading: false,
+ }),
+ );
+ }
+ };
+
+
+ render() {
+ const menu = (
+
+ );
+
+ const { loading, imageUrl } = this.state;
+ const uploadButton = (
+
+ {loading ?
:
}
+
Upload
+
+ );
+ return (
+ <>
+
+
+
+
+
+
+ {imageUrl ?
: uploadButton}
+
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ user: state.userInfoReducer.user,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setUser: (data) => dispatch(actions.setUser(data)),
+ }
+}
+
+UserInfo = connect(mapStateToProps, mapDispatchToProps)(UserInfo)
+
+export default UserInfo
\ No newline at end of file
diff --git a/src/chat/proto/message.proto b/src/chat/proto/message.proto
index e30e6fc..8fce81e 100644
--- a/src/chat/proto/message.proto
+++ b/src/chat/proto/message.proto
@@ -11,5 +11,6 @@ message Message {
string type = 7;
int32 messageType = 8;
string url = 9;
- bytes file = 10;
+ string fileSuffix = 10;
+ bytes file = 11;
}
\ No newline at end of file
diff --git a/src/chat/proto/proto.js b/src/chat/proto/proto.js
index 23864eb..f0f6967 100644
--- a/src/chat/proto/proto.js
+++ b/src/chat/proto/proto.js
@@ -1,5 +1,4 @@
/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/
-// "use strict";
var $protobuf = require("protobufjs/light");
@@ -45,9 +44,13 @@ var $root = ($protobuf.roots["default"] || ($protobuf.roots["default"] = new $pr
type: "string",
id: 9
},
+ fileSuffix: {
+ type: "string",
+ id: 10
+ },
file: {
type: "bytes",
- id: 10
+ id: 11
}
}
}
diff --git a/src/chat/redux/module/index.jsx b/src/chat/redux/module/index.jsx
new file mode 100644
index 0000000..1166c74
--- /dev/null
+++ b/src/chat/redux/module/index.jsx
@@ -0,0 +1,9 @@
+import { createStore, combineReducers, applyMiddleware } from 'redux'
+import thunk from 'redux-thunk'
+import userInfoReducer from './userInfo'
+
+const reducer = combineReducers({
+ userInfoReducer
+});
+
+export default createStore(reducer, applyMiddleware(thunk));
\ No newline at end of file
diff --git a/src/chat/redux/module/userInfo.jsx b/src/chat/redux/module/userInfo.jsx
new file mode 100644
index 0000000..2009354
--- /dev/null
+++ b/src/chat/redux/module/userInfo.jsx
@@ -0,0 +1,25 @@
+const initialState = {
+ user: {}
+}
+
+export const types = {
+ USER_SET: 'USER/SET',
+}
+
+export const actions = {
+ setUser: (user) => ({
+ type: types.USER_SET,
+ user: user
+ }),
+}
+
+const userInfoReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case types.USER_SET:
+ return { ...state, user: action.user }
+ default:
+ return state
+ }
+}
+
+export default userInfoReducer
diff --git a/src/index.js b/src/index.js
index 9716298..61f01f5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,15 +5,19 @@ import Login from './chat/Login';
import Panel from './chat/Panel';
import { Switch, Route, BrowserRouter } from 'react-router-dom';
import reportWebVitals from './reportWebVitals';
+import { Provider } from 'react-redux';
+import store from './chat/redux/module/index'
ReactDOM.render(
-
+
+
- ,
+
+ ,
document.getElementById('root')
);
diff --git a/yarn.lock b/yarn.lock
index 284519e..271571d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1239,7 +1239,7 @@
"core-js-pure" "^3.16.0"
"regenerator-runtime" "^0.13.4"
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"integrity" "sha1-/RfRa/34eObdAtGXU6OfqKjZyEo="
"resolved" "https://registry.nlark.com/@babel/runtime/download/@babel/runtime-7.15.4.tgz?cache=0&sync_timestamp=1630618914695&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.15.4.tgz"
"version" "7.15.4"
@@ -1957,6 +1957,14 @@
dependencies:
"@types/node" "*"
+"@types/hoist-non-react-statics@^3.3.0":
+ "integrity" "sha1-ESSq/lEYy1kZd66xzqrtEHDrA58="
+ "resolved" "https://registry.npmmirror.com/@types/hoist-non-react-statics/download/@types/hoist-non-react-statics-3.3.1.tgz?cache=0&sync_timestamp=1637265727153&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fhoist-non-react-statics%2Fdownload%2F%40types%2Fhoist-non-react-statics-3.3.1.tgz"
+ "version" "3.3.1"
+ dependencies:
+ "@types/react" "*"
+ "hoist-non-react-statics" "^3.3.0"
+
"@types/html-minifier-terser@^5.0.0":
"integrity" "sha1-aTsxatMj6pfu1rOO0aPMArFnK1c="
"resolved" "https://registry.nlark.com/@types/html-minifier-terser/download/@types/html-minifier-terser-5.1.2.tgz?cache=0&sync_timestamp=1631043806613&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fhtml-minifier-terser%2Fdownload%2F%40types%2Fhtml-minifier-terser-5.1.2.tgz"
@@ -2029,11 +2037,35 @@
"resolved" "https://registry.npmmirror.com/@types/prettier/download/@types/prettier-2.4.1.tgz"
"version" "2.4.1"
+"@types/prop-types@*":
+ "integrity" "sha1-/PcgXCXf95Xuea8eMNosl5CAjxE="
+ "resolved" "https://registry.npmmirror.com/@types/prop-types/download/@types/prop-types-15.7.4.tgz"
+ "version" "15.7.4"
+
"@types/q@^1.5.1":
"integrity" "sha1-daKo59irSyMEFFBdkjNdHctTpt8="
"resolved" "https://registry.nlark.com/@types/q/download/@types/q-1.5.5.tgz"
"version" "1.5.5"
+"@types/react-redux@^7.1.20":
+ "integrity" "sha1-QvDmGrq7Yh4SxmyW3alMWEI7198="
+ "resolved" "https://registry.npmmirror.com/@types/react-redux/download/@types/react-redux-7.1.20.tgz"
+ "version" "7.1.20"
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ "hoist-non-react-statics" "^3.3.0"
+ "redux" "^4.0.0"
+
+"@types/react@*":
+ "integrity" "sha512-CUFUp01OdfbpN/76v4koqgcpcRGT3sYOq3U3N6q0ZVGcyeP40NUdVU+EWe3hs34RNaTefiYyBzOpxBBidCc5zw=="
+ "resolved" "https://registry.npmmirror.com/@types/react/download/@types/react-17.0.36.tgz?cache=0&sync_timestamp=1637514386557&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Freact%2Fdownload%2F%40types%2Freact-17.0.36.tgz"
+ "version" "17.0.36"
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ "csstype" "^3.0.2"
+
"@types/resolve@0.0.8":
"integrity" "sha1-8mB00jjgJlnjI84aE9BB7uKA4ZQ="
"resolved" "https://registry.nlark.com/@types/resolve/download/@types/resolve-0.0.8.tgz?cache=0&sync_timestamp=1629709391127&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fresolve%2Fdownload%2F%40types%2Fresolve-0.0.8.tgz"
@@ -2041,6 +2073,11 @@
dependencies:
"@types/node" "*"
+"@types/scheduler@*":
+ "integrity" "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk="
+ "resolved" "https://registry.npmmirror.com/@types/scheduler/download/@types/scheduler-0.16.2.tgz?cache=0&sync_timestamp=1637270013832&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fscheduler%2Fdownload%2F%40types%2Fscheduler-0.16.2.tgz"
+ "version" "0.16.2"
+
"@types/source-list-map@*":
"integrity" "sha1-AHiDYGP/rxdBI0m7o2QIfgrALsk="
"resolved" "https://registry.nlark.com/@types/source-list-map/download/@types/source-list-map-0.1.2.tgz"
@@ -4250,6 +4287,11 @@
dependencies:
"cssom" "~0.3.6"
+"csstype@^3.0.2":
+ "integrity" "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
+ "resolved" "https://registry.npmmirror.com/csstype/download/csstype-3.0.10.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcsstype%2Fdownload%2Fcsstype-3.0.10.tgz"
+ "version" "3.0.10"
+
"cyclist@^1.0.1":
"integrity" "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk="
"resolved" "https://registry.npm.taobao.org/cyclist/download/cyclist-1.0.1.tgz"
@@ -5928,7 +5970,7 @@
"minimalistic-assert" "^1.0.0"
"minimalistic-crypto-utils" "^1.0.1"
-"hoist-non-react-statics@^3.1.0":
+"hoist-non-react-statics@^3.1.0", "hoist-non-react-statics@^3.3.0", "hoist-non-react-statics@^3.3.2":
"integrity" "sha1-7OCsr3HWLClpwuxZ/v9CpLGoW0U="
"resolved" "https://registry.npm.taobao.org/hoist-non-react-statics/download/hoist-non-react-statics-3.3.2.tgz"
"version" "3.3.2"
@@ -10065,6 +10107,23 @@
"resolved" "https://registry.npmmirror.com/react-is/download/react-is-17.0.2.tgz?cache=0&sync_timestamp=1634573849595&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-17.0.2.tgz"
"version" "17.0.2"
+"react-is@^17.0.2":
+ "integrity" "sha1-5pHUqOnHiTZWVVOas3J2Kw77VPA="
+ "resolved" "https://registry.npmmirror.com/react-is/download/react-is-17.0.2.tgz?cache=0&sync_timestamp=1637338596901&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-17.0.2.tgz"
+ "version" "17.0.2"
+
+"react-redux@^7.2.6":
+ "integrity" "sha1-SWM6JP5VK1+cr1j+uKE4k23f6ao="
+ "resolved" "https://registry.npmmirror.com/react-redux/download/react-redux-7.2.6.tgz?cache=0&sync_timestamp=1637424929776&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-redux%2Fdownload%2Freact-redux-7.2.6.tgz"
+ "version" "7.2.6"
+ dependencies:
+ "@babel/runtime" "^7.15.4"
+ "@types/react-redux" "^7.1.20"
+ "hoist-non-react-statics" "^3.3.2"
+ "loose-envify" "^1.4.0"
+ "prop-types" "^15.7.2"
+ "react-is" "^17.0.2"
+
"react-refresh@^0.8.3", "react-refresh@>=0.8.3 <0.10.0":
"integrity" "sha1-ch1GV2ctQAxePHXQY8SoX7LV1o8="
"resolved" "https://registry.npmmirror.com/react-refresh/download/react-refresh-0.8.3.tgz"
@@ -10165,7 +10224,7 @@
optionalDependencies:
"fsevents" "^2.1.3"
-"react@*", "react@^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "react@^17.0.2", "react@>= 16", "react@>= 16.9.0", "react@>=15", "react@>=16.0.0", "react@>=16.9.0", "react@17.0.2":
+"react@*", "react@^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "react@^16.8.3 || ^17", "react@^17.0.2", "react@>= 16", "react@>= 16.9.0", "react@>=15", "react@>=16.0.0", "react@>=16.9.0", "react@17.0.2":
"integrity" "sha1-0LXMUW0p6z7uOD91tihkz7aAADc="
"resolved" "https://registry.npmmirror.com/react/download/react-17.0.2.tgz?cache=0&sync_timestamp=1634573830599&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact%2Fdownload%2Freact-17.0.2.tgz"
"version" "17.0.2"
@@ -10254,6 +10313,18 @@
"indent-string" "^4.0.0"
"strip-indent" "^3.0.0"
+"redux-thunk@^2.4.0":
+ "integrity" "sha1-rInh1rm9ue5JzmmmkHG+QbvYLWc="
+ "resolved" "https://registry.npmmirror.com/redux-thunk/download/redux-thunk-2.4.0.tgz?cache=0&sync_timestamp=1635214108732&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fredux-thunk%2Fdownload%2Fredux-thunk-2.4.0.tgz"
+ "version" "2.4.0"
+
+"redux@^4", "redux@^4.0.0":
+ "integrity" "sha1-FA81Qm2Zu0cpr3YK/PeeqqxAcQQ="
+ "resolved" "https://registry.npmmirror.com/redux/download/redux-4.1.2.tgz"
+ "version" "4.1.2"
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+
"regenerate-unicode-properties@^9.0.0":
"integrity" "sha1-VNCccRXh9T3CMUqXSzLBw0Tv4yY="
"resolved" "https://registry.nlark.com/regenerate-unicode-properties/download/regenerate-unicode-properties-9.0.0.tgz?cache=0&sync_timestamp=1631617208210&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fregenerate-unicode-properties%2Fdownload%2Fregenerate-unicode-properties-9.0.0.tgz"