Browse Source

feat: video chat, register and join group

one-file
konenet 4 years ago
parent
commit
f92fca9fb1
  1. 67
      README.md
  2. 175
      package-lock.json
  3. 2
      package.json
  4. BIN
      public/screenshot/go-chat-panel.jpeg
  5. 89
      src/chat/Login.jsx
  6. 461
      src/chat/Panel.jsx
  7. 7
      src/chat/common/param/Params.jsx
  8. 159
      src/chat/component/UserInfo.jsx
  9. 3
      src/chat/proto/message.proto
  10. 7
      src/chat/proto/proto.js
  11. 9
      src/chat/redux/module/index.jsx
  12. 25
      src/chat/redux/module/userInfo.jsx
  13. 8
      src/index.js
  14. 77
      yarn.lock

67
README.md

@ -1,5 +1,64 @@ @@ -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视频通话
### 截图
![go-chat-panel](/public/screenshot/go-chat-panel.jpeg)

175
package-lock.json generated

@ -17,8 +17,10 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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",

2
package.json

@ -13,8 +13,10 @@ @@ -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"
},

BIN
public/screenshot/go-chat-panel.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

89
src/chat/Login.jsx

@ -3,6 +3,7 @@ import { @@ -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 { @@ -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 { @@ -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 { @@ -51,7 +79,7 @@ class Login extends React.Component {
style={{ marginTop: 150 }}
>
<Form.Item
label="Username"
label="用户名"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
@ -59,7 +87,7 @@ class Login extends React.Component { @@ -59,7 +87,7 @@ class Login extends React.Component {
</Form.Item>
<Form.Item
label="Password"
label="密码"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
@ -68,10 +96,65 @@ class Login extends React.Component { @@ -68,10 +96,65 @@ class Login extends React.Component {
<Form.Item wrapperCol={{ offset: 9, span: 6 }}>
<Button type="primary" htmlType="submit">
Submit
登录
</Button>
<Button onClick={this.showRegister} style={{ marginLeft: 40 }}>
注册
</Button>
</Form.Item>
</Form>
<Drawer width='500px' forceRender={true} title="注册" placement="right" onClose={this.registerDrawerOnClose} visible={this.state.registerDrawerVisible}>
<Form
name="basic"
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
onFinish={this.onRegister}
autoComplete="off"
style={{ marginTop: 150 }}
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '用户名!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '密码!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[{ required: true, message: '邮箱!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="昵称"
name="nickname"
rules={[{ required: true, message: '昵称!' }]}
>
<Input />
</Form.Item>
<Form.Item wrapperCol={{ offset: 2, span: 6 }}>
<Button type="primary" htmlType="submit" style={{ marginLeft: 40 }}>
注册
</Button>
</Form.Item>
</Form>
</Drawer>
</div>
);
}

461
src/chat/Panel.jsx

@ -7,7 +7,7 @@ import { @@ -7,7 +7,7 @@ import {
Drawer,
Tag,
Popover,
Tooltip
Tooltip,
} from 'antd';
import {
UserOutlined,
@ -21,17 +21,22 @@ import { @@ -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 = { @@ -105,7 +110,6 @@ var heartCheck = {
}
}
class Panel extends React.Component {
constructor(props) {
super(props)
@ -137,12 +141,13 @@ class Panel extends React.Component { @@ -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 { @@ -194,34 +199,54 @@ class Panel extends React.Component {
const messagePB = message.create(data)
socket.send(message.encode(messagePB).finish())
// ArrayBufferbase64
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: <p><img src={base64String} alt="" width="150px" /></p>,
datetime: moment().fromNow(),
},
],
})
this.appendImgToPanel(imgData)
})
}, false)
}
/**
* 本地上传后将图片追加到聊天框
* @param {Arraybuffer类型图片}} imgData
*/
appendImgToPanel(imgData) {
// ArrayBufferbase64
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: <p><img src={base64String} alt="" width="150px" /></p>,
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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: <p>{(messagePB.contentType === 2 || messagePB.contentType === 3) ? <img src={Params.HOST + "/file/" + messagePB.url} alt="" height="350px" /> : messagePB.content}</p>,
content: <p>{content}</p>,
datetime: moment().fromNow(),
},
],
}, () => {
this.scrollToBottom()
setTimeout(this.scrollToBottom(), 3000)
})
})
}
@ -360,6 +346,98 @@ class Panel extends React.Component { @@ -360,6 +346,98 @@ class Panel extends React.Component {
}
}
/**
* webrtc 绑定事件
*/
webrtcConnection = () => {
/**
* 对等方收到ice信息后通过调用 addIceCandidate 将接收的候选者信息传递给浏览器的ICE代理
* @param {候选人信息} e
*/
peer.onicecandidate = (e) => {
if (e.candidate) {
// rtcTypeansweroffer
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 = <img src={Params.HOST + "/file/" + data[i].url} alt="" width="150px" />
} else if (contentType === 4) {
content = <audio src={Params.HOST + "/file/" + data[i].url} controls autoPlay={false} preload="auto" />
} else if (contentType === 5) {
content = <video src={Params.HOST + "/file/" + data[i].url} controls autoPlay={false} preload="auto" width='200px' />
}
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: <p>{content}</p>,
datetime: moment(data[i].createAt).fromNow(),
}
@ -626,17 +712,40 @@ class Panel extends React.Component { @@ -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 = <FileOutlined style={{ fontSize: 38 }} />
} else if (type === 3) {
content = <img src={Params.HOST + "/file/" + url} alt="" width="150px" />
} else if (type === 4) {
content = <audio src={Params.HOST + "/file/" + url} controls autoPlay={false} preload="auto" />
} else if (type === 5) {
content = <video src={Params.HOST + "/file/" + url} controls autoPlay={false} preload="auto" width='200px' />
}
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 { @@ -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 { @@ -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: <p><FileOutlined style={{ fontSize: 38 }} /></p>,
datetime: moment().fromNow(),
},
],
}, () => {
this.scrollToBottom()
})
}
/**
* 开始录制音频
*/
@ -854,7 +997,8 @@ class Panel extends React.Component { @@ -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 { @@ -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
// // ArrayBufferUint8Array
// 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 { @@ -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 { @@ -1032,9 +1145,7 @@ class Panel extends React.Component {
<Row style={{ paddingTop: 20, paddingBottom: 40 }}>
<Col span={2} style={{ borderRight: '1px solid #f0f0f0', textAlign: 'center' }}>
<p style={{ marginTop: 15 }}>
<Tooltip title={this.state.user.username}>
<Avatar src={this.state.user.avatar} alt={this.state.user.username} />
</Tooltip>
<UserInfo history={this.props.history} />
</p>
<p >
<Button icon={<UserOutlined />} size="large" type='link' disabled={this.state.menuType === 1} onClick={this.fetchUserList}>
@ -1133,7 +1244,9 @@ class Panel extends React.Component { @@ -1133,7 +1244,9 @@ class Panel extends React.Component {
shape="circle"
onClick={this.startVideoOnline}
style={{ marginRight: 10 }}
icon={<PhoneOutlined />} disabled={toUser === ''}
icon={<PhoneOutlined />}
// disabled={toUser === ''}
disabled
/>
</Tooltip>
<Tooltip title="视频聊天">
@ -1189,9 +1302,14 @@ class Panel extends React.Component { @@ -1189,9 +1302,14 @@ class Panel extends React.Component {
</Col>
</Row>
<Modal title="用户信息" visible={this.state.hasUser} onOk={this.handleOk} onCancel={this.handleCancel} okText="添加用户">
<Modal title="用户信息" visible={this.state.hasUser} onCancel={this.handleCancel} okText="添加用户" footer={null}>
<p>用户名{this.state.queryUser.username}</p>
<p>昵称{this.state.queryUser.nickname}</p>
<Button type='primary' onClick={this.handleOk} disabled={this.state.queryUser.username == null || this.state.queryUser.username === ''}>添加用户</Button>
<br /><br /><hr /><br /><br />
<p>群信息{this.state.queryUser.groupName}</p>
<Button type='primary' onClick={this.joinGroup} disabled={this.state.queryUser.groupUuid == null || this.state.queryUser.groupUuid === ''}>添加群</Button>
</Modal>
<Drawer title="成员列表" placement="right" onClose={this.drawerOnClose} visible={this.state.drawerVisible}>
@ -1202,7 +1320,7 @@ class Panel extends React.Component { @@ -1202,7 +1320,7 @@ class Panel extends React.Component {
<List.Item>
<List.Item.Meta
style={{ paddingLeft: 30 }}
avatar={<Avatar src={item.avatar} />}
avatar={<Avatar src={Params.HOST + "/file/" + item.avatar} />}
title={item.username}
description=""
/>
@ -1211,9 +1329,20 @@ class Panel extends React.Component { @@ -1211,9 +1329,20 @@ class Panel extends React.Component {
/>
</Drawer>
<Drawer width='1000px' forceRender={true} title="媒体面板" placement="right" onClose={this.mediaPanelDrawerOnClose} visible={this.state.mediaPanelDrawerVisible}>
<video id="preview1" width="540px" height="auto" autoPlay muted controls />
<img id="receiver" width={this.state.currentScreen.width} height={this.state.currentScreen.height} alt="" />
<Drawer width='820px' forceRender={true} title="媒体面板" placement="right" onClose={this.mediaPanelDrawerOnClose} visible={this.state.mediaPanelDrawerVisible}>
<Tooltip title="结束视频语音">
<Button
shape="circle"
onClick={this.stopVideoOnline}
style={{ marginRight: 10, float: 'right' }}
icon={<PoweroffOutlined style={{ color: 'red' }} />}
/>
</Tooltip>
<br />
<video id="preview1" width="700px" height="auto" autoPlay muted controls />
<video id="remoteVideo" width="700px" height="auto" autoPlay muted controls />
<img id="receiver" width={this.state.currentScreen.width} height="auto" alt="" />
<canvas id="canvas" width={this.state.currentScreen.width} height={this.state.currentScreen.height} />
</Drawer>
</>
@ -1221,4 +1350,18 @@ class Panel extends React.Component { @@ -1221,4 +1350,18 @@ class Panel extends React.Component {
}
}
export default Panel;
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

7
src/chat/common/param/Params.jsx

@ -1,12 +1,14 @@ @@ -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' @@ -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'

159
src/chat/component/UserInfo.jsx

@ -0,0 +1,159 @@ @@ -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 = (
<Menu>
<Menu.Item key={1}>
<Button type='link'>{this.props.user.username}</Button>
</Menu.Item>
<Menu.Item key={2}>
<Button type='link' onClick={this.modifyAvatar}>更新头像</Button>
</Menu.Item>
<Menu.Item key={3}>
<Button type='link' onClick={this.loginout}>退出</Button>
</Menu.Item>
</Menu>
);
const { loading, imageUrl } = this.state;
const uploadButton = (
<div>
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div style={{ marginTop: 8 }}>Upload</div>
</div>
);
return (
<>
<Dropdown overlay={menu} placement="bottomCenter" arrow>
<Avatar src={this.props.user.avatar} alt={this.props.user.username} />
</Dropdown>
<Modal title="更新头像" visible={this.state.isModalVisible} onCancel={this.handleCancel} footer={null}>
<Upload
name="file"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action={Params.FILE_URL}
beforeUpload={beforeUpload}
onChange={this.handleChange}
data={{uuid: this.props.user.uuid}}
>
{imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
</Upload>
</Modal>
</>
);
}
}
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

3
src/chat/proto/message.proto

@ -11,5 +11,6 @@ message Message { @@ -11,5 +11,6 @@ message Message {
string type = 7;
int32 messageType = 8;
string url = 9;
bytes file = 10;
string fileSuffix = 10;
bytes file = 11;
}

7
src/chat/proto/proto.js

@ -1,5 +1,4 @@ @@ -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 @@ -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
}
}
}

9
src/chat/redux/module/index.jsx

@ -0,0 +1,9 @@ @@ -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));

25
src/chat/redux/module/userInfo.jsx

@ -0,0 +1,25 @@ @@ -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

8
src/index.js

@ -5,15 +5,19 @@ import Login from './chat/Login'; @@ -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(
<BrowserRouter>
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/login" component={Login} />
<Route path="/panel/:user" component={Panel} />
<Route path="/" component={Login} />
</Switch>
</BrowserRouter>,
</BrowserRouter>
</Provider>,
document.getElementById('root')
);

77
yarn.lock

@ -1239,7 +1239,7 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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"

Loading…
Cancel
Save