├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── screenshot │ ├── go-chat-panel.jpeg │ ├── screen-share.png │ └── video-chat.png └── src ├── chat ├── Login.jsx ├── Panel.jsx ├── common │ ├── constant │ │ └── Constant.jsx │ └── param │ │ └── Params.jsx ├── panel │ ├── center │ │ ├── component │ │ │ ├── UserList.jsx │ │ │ └── UserSearch.jsx │ │ └── index.jsx │ ├── left │ │ ├── component │ │ │ ├── SwitchChat.jsx │ │ │ └── UserInfo.jsx │ │ └── index.jsx │ └── right │ │ ├── component │ │ ├── ChatAudio.jsx │ │ ├── ChatAudioOline.jsx │ │ ├── ChatDetails.jsx │ │ ├── ChatEdit.jsx │ │ ├── ChatFile.jsx │ │ ├── ChatShareScreen.jsx │ │ ├── ChatVideo.jsx │ │ └── ChatVideoOline.jsx │ │ └── index.jsx ├── proto │ ├── message.proto │ └── proto.js ├── redux │ └── module │ │ ├── index.jsx │ │ ├── panel.jsx │ │ └── userInfo.jsx └── util │ └── Request.jsx ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine3.15 as builder 2 | WORKDIR /home/go-chat-web 3 | COPY ./ /home/go-chat-web 4 | 5 | RUN npm config set registry http://registry.npm.taobao.org 6 | RUN npm install 7 | RUN npm run build && rm -rf ./node_modules 8 | 9 | WORKDIR /home/go-chat-web 10 | COPY --from=builder /home/go-chat-web/build /home/go-chat-web 11 | RUN npm config set registry http://registry.npm.taobao.org 12 | RUN npm install -g serve 13 | 14 | CMD ["serve", "-s"] 15 | 16 | 17 | # 如果本地编译好,直接复制build文件后运行 18 | # FROM node:16-alpine3.15 19 | # WORKDIR /home/go-chat-web 20 | # COPY ./ /home/go-chat-web 21 | # RUN npm config set registry http://registry.npm.taobao.org 22 | # RUN npm install -g serve 23 | # CMD ["serve", "-s"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://api.gitsponsors.com/api/badge/link?p=7tFDsd584o96OI5wF/+5gGz7zPQ0oqxF/5x80dXnuK4GFcxewmnznrgb1ptFymOUd1XshHXAUoy5G0P6j+IZ0fqmzTbrrQWA3BEOIbnRzjG+A4q3JA2rkUDWOjslmFtRCc/SAg7DHBS+wTFrfpErUQ==) 2 | 3 | ## go-chat 4 | 使用Go基于WebSocket的通讯聊天软件。 5 | 6 | ### 功能列表: 7 | * 登录注册 8 | * 修改头像 9 | * 群聊天 10 | * 群好友列表 11 | * 单人聊天 12 | * 添加好友 13 | * 添加群组 14 | * 文本消息 15 | * 剪切板图片 16 | * 图片消息 17 | * 文件发送 18 | * 语音消息 19 | * 视频消息 20 | * 屏幕共享(基于图片) 21 | * 视频通话(基于webrtc的p2p视频通话) 22 | 23 | ## 后端 24 | [代码仓库](https://github.com/kone-net/go-chat) 25 | go中协程是非常轻量级的。在每个client接入的时候,为每一个client开启一个协程,能够在单机实现更大的并发。同时go的channel,可以非常完美的解耦client接入和消息的转发等操作。 26 | 27 | 通过go-chat,可以掌握channel的和Select的配合使用,ORM框架的使用,web框架Gin的使用,配置管理,日志操作,还包括proto buffer协议的使用,等一些列项目中常用的技术。 28 | 29 | 30 | ### 后端技术和框架 31 | * web框架Gin 32 | * 长连接WebSocket 33 | * 日志框架Uber的zap 34 | * 配置管理viper 35 | * ORM框架gorm 36 | * 通讯协议Google的proto buffer 37 | * makefile 的编写 38 | * 数据库MySQL 39 | * 图片文件二进制操作 40 | 41 | ## 前端 42 | 基于react,UI和基本组件是使用ant design。可以很方便搭建前端界面。 43 | 44 | 界面选择单页框架可以更加方便写聊天界面,比如像消息提醒,可以在一个界面接受到消息进行提醒,不会因为换页面或者查看其他内容影响消息接受。 45 | [前端代码仓库](https://github.com/kone-net/go-chat-web): 46 | https://github.com/kone-net/go-chat-web 47 | 48 | 49 | ### 前端技术和框架 50 | * React 51 | * Redux状态管理 52 | * AntDesign 53 | * proto buffer的使用 54 | * WebSocket 55 | * 剪切板的文件读取和操作 56 | * 聊天框发送文字显示底部 57 | * FileReader对文件操作 58 | * ArrayBuffer,Blob,Uint8Array之间的转换 59 | * 获取摄像头视频(mediaDevices) 60 | * 获取麦克风音频(Recorder) 61 | * 获取屏幕共享(mediaDevices) 62 | * WebRTC的p2p视频通话 63 | 64 | 65 | ### 截图 66 | * 语音,文字,图片,视频消息 67 | ![go-chat-panel](/public/screenshot/go-chat-panel.jpeg) 68 | 69 | * 视频通话 70 | ![video-chat](/public/screenshot/video-chat.png) 71 | 72 | * 屏幕共享 73 | ![screen-share](/public/screenshot/screen-share.png) 74 | 75 | ## 分支说明 76 | one-file分支: 77 | 该分支是所有逻辑都在一个文件实现,包括语音,文字,图片,视频消息,视频通话,语音电话,屏幕共享。 78 | main分支: 79 | 是将各个部分进行拆分。将Panel拆分成,左、中、右。又将右边的发送文件,图片,文件拆分成更小的组件。 80 | 81 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | scan-web-one: 5 | build: 6 | context: ./ 7 | dockerfile: ./Dockerfile 8 | image: go-chat-web:v1 9 | container_name: go-chat-web 10 | restart: always 11 | ports: 12 | - 3000:3000 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-room", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^18.3.1", 7 | "react-dom": "^18.3.1", 8 | "react-scripts": "5.0.1", 9 | "web-vitals": "^2.1.4", 10 | 11 | "antd": "^4.16.13", 12 | "axios": "^0.24.0", 13 | "js-audio-recorder": "^1.0.7", 14 | "protobufjs": "^6.11.2", 15 | "socket.io-client": "^4.3.2", 16 | "react-infinite-scroll-component": "^6.1.0", 17 | "react-redux": "^7.2.6", 18 | "redux-thunk": "^2.4.0", 19 | "react-router-dom": "^5.3.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject", 26 | "proto": "npx pbjs -t json-module -w commonjs -o src/chat/proto/proto.js src/chat/proto/*.proto" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kone-net/go-chat-web/0de8d3ed8a0e250dff7562bea054f8c7ed2e490b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | go-chat 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kone-net/go-chat-web/0de8d3ed8a0e250dff7562bea054f8c7ed2e490b/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kone-net/go-chat-web/0de8d3ed8a0e250dff7562bea054f8c7ed2e490b/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/screenshot/go-chat-panel.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kone-net/go-chat-web/0de8d3ed8a0e250dff7562bea054f8c7ed2e490b/public/screenshot/go-chat-panel.jpeg -------------------------------------------------------------------------------- /public/screenshot/screen-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kone-net/go-chat-web/0de8d3ed8a0e250dff7562bea054f8c7ed2e490b/public/screenshot/screen-share.png -------------------------------------------------------------------------------- /public/screenshot/video-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kone-net/go-chat-web/0de8d3ed8a0e250dff7562bea054f8c7ed2e490b/public/screenshot/video-chat.png -------------------------------------------------------------------------------- /src/chat/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Button, 4 | Form, 5 | Input, 6 | Drawer, 7 | message 8 | } from 'antd'; 9 | import { axiosPostBody } from './util/Request'; 10 | import * as Params from './common/param/Params' 11 | 12 | class Login extends React.Component { 13 | constructor(props) { 14 | super(props) 15 | this.state = { 16 | registerDrawerVisible: false 17 | } 18 | 19 | } 20 | 21 | componentDidMount() { 22 | 23 | } 24 | 25 | onFinish = (values) => { 26 | let data = { 27 | username: values.username, 28 | password: values.password 29 | } 30 | axiosPostBody(Params.LOGIN_URL, data) 31 | .then(response => { 32 | message.success("登录成功!"); 33 | localStorage.username = response.data.username 34 | this.props.history.push("panel/" + response.data.uuid) 35 | }); 36 | }; 37 | 38 | onFinishFailed = (errorInfo) => { 39 | console.log('Failed:', errorInfo); 40 | }; 41 | 42 | showRegister = () => { 43 | this.setState({ 44 | registerDrawerVisible: true 45 | }) 46 | } 47 | 48 | registerDrawerOnClose = () => { 49 | this.setState({ 50 | registerDrawerVisible: false 51 | }) 52 | } 53 | 54 | onRegister = (values) => { 55 | let data = { 56 | ...values 57 | } 58 | 59 | axiosPostBody(Params.REGISTER_URL, data) 60 | .then(_response => { 61 | message.success("注册成功!"); 62 | this.setState({ 63 | registerDrawerVisible: false 64 | }) 65 | }); 66 | } 67 | 68 | render() { 69 | 70 | return ( 71 |
72 |
81 | 86 | 87 | 88 | 89 | 94 | 95 | 96 | 97 | 98 | 101 | 102 | 105 | 106 | 107 |
108 | 109 | 110 |
118 | 123 | 124 | 125 | 126 | 131 | 132 | 133 | 134 | 139 | 140 | 141 | 142 | 147 | 148 | 149 | 150 | 151 | 154 | 155 | 156 |
157 |
158 |
159 | ); 160 | } 161 | } 162 | 163 | export default Login; -------------------------------------------------------------------------------- /src/chat/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Button, 4 | Row, 5 | Col, 6 | message, 7 | Drawer, 8 | Tooltip, 9 | Modal 10 | } from 'antd'; 11 | import { 12 | PoweroffOutlined, 13 | FileOutlined, 14 | } from '@ant-design/icons'; 15 | import moment from 'moment'; 16 | import * as Params from './common/param/Params' 17 | import * as Constant from './common/constant/Constant' 18 | import Center from './panel/center/index' 19 | import Left from './panel/left/index' 20 | import Right from './panel/right/index' 21 | 22 | import protobuf from './proto/proto' 23 | import { connect } from 'react-redux' 24 | import { actions } from './redux/module/panel' 25 | 26 | var socket = null; 27 | var peer = null; 28 | var lockConnection = false; 29 | 30 | var heartCheck = { 31 | timeout: 10000, 32 | timeoutObj: null, 33 | serverTimeoutObj: null, 34 | num: 3, 35 | start: function () { 36 | var self = this; 37 | var _num = this.num 38 | this.timeoutObj && clearTimeout(this.timeoutObj); 39 | this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj); 40 | this.timeoutObj = setTimeout(function () { 41 | //这里发送一个心跳,后端收到后,返回一个心跳消息, 42 | //onmessage拿到返回的心跳就说明连接正常 43 | let data = { 44 | type: "heatbeat", 45 | content: "ping", 46 | } 47 | 48 | if (socket.readyState === 1) { 49 | let message = protobuf.lookup("protocol.Message") 50 | const messagePB = message.create(data) 51 | socket.send(message.encode(messagePB).finish()) 52 | } 53 | 54 | self.serverTimeoutObj = setTimeout(function () { 55 | _num-- 56 | if (_num <= 0) { 57 | console.log("the ping num is more then 3, close socket!") 58 | socket.close(); 59 | } 60 | }, self.timeout); 61 | 62 | }, this.timeout) 63 | } 64 | } 65 | 66 | class Panel extends React.Component { 67 | constructor(props) { 68 | super(props) 69 | localStorage.uuid = props.match.params.user; 70 | this.state = { 71 | onlineType: 1, // 在线视频或者音频: 1视频,2音频 72 | video: { 73 | height: 400, 74 | width: 540 75 | }, 76 | share: { 77 | height: 540, 78 | width: 750 79 | }, 80 | currentScreen: { 81 | height: 0, 82 | width: 0 83 | }, 84 | videoCallModal: false, 85 | callName: '', 86 | fromUserUuid: '', 87 | } 88 | } 89 | 90 | componentDidMount() { 91 | this.connection() 92 | } 93 | 94 | /** 95 | * websocket连接 96 | */ 97 | connection = () => { 98 | console.log("to connect...") 99 | peer = new RTCPeerConnection(); 100 | var image = document.getElementById('receiver'); 101 | socket = new WebSocket("ws://" + Params.IP_PORT + "/socket.io?user=" + this.props.match.params.user) 102 | 103 | socket.onopen = () => { 104 | heartCheck.start() 105 | console.log("connected") 106 | this.webrtcConnection() 107 | 108 | this.props.setSocket(socket); 109 | } 110 | socket.onmessage = (message) => { 111 | heartCheck.start() 112 | 113 | // 接收到的message.data,是一个blob对象。需要将该对象转换为ArrayBuffer,才能进行proto解析 114 | let messageProto = protobuf.lookup("protocol.Message") 115 | let reader = new FileReader(); 116 | reader.readAsArrayBuffer(message.data); 117 | reader.onload = ((event) => { 118 | let messagePB = messageProto.decode(new Uint8Array(event.target.result)) 119 | console.log(messagePB) 120 | if (messagePB.type === "heatbeat") { 121 | return; 122 | } 123 | 124 | // 接受语音电话或者视频电话 webrtc 125 | if (messagePB.type === Constant.MESSAGE_TRANS_TYPE) { 126 | this.dealWebRtcMessage(messagePB); 127 | return; 128 | } 129 | 130 | // 如果该消息不是正在聊天消息,显示未读提醒 131 | if (this.props.chooseUser.toUser !== messagePB.from) { 132 | this.showUnreadMessageDot(messagePB.from); 133 | return; 134 | } 135 | 136 | // 视频图像 137 | if (messagePB.contentType === 8) { 138 | let currentScreen = { 139 | width: this.state.video.width, 140 | height: this.state.video.height 141 | } 142 | this.setState({ 143 | currentScreen: currentScreen 144 | }) 145 | image.src = messagePB.content 146 | return; 147 | } 148 | 149 | // 屏幕共享 150 | if (messagePB.contentType === 9) { 151 | let currentScreen = { 152 | width: this.state.share.width, 153 | height: this.state.share.height 154 | } 155 | this.setState({ 156 | currentScreen: currentScreen 157 | }) 158 | image.src = messagePB.content 159 | return; 160 | } 161 | 162 | // // 接受语音电话或者视频电话 webrtc 163 | // if (messagePB.type === Constant.MESSAGE_TRANS_TYPE) { 164 | // this.dealWebRtcMessage(messagePB); 165 | // return; 166 | // } 167 | 168 | let avatar = this.props.chooseUser.avatar 169 | if (messagePB.messageType === 2) { 170 | avatar = Params.HOST + "/file/" + messagePB.avatar 171 | } 172 | 173 | // 文件内容,录制的视频,语音内容 174 | let content = this.getContentByType(messagePB.contentType, messagePB.url, messagePB.content) 175 | let messageList = [ 176 | ...this.props.messageList, 177 | { 178 | author: messagePB.fromUsername, 179 | avatar: avatar, 180 | content:

{content}

, 181 | datetime: moment().fromNow(), 182 | }, 183 | ]; 184 | this.props.setMessageList(messageList); 185 | }) 186 | } 187 | 188 | socket.onclose = (_message) => { 189 | console.log("close and reconnect-->--->") 190 | 191 | this.reconnect() 192 | } 193 | 194 | socket.onerror = (_message) => { 195 | console.log("error----->>>>") 196 | 197 | this.reconnect() 198 | } 199 | } 200 | 201 | /** 202 | * webrtc 绑定事件 203 | */ 204 | webrtcConnection = () => { 205 | /** 206 | * 对等方收到ice信息后,通过调用 addIceCandidate 将接收的候选者信息传递给浏览器的ICE代理。 207 | * @param {候选人信息} e 208 | */ 209 | peer.onicecandidate = (e) => { 210 | if (e.candidate) { 211 | // rtcType参数默认是对端值为answer,如果是发起端,会将值设置为offer 212 | let candidate = { 213 | type: 'answer_ice', 214 | iceCandidate: e.candidate 215 | } 216 | let message = { 217 | content: JSON.stringify(candidate), 218 | type: Constant.MESSAGE_TRANS_TYPE, 219 | } 220 | this.sendMessage(message); 221 | } 222 | 223 | }; 224 | 225 | /** 226 | * 当连接成功后,从里面获取语音视频流 227 | * @param {包含语音视频流} e 228 | */ 229 | peer.ontrack = (e) => { 230 | if (e && e.streams) { 231 | if (this.state.onlineType === 1) { 232 | let remoteVideo = document.getElementById("remoteVideoReceiver"); 233 | remoteVideo.srcObject = e.streams[0]; 234 | } else { 235 | let remoteAudio = document.getElementById("audioPhone"); 236 | remoteAudio.srcObject = e.streams[0]; 237 | } 238 | } 239 | }; 240 | } 241 | 242 | /** 243 | * 处理webrtc消息,包括获取请求方的offer,回应answer等 244 | * @param {消息内容}} messagePB 245 | */ 246 | dealWebRtcMessage = (messagePB) => { 247 | if (messagePB.contentType >= Constant.DIAL_MEDIA_START && messagePB.contentType <= Constant.DIAL_MEDIA_END) { 248 | this.dealMediaCall(messagePB); 249 | return; 250 | } 251 | const { type, sdp, iceCandidate } = JSON.parse(messagePB.content); 252 | 253 | if (type === "answer") { 254 | const answerSdp = new RTCSessionDescription({ type, sdp }); 255 | this.props.peer.localPeer.setRemoteDescription(answerSdp) 256 | } else if (type === "answer_ice") { 257 | this.props.peer.localPeer.addIceCandidate(iceCandidate) 258 | } else if (type === "offer_ice") { 259 | peer.addIceCandidate(iceCandidate) 260 | } else if (type === "offer") { 261 | if (!this.checkMediaPermisssion()) { 262 | return; 263 | } 264 | let preview 265 | 266 | let video = false; 267 | if (messagePB.contentType === Constant.VIDEO_ONLINE) { 268 | preview = document.getElementById("localVideoReceiver"); 269 | video = true 270 | this.setState({ 271 | onlineType: 1, 272 | }) 273 | } else { 274 | preview = document.getElementById("audioPhone"); 275 | this.setState({ 276 | onlineType: 2, 277 | }) 278 | } 279 | 280 | navigator.mediaDevices 281 | .getUserMedia({ 282 | audio: true, 283 | video: video, 284 | }).then((stream) => { 285 | preview.srcObject = stream; 286 | stream.getTracks().forEach(track => { 287 | peer.addTrack(track, stream); 288 | }); 289 | 290 | // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行answer创建。不然不能获取到流,从而不能播放视频。 291 | const offerSdp = new RTCSessionDescription({ type, sdp }); 292 | peer.setRemoteDescription(offerSdp) 293 | .then(() => { 294 | peer.createAnswer().then(answer => { 295 | peer.setLocalDescription(answer) 296 | 297 | let message = { 298 | content: JSON.stringify(answer), 299 | type: Constant.MESSAGE_TRANS_TYPE, 300 | messageType: messagePB.contentType 301 | } 302 | this.sendMessage(message); 303 | }) 304 | }); 305 | }); 306 | } 307 | } 308 | 309 | /** 310 | * 断开连接后重新连接 311 | */ 312 | reconnectTimeoutObj = null; 313 | reconnect = () => { 314 | if (lockConnection) return; 315 | lockConnection = true 316 | 317 | this.reconnectTimeoutObj && clearTimeout(this.reconnectTimeoutObj) 318 | 319 | this.reconnectTimeoutObj = setTimeout(() => { 320 | if (socket.readyState !== 1) { 321 | this.connection() 322 | } 323 | lockConnection = false 324 | }, 10000) 325 | } 326 | 327 | /** 328 | * 检查媒体权限是否开启 329 | * @returns 媒体权限是否开启 330 | */ 331 | checkMediaPermisssion = () => { 332 | navigator.getUserMedia = navigator.getUserMedia || 333 | navigator.webkitGetUserMedia || 334 | navigator.mozGetUserMedia || 335 | navigator.msGetUserMedia; //获取媒体对象(这里指摄像头) 336 | if (!navigator || !navigator.mediaDevices) { 337 | message.error("获取摄像头权限失败!") 338 | return false; 339 | } 340 | return true; 341 | } 342 | 343 | /** 344 | * 发送消息 345 | * @param {消息内容} messageData 346 | */ 347 | sendMessage = (messageData) => { 348 | let toUser = messageData.toUser; 349 | if (null == toUser) { 350 | toUser = this.props.chooseUser.toUser; 351 | } 352 | let data = { 353 | ...messageData, 354 | messageType: this.props.chooseUser.messageType, // 消息类型,1.单聊 2.群聊 355 | fromUsername: localStorage.username, 356 | from: localStorage.uuid, 357 | to: toUser, 358 | } 359 | let message = protobuf.lookup("protocol.Message") 360 | const messagePB = message.create(data) 361 | 362 | socket.send(message.encode(messagePB).finish()) 363 | } 364 | 365 | /** 366 | * 根据文件类型渲染对应的标签,比如视频,图片等。 367 | * @param {文件类型} type 368 | * @param {文件地址} url 369 | * @returns 370 | */ 371 | getContentByType = (type, url, content) => { 372 | if (type === 2) { 373 | content = 374 | } else if (type === 3) { 375 | content = 376 | } else if (type === 4) { 377 | content =