From 9b061c2d6f9f87ca64bba4e0f9df60492627ce29 Mon Sep 17 00:00:00 2001
From: konenet
Date: Sat, 11 Dec 2021 00:57:58 +0800
Subject: [PATCH] refactor: refactor project layout and spread to multiple
components
---
src/chat/Panel.jsx | 1142 +----------------
src/chat/panel/center/component/UserList.jsx | 146 +++
.../panel/center/component/UserSearch.jsx | 227 ++++
src/chat/panel/center/index.jsx | 16 +
src/chat/panel/left/component/SwitchChat.jsx | 125 ++
.../{ => panel/left}/component/UserInfo.jsx | 23 +-
src/chat/panel/left/index.jsx | 16 +
src/chat/panel/right/component/ChatAudio.jsx | 127 ++
.../panel/right/component/ChatAudioOline.jsx | 180 +++
.../panel/right/component/ChatDetails.jsx | 156 +++
src/chat/panel/right/component/ChatEdit.jsx | 153 +++
src/chat/panel/right/component/ChatFile.jsx | 121 ++
.../panel/right/component/ChatShareScreen.jsx | 162 +++
src/chat/panel/right/component/ChatVideo.jsx | 148 +++
.../panel/right/component/ChatVideoOline.jsx | 180 +++
src/chat/panel/right/index.jsx | 199 +++
src/chat/redux/module/index.jsx | 4 +-
src/chat/redux/module/panel.jsx | 76 ++
18 files changed, 2116 insertions(+), 1085 deletions(-)
create mode 100644 src/chat/panel/center/component/UserList.jsx
create mode 100644 src/chat/panel/center/component/UserSearch.jsx
create mode 100644 src/chat/panel/center/index.jsx
create mode 100644 src/chat/panel/left/component/SwitchChat.jsx
rename src/chat/{ => panel/left}/component/UserInfo.jsx (86%)
create mode 100644 src/chat/panel/left/index.jsx
create mode 100644 src/chat/panel/right/component/ChatAudio.jsx
create mode 100644 src/chat/panel/right/component/ChatAudioOline.jsx
create mode 100644 src/chat/panel/right/component/ChatDetails.jsx
create mode 100644 src/chat/panel/right/component/ChatEdit.jsx
create mode 100644 src/chat/panel/right/component/ChatFile.jsx
create mode 100644 src/chat/panel/right/component/ChatShareScreen.jsx
create mode 100644 src/chat/panel/right/component/ChatVideo.jsx
create mode 100644 src/chat/panel/right/component/ChatVideoOline.jsx
create mode 100644 src/chat/panel/right/index.jsx
create mode 100644 src/chat/redux/module/panel.jsx
diff --git a/src/chat/Panel.jsx b/src/chat/Panel.jsx
index 4ce2b33..b77b838 100755
--- a/src/chat/Panel.jsx
+++ b/src/chat/Panel.jsx
@@ -1,78 +1,29 @@
import React from 'react';
import {
- Comment, Avatar, Form, Button, List, Input, Row, Col, Badge,
- Card,
+ Button,
+ Row,
+ Col,
message,
- Modal,
Drawer,
- Tag,
- Popover,
Tooltip,
} from 'antd';
import {
- UserOutlined,
- TeamOutlined,
- MoreOutlined,
- SyncOutlined,
- FileAddOutlined,
- VideoCameraAddOutlined,
- AudioOutlined,
PoweroffOutlined,
- PhoneOutlined,
- VideoCameraOutlined,
- UngroupOutlined,
- DesktopOutlined,
- FileOutlined
+ 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 * as Constant from './common/constant/Constant'
-import UserInfo from './component/UserInfo'
+import Center from './panel/center/index'
+import Left from './panel/left/index'
+import Right from './panel/right/index'
import protobuf from './proto/proto'
-import Recorder from 'js-audio-recorder';
import { connect } from 'react-redux'
-import { actions } from './redux/module/userInfo'
+import { actions } from './redux/module/panel'
var socket = null;
var peer = null;
-
-const { TextArea } = Input;
-
-const CommentList = ({ comments }) => (
- }
- // endMessage={It is all, nothing more 🤐}
- scrollableTarget="scrollableDiv"
- >
- 1 ? 'replies' : 'reply'}`}
- itemLayout="horizontal"
- renderItem={props => }
- />
-
-);
-
-const Editor = ({ onChange, onSubmit, submitting, value, toUser }) => (
- <>
-
-
-
-
-
-
- >
-);
-
-
var lockConnection = false;
var heartCheck = {
@@ -114,29 +65,9 @@ var heartCheck = {
class Panel extends React.Component {
constructor(props) {
super(props)
+ localStorage.uuid = props.match.params.user;
this.state = {
- isRecord: false,
onlineType: 1, // 在线视频或者音频: 1视频,2音频
- user: {},
- comments: [],
- submitting: false,
- value: '',
- toUser: '',
- toUsername: ' ',
- fromUser: props.match.params.user,
- hasUser: false,
- queryUser: {
- username: '',
- nickname: '',
- },
- data: [
-
- ],
- messageType: 1,
- menuType: 1,
- drawerVisible: false,
- mediaPanelDrawerVisible: false,
- groupUsers: [],
video: {
height: 400,
width: 540
@@ -154,103 +85,7 @@ class Panel extends React.Component {
}
componentDidMount() {
- this.fetchUserDetails()
- this.fetchUserList()
this.connection()
- this.bindParse()
- }
-
- /**
- * 解析剪切板的文件
- */
- bindParse = () => {
- document.getElementById("messageArea").addEventListener("paste", (e) => {
- var data = e.clipboardData
- if (!data.items) {
- return;
- }
- var items = data.items
-
- if (null == items || items.length <= 0) {
- return;
- }
-
- let item = items[0]
- if (item.kind !== 'file') {
- return;
- }
- let blob = item.getAsFile()
-
- let reader = new FileReader()
- reader.readAsArrayBuffer(blob)
-
- reader.onload = ((e) => {
- let imgData = 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: 3,
- file: new Uint8Array(imgData)
- }
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
-
- 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: user,
- })
- });
}
/**
@@ -266,6 +101,8 @@ class Panel extends React.Component {
heartCheck.start()
console.log("connected")
this.webrtcConnection()
+
+ this.props.setSocket(socket);
}
socket.onmessage = (message) => {
heartCheck.start()
@@ -276,7 +113,7 @@ class Panel extends React.Component {
reader.readAsArrayBuffer(message.data);
reader.onload = ((event) => {
let messagePB = messageProto.decode(new Uint8Array(event.target.result))
- if (this.state.toUser !== messagePB.from || messagePB.type === "heatbeat") {
+ if (this.props.chooseUser.toUser !== messagePB.from || messagePB.type === "heatbeat") {
return;
}
@@ -312,36 +149,33 @@ class Panel extends React.Component {
return;
}
- let avatar = this.state.avatar
+ let avatar = this.props.chooseUser.avatar
if (messagePB.messageType === 2) {
- avatar = messagePB.avatar
+ avatar = Params.HOST + "/file/" + messagePB.avatar
}
// 文件内容,录制的视频,语音内容
let content = this.getContentByType(messagePB.contentType, messagePB.url, messagePB.content)
- this.setState({
- comments: [
- ...this.state.comments,
- {
- author: messagePB.fromUsername,
- avatar: avatar,
- content: {content}
,
- datetime: moment().fromNow(),
- },
- ],
- }, () => {
- setTimeout(this.scrollToBottom(), 3000)
- })
+ let messageList = [
+ ...this.props.messageList,
+ {
+ author: messagePB.fromUsername,
+ avatar: avatar,
+ content: {content}
,
+ datetime: moment().fromNow(),
+ },
+ ];
+ this.props.setMessageList(messageList);
})
}
- socket.onclose = (message) => {
+ socket.onclose = (_message) => {
console.log("close and reconnect-->--->")
this.reconnect()
}
- socket.onerror = (message) => {
+ socket.onerror = (_message) => {
console.log("error----->>>>")
this.reconnect()
@@ -357,24 +191,18 @@ class Panel extends React.Component {
* @param {候选人信息} e
*/
peer.onicecandidate = (e) => {
+ console.log(this.state.rtcType + '_ice', 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,
+ let message = {
content: JSON.stringify(candidate),
type: Constant.MESSAGE_TRANS_TYPE,
}
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
+ this.sendMessage(message);
}
};
@@ -384,6 +212,7 @@ class Panel extends React.Component {
* @param {包含语音视频流} e
*/
peer.ontrack = (e) => {
+ console.log(e)
if (e && e.streams) {
if (this.state.onlineType === 1) {
let remoteVideo = document.getElementById("remoteVideo");
@@ -402,12 +231,12 @@ class Panel extends React.Component {
*/
dealWebRtcMessage = (messagePB) => {
const { type, sdp, iceCandidate } = JSON.parse(messagePB.content);
-
+ console.log(type)
if (type === "answer") {
const offerSdp = new RTCSessionDescription({ type, sdp });
- peer.setRemoteDescription(offerSdp)
+ this.props.peer.localPeer.setRemoteDescription(offerSdp)
} else if (type === "answer_ice") {
- peer.addIceCandidate(iceCandidate)
+ this.props.peer.localPeer.addIceCandidate(iceCandidate)
} else if (type === "offer_ice") {
peer.addIceCandidate(iceCandidate)
} else if (type === "offer") {
@@ -446,17 +275,13 @@ class Panel extends React.Component {
.then(() => {
peer.createAnswer().then(answer => {
peer.setLocalDescription(answer)
- let data = {
- fromUsername: localStorage.username,
- from: this.state.fromUser,
- to: this.state.toUser,
- messageType: messagePB.contentType,
+
+ let message = {
content: JSON.stringify(answer),
type: Constant.MESSAGE_TRANS_TYPE,
+ messageType: messagePB.contentType
}
- let message = protobuf.lookup("protocol.Message")
- const messagePBNew = message.create(data)
- socket.send(message.encode(messagePBNew).finish())
+ this.sendMessage(message);
})
});
});
@@ -498,264 +323,21 @@ class Panel extends React.Component {
}
/**
- * 获取好友列表
- */
- fetchUserList = () => {
- this.setState({
- menuType: 1,
- })
- let data = {
- uuid: this.state.fromUser
- }
- axiosGet(Params.USER_LIST_URL, data)
- .then(response => {
- let users = response.data
- let data = []
- for (var index in users) {
- let d = {
- username: users[index].username,
- uuid: users[index].uuid,
- messageType: 1,
- avatar: Params.HOST + "/file/" + users[index].avatar,
- }
- data.push(d)
- }
-
- this.setState({
- data: data
- })
- })
- }
-
- /**
- * 获取群组列表
- */
- fetchGroupList = () => {
- this.setState({
- menuType: 2,
- })
- let data = {
- uuid: this.state.fromUser
- }
- axiosGet(Params.GROUP_LIST_URL + "/" + this.state.fromUser, data)
- .then(response => {
- let users = response.data
- let data = []
- for (var index in users) {
- let d = {
- username: users[index].name,
- uuid: users[index].uuid,
- messageType: 2,
- }
- data.push(d)
- }
-
- this.setState({
- data: data
- })
- })
- }
-
- /**
- * 发送消息或者接受消息后,滚动到最后
- */
- scrollToBottom = () => {
- let div = document.getElementById("scrollableDiv")
- div.scrollTop = div.scrollHeight
- }
-
- /**
- * 发送消息
- * @returns
- */
- handleSubmit = () => {
- if (!this.state.value) {
- return;
- }
-
- this.setState({
- submitting: true,
- });
-
+ * 发送消息
+ * @param {消息内容} messageData
+ */
+ sendMessage = (messageData) => {
let data = {
+ ...messageData,
+ messageType: this.props.chooseUser.messageType, // 消息类型,1.单聊 2.群聊
fromUsername: localStorage.username,
- from: this.state.fromUser,
- to: this.state.toUser,
- messageType: this.state.messageType,
- content: this.state.value,
- contentType: 1,
+ from: localStorage.uuid,
+ to: this.props.chooseUser.toUser,
}
let message = protobuf.lookup("protocol.Message")
const messagePB = message.create(data)
socket.send(message.encode(messagePB).finish())
-
- this.setState({
- submitting: false,
- value: '',
- comments: [
- ...this.state.comments,
- {
- author: localStorage.username,
- avatar: this.state.user.avatar,
- content: {this.state.value}
,
- datetime: moment().fromNow(),
- },
- ],
- }, () => {
- this.scrollToBottom()
- })
- };
-
- /**
- * 每次输入框输入后,将值存放在state中
- * @param {事件} e
- */
- handleChange = e => {
- this.setState({
- value: e.target.value,
- });
- };
-
- /**
- * 切换用户聊天,获取用户的基本信息
- * @param {事件} e
- */
- userChange = (e) => {
- this.setState({
- toUser: e.target.value
- })
- }
-
- /**
- * 选择用户,获取对应的消息
- * @param {选择的用户} value
- */
- chooseUser = (value) => {
- this.setState({
- toUser: value.uuid,
- toUsername: value.username,
- messageType: value.messageType,
- avatar: value.avatar
- }, () => {
- this.fetchMessages()
- })
- }
-
- /**
- * 搜索用户
- * @param {*} value
- * @param {*} _event
- * @returns
- */
- searchUser = (value, _event) => {
- if (null === value || "" === value) {
- return
- }
-
- let data = {
- name: value
- }
- axiosGet(Params.USER_NAME_URL, data)
- .then(response => {
- let data = response.data
- if (data.user.username === "" && data.group.name === "") {
- message.error("未查找到群或者用户")
- return
- }
- let queryUser = {
- username: data.user.username,
- nickname: data.user.nickname,
-
- groupUuid: data.group.uuid,
- groupName: data.group.name,
- }
- this.setState({
- hasUser: true,
- queryUser: queryUser
- });
- });
- }
-
- showModal = () => {
- this.setState({
- hasUser: true
- });
- };
-
- handleOk = () => {
- let data = {
- uuid: this.state.fromUser,
- friendUsername: this.state.queryUser.username
- }
- axiosPostBody(Params.USER_FRIEND_URL, data)
- .then(_response => {
- message.success("添加成功")
- this.fetchUserList()
- this.setState({
- hasUser: false
- });
- });
- };
-
- 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
- });
- };
-
- /**
- * 获取消息
- */
- fetchMessages = () => {
- let uuid = this.state.fromUser
- if (this.state.messageType === 2) {
- uuid = this.state.toUser
- }
- let data = {
- Uuid: uuid,
- FriendUsername: this.state.toUsername,
- MessageType: this.state.messageType
- }
- axiosGet(Params.MESSAGE_URL, data)
- .then(response => {
- let comments = []
- let data = response.data
- if (null == data) {
- data = []
- }
- for (var i = 0; i < data.length; i++) {
- let contentType = data[i].contentType
- let content = this.getContentByType(contentType, data[i].url, data[i].content)
-
- let comment = {
- author: data[i].fromUsername,
- avatar: Params.HOST + "/file/" + data[i].avatar,
- content: {content}
,
- datetime: moment(data[i].createAt).fromNow(),
- }
- comments.push(comment)
- }
-
- this.setState({
- comments: comments
- }, () => {
- this.scrollToBottom()
- setTimeout(this.scrollToBottom(), 5000)
- })
- });
}
/**
@@ -778,322 +360,6 @@ class Panel extends React.Component {
return content;
}
- /**
- * 获取群聊信息,群成员列表
- */
- chatDetails = () => {
- axiosGet(Params.GROUP_USER_URL + this.state.toUser)
- .then(response => {
- if (null == response.data) {
- return;
- }
- this.setState({
- drawerVisible: true,
- groupUsers: response.data
- })
- });
-
- }
- drawerOnClose = () => {
- this.setState({
- drawerVisible: false,
- })
- }
-
- /**
- * 上传文件
- * @param {事件} e
- * @returns
- */
- uploadFile = (e) => {
- let files = e.target.files
- 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) => {
- let file = event.target.result
- // Uint8数组可以直观的看到ArrayBuffer中每个字节(1字节 == 8位)的值。一般我们要将ArrayBuffer转成Uint类型数组后才能对其中的字节进行存取操作。
- // 上传文件必须转换为Uint8Array
- var u8 = new Uint8Array(file);
- let message = protobuf.lookup("protocol.Message")
-
- let data = {
- fromUsername: localStorage.username,
- from: this.state.fromUser,
- to: this.state.toUser,
- 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()
- })
- }
-
- /**
- * 开始录制音频
- */
- audiorecorder = null;
- hasAudioPermission = true;
- startAudio = () => {
- this.setState({
- isRecord: true
- })
- this.audiorecorder = new Recorder()
- this.hasAudioPermission = true;
- this.audiorecorder
- .start()
- .then(() => {
- console.log("start audio...")
- }, (_error) => {
- this.hasAudioPermission = false;
- message.error("录音权限获取失败!")
- })
- }
-
- /**
- * 停止录制音频
- */
- stopAudio = () => {
- this.setState({
- isRecord: false
- })
- if (!this.hasAudioPermission) {
- return;
- }
- let blob = this.audiorecorder.getWAVBlob();
- this.audiorecorder.stop()
- this.audiorecorder.destroy()
- .then(() => {
- this.audiorecorder = null;
- });
- this.audiorecorder = null;
-
- let reader = new FileReader()
- reader.readAsArrayBuffer(blob)
-
- reader.onload = ((e) => {
- let imgData = 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: 3,
- fileSuffix: "wav",
- file: new Uint8Array(imgData)
- }
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
- })
-
- this.setState({
- comments: [
- ...this.state.comments,
- {
- author: localStorage.username,
- avatar: this.state.user.avatar,
- content: ,
- datetime: moment().fromNow(),
- },
- ],
- }, () => {
- this.scrollToBottom()
- })
- }
-
-
- /**
- * 当按下按钮时录制视频
- */
- dataChunks = [];
- recorder = null;
- hasVideoPermission = true;
- startVideoRecord = (e) => {
- this.hasVideoPermission = true;
- navigator.getUserMedia = navigator.getUserMedia ||
- navigator.webkitGetUserMedia ||
- navigator.mozGetUserMedia ||
- navigator.msGetUserMedia; //获取媒体对象(这里指摄像头)
- if (!this.checkMediaPermisssion()) {
- this.hasVideoPermission = false;
- return;
- }
-
- let preview = document.getElementById("preview");
- this.setState({
- isRecord: true
- })
-
- navigator.mediaDevices
- .getUserMedia({
- audio: true,
- video: true,
- }).then((stream) => {
- preview.srcObject = stream;
- this.recorder = new MediaRecorder(stream);
-
- this.recorder.ondataavailable = (event) => {
- let data = event.data;
- this.dataChunks.push(data);
- };
- this.recorder.start(1000);
- });
- }
-
- /**
- * 松开按钮发送视频到服务器
- * @param {事件} e
- */
- stopVideoRecord = (e) => {
- this.setState({
- isRecord: false
- })
- if (!this.hasVideoPermission) {
- return;
- }
-
- let recordedBlob = new Blob(this.dataChunks, { type: "video/webm" });
-
- let reader = new FileReader()
- reader.readAsArrayBuffer(recordedBlob)
-
- 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: 3,
- fileSuffix: "webm",
- file: new Uint8Array(fileData)
- }
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
- })
-
- this.setState({
- comments: [
- ...this.state.comments,
- {
- author: localStorage.username,
- avatar: this.state.user.avatar,
- content: ,
- datetime: moment().fromNow(),
- },
- ],
- }, () => {
- this.scrollToBottom()
- })
- if (this.recorder) {
- this.recorder.stop()
- this.recorder = null
- }
- let preview = document.getElementById("preview");
- if (preview.srcObject && preview.srcObject.getTracks()) {
- preview.srcObject.getTracks().forEach((track) => track.stop());
- }
- this.dataChunks = []
- }
-
- interval = null;
- /**
- * 开启视频电话
- */
- startVideoOnline = () => {
- if (!this.checkMediaPermisssion()) {
- return;
- }
-
- let preview = document.getElementById("preview1");
- this.setState({
- onlineType: 1,
- isRecord: true,
- rtcType: 'offer'
- })
-
- navigator.mediaDevices
- .getUserMedia({
- audio: true,
- video: true,
- }).then((stream) => {
- preview.srcObject = stream;
- stream.getTracks().forEach(track => {
- peer.addTrack(track, stream);
- });
-
- // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行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,
- contentType: Constant.VIDEO_ONLINE,
- content: JSON.stringify(offer),
- type: Constant.MESSAGE_TRANS_TYPE,
- }
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
- });
- });
-
- this.setState({
- mediaPanelDrawerVisible: true
- })
- }
-
/**
* 停止视频电话,屏幕共享
*/
@@ -1101,10 +367,7 @@ class Panel extends React.Component {
this.setState({
isRecord: false
})
- if (this.recorder) {
- this.recorder.stop()
- this.recorder = null
- }
+
let preview1 = document.getElementById("preview1");
if (preview1 && preview1.srcObject && preview1.srcObject.getTracks()) {
preview1.srcObject.getTracks().forEach((track) => track.stop());
@@ -1121,10 +384,6 @@ class Panel extends React.Component {
}
this.dataChunks = []
- if (this.interval) {
- clearInterval(this.interval)
- }
-
// 停止视频或者屏幕共享时,将画布最小
let currentScreen = {
width: 0,
@@ -1135,320 +394,37 @@ class Panel extends React.Component {
})
}
- /**
- * 开启语音电话
- */
- startAudioOnline = () => {
- if (!this.checkMediaPermisssion()) {
- return;
- }
-
- this.setState({
- onlineType: 2,
- isRecord: true,
- rtcType: 'offer'
- })
-
- navigator.mediaDevices
- .getUserMedia({
- audio: true,
- video: false,
- }).then((stream) => {
- stream.getTracks().forEach(track => {
- peer.addTrack(track, stream);
- });
-
- // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行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, // 消息类型,1.单聊 2.群聊
- contentType: Constant.AUDIO_ONLINE, // 消息内容类型
- content: JSON.stringify(offer),
- type: Constant.MESSAGE_TRANS_TYPE, // 消息传输类型
- }
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
- });
- });
-
- this.setState({
- mediaPanelDrawerVisible: true
- })
- }
-
- /**
- * 屏幕共享
- */
- startShareOnline = () => {
- navigator.getUserMedia = navigator.getUserMedia ||
- navigator.webkitGetUserMedia ||
- navigator.mozGetUserMedia ||
- navigator.msGetUserMedia; //获取媒体对象(这里指摄像头)
- if (!this.checkMediaPermisssion()) {
- return;
- }
-
- let preview = document.getElementById("preview1");
- this.setState({
- isRecord: true,
- mediaPanelDrawerVisible: true
- })
-
- navigator.mediaDevices
- .getDisplayMedia({
- video: true,
- }).then((stream) => {
- preview.srcObject = stream;
- });
-
-
- var canvas = document.getElementById("canvas");
- var ctx = canvas.getContext('2d');
- this.interval = window.setInterval(() => {
- let width = this.state.share.width
- let height = this.state.share.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: 9,
- }
- let message = protobuf.lookup("protocol.Message")
- const messagePB = message.create(data)
- socket.send(message.encode(messagePB).finish())
- }, 60);
- }
-
- /**
- * 隐藏真正的文件上传控件,通过按钮模拟点击文件上传控件
- */
- clickFile = () => {
- let file = document.getElementById("file")
- file.click();
- }
-
/**
* 显示视频或者音频的面板
*/
mediaPanelDrawerOnClose = () => {
- this.setState({
- mediaPanelDrawerVisible: false,
- })
- }
- showMediaPanel = () => {
- this.setState({
- mediaPanelDrawerVisible: true,
- })
+ let media = {
+ ...this.props.media,
+ showMediaPanel: false,
+ }
+ this.props.setMedia(media)
}
render() {
- const { comments, submitting, value, toUser } = this.state;
return (
<>
-
-
-
-
- } size="large" type='link' disabled={this.state.menuType === 1} onClick={this.fetchUserList}>
-
-
-
- } size="large" type='link' disabled={this.state.menuType === 2}>
-
-
+
-
-
-
- (
-
- this.chooseUser(item)}
- avatar={}
- title={item.username}
- description=""
- />
-
- )}
- />
+
-
- }>
-
-
-
- {comments.length > 0 && }
-
-
-
-
-
-
-
-
- }
- disabled={toUser === ''}
- />
-
-
-
- }
- disabled={toUser === ''}
- />
-
-
-
- } title="视频">
- }
- disabled={toUser === ''}
- />
-
-
-
-
-
- }
- disabled={toUser === ''}
- />
-
-
- } disabled={toUser === ''}
- />
-
-
- } disabled={toUser === ''}
- />
-
-
- }
- />
-
-
- }
- />
-
-
-
-
} color="processing" hidden={!this.state.isRecord}>
- 录制中
-
-
-
- }
- />
-
+
-
- 用户名:{this.state.queryUser.username}
- 昵称:{this.state.queryUser.nickname}
-
-
-
- 群信息:{this.state.queryUser.groupName}
-
-
-
-
- (
-
- }
- title={item.username}
- description=""
- />
-
- )}
- />
-
-
+
+
+
+ }
+ size="large"
+ type='link'
+ disabled={menuType === 2}
+ style={{color: menuType === 2 ? '#1890ff' : 'gray'}}
+ >
+
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ user: state.userInfoReducer.user,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setUserList: (data) => dispatch(actions.setUserList(data)),
+ }
+}
+
+SwitchChat = connect(mapStateToProps, mapDispatchToProps)(SwitchChat)
+
+export default SwitchChat
\ No newline at end of file
diff --git a/src/chat/component/UserInfo.jsx b/src/chat/panel/left/component/UserInfo.jsx
similarity index 86%
rename from src/chat/component/UserInfo.jsx
rename to src/chat/panel/left/component/UserInfo.jsx
index 4819714..521da9b 100644
--- a/src/chat/component/UserInfo.jsx
+++ b/src/chat/panel/left/component/UserInfo.jsx
@@ -11,8 +11,9 @@ import {
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'
+import { actions } from '../../../redux/module/userInfo'
+import * as Params from '../../../common/param/Params'
+import { axiosGet } from '../../../util/Request';
function getBase64(img, callback) {
const reader = new FileReader();
@@ -48,7 +49,21 @@ class UserInfo extends React.Component {
}
componentDidMount() {
+ this.fetchUserDetails();
+ }
+ /**
+ * 获取用户详情
+ */
+ fetchUserDetails = () => {
+ axiosGet(Params.USER_URL + localStorage.uuid)
+ .then(response => {
+ let user = {
+ ...response.data,
+ avatar: Params.HOST + "/file/" + response.data.avatar
+ }
+ this.props.setUser(user)
+ });
}
modifyAvatar = () => {
@@ -77,7 +92,7 @@ class UserInfo extends React.Component {
if (response.code !== 0) {
message.error(info.file.response.msg)
}
-
+
let user = {
...this.props.user,
avatar: Params.HOST + "/file/" + info.file.response.data
@@ -131,7 +146,7 @@ class UserInfo extends React.Component {
action={Params.FILE_URL}
beforeUpload={beforeUpload}
onChange={this.handleChange}
- data={{uuid: this.props.user.uuid}}
+ data={{ uuid: this.props.user.uuid }}
>
{imageUrl ?
: uploadButton}
diff --git a/src/chat/panel/left/index.jsx b/src/chat/panel/left/index.jsx
new file mode 100644
index 0000000..e13531e
--- /dev/null
+++ b/src/chat/panel/left/index.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import SwitchChat from './component/SwitchChat'
+import UserInfo from './component/UserInfo'
+
+export default class LeftIndex extends React.Component {
+
+ render() {
+
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/src/chat/panel/right/component/ChatAudio.jsx b/src/chat/panel/right/component/ChatAudio.jsx
new file mode 100644
index 0000000..08334bc
--- /dev/null
+++ b/src/chat/panel/right/component/ChatAudio.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import {
+ Tooltip,
+ Button,
+ message
+} from 'antd';
+
+import {
+ AudioOutlined,
+} from '@ant-design/icons';
+
+import Recorder from 'js-audio-recorder';
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+
+
+class ChatAudio extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+
+ }
+ }
+
+ componentDidMount() {
+
+ }
+ /**
+ * 开始录制音频
+ */
+ audiorecorder = null;
+ hasAudioPermission = true;
+ startAudio = () => {
+ let media = {
+ isRecord: true
+ }
+ this.props.setMedia(media);
+
+ this.audiorecorder = new Recorder()
+ this.hasAudioPermission = true;
+ this.audiorecorder
+ .start()
+ .then(() => {
+ console.log("start audio...")
+ }, (_error) => {
+ this.hasAudioPermission = false;
+ message.error("录音权限获取失败!")
+ })
+ }
+
+ /**
+ * 停止录制音频
+ */
+ stopAudio = () => {
+ let media = {
+ isRecord: false
+ }
+ this.props.setMedia(media);
+
+ if (!this.hasAudioPermission) {
+ return;
+ }
+ let blob = this.audiorecorder.getWAVBlob();
+ this.audiorecorder.stop()
+ this.audiorecorder.destroy()
+ .then(() => {
+ this.audiorecorder = null;
+ });
+ this.audiorecorder = null;
+
+ let reader = new FileReader()
+ reader.readAsArrayBuffer(blob)
+
+ reader.onload = ((e) => {
+ let imgData = e.target.result
+
+ // 上传文件必须将ArrayBuffer转换为Uint8Array
+ let data = {
+ content: this.state.value,
+ contentType: 3,
+ fileSuffix: "wav",
+ file: new Uint8Array(imgData)
+ }
+ this.props.sendMessage(data)
+ })
+
+ this.props.appendMessage();
+ }
+
+ render() {
+ const { chooseUser } = this.props;
+ return (
+ <>
+
+ }
+ disabled={chooseUser.toUser === ''}
+ />
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ chooseUser: state.panelReducer.chooseUser,
+ socket: state.panelReducer.socket,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setMedia: (data) => dispatch(actions.setMedia(data)),
+ }
+}
+
+ChatAudio = connect(mapStateToProps, mapDispatchToProps)(ChatAudio)
+
+export default ChatAudio
\ No newline at end of file
diff --git a/src/chat/panel/right/component/ChatAudioOline.jsx b/src/chat/panel/right/component/ChatAudioOline.jsx
new file mode 100644
index 0000000..b150883
--- /dev/null
+++ b/src/chat/panel/right/component/ChatAudioOline.jsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import {
+ Tooltip,
+ Button,
+ Drawer
+} from 'antd';
+
+import {
+ PhoneOutlined,
+ PoweroffOutlined
+} from '@ant-design/icons';
+
+import * as Constant from '../../../common/constant/Constant'
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+
+let localPeer = null;
+class ChatAudioOline extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+
+ }
+ }
+
+ componentDidMount() {
+ localPeer = new RTCPeerConnection();
+ let peer = {
+ ...this.props.peer,
+ localPeer: localPeer
+ }
+ this.props.setPeer(peer);
+ }
+ /**
+ * 开启语音电话
+ */
+ startAudioOnline = () => {
+ if (!this.props.checkMediaPermisssion()) {
+ return;
+ }
+
+ this.webrtcConnection();
+ console.log(this.props.peer)
+ this.setState({
+ onlineType: 2,
+ rtcType: 'offer'
+ })
+
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ video: false,
+ }).then((stream) => {
+ stream.getTracks().forEach(track => {
+ this.props.peer.localPeer.addTrack(track, stream);
+ });
+
+ // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行offer创建。不然不能获取到流,从而不能播放视频。
+ this.props.peer.localPeer.createOffer()
+ .then(offer => {
+ this.props.peer.localPeer.setLocalDescription(offer);
+ let data = {
+ contentType: Constant.AUDIO_ONLINE, // 消息内容类型
+ content: JSON.stringify(offer),
+ type: Constant.MESSAGE_TRANS_TYPE, // 消息传输类型
+ }
+ this.props.sendMessage(data);
+ });
+ });
+
+ this.setState({
+ mediaPanelDrawerVisible: true
+ })
+ }
+
+ /**
+ * webrtc 绑定事件
+ */
+ webrtcConnection = () => {
+
+
+ /**
+ * 对等方收到ice信息后,通过调用 addIceCandidate 将接收的候选者信息传递给浏览器的ICE代理。
+ * @param {候选人信息} e
+ */
+ localPeer.onicecandidate = (e) => {
+ console.log(e)
+ if (e.candidate) {
+ // rtcType参数默认是对端值为answer,如果是发起端,会将值设置为offer
+ let candidate = {
+ type: 'offer_ice',
+ iceCandidate: e.candidate
+ }
+ let message = {
+ content: JSON.stringify(candidate),
+ type: Constant.MESSAGE_TRANS_TYPE,
+ }
+ this.props.sendMessage(message);
+ }
+
+ };
+
+ /**
+ * 当连接成功后,从里面获取语音视频流
+ * @param {包含语音视频流} e
+ */
+ localPeer.ontrack = (e) => {
+ if (e && e.streams) {
+ if (this.state.onlineType === 1) {
+ let remoteVideo = document.getElementById("remoteVideo");
+ remoteVideo.srcObject = e.streams[0];
+ } else {
+ let remoteAudio = document.getElementById("audioPhone");
+ remoteAudio.srcObject = e.streams[0];
+ }
+ }
+ };
+ }
+
+ /**
+ * 停止视频电话,屏幕共享
+ */
+ stopVideoOnline = () => {
+ let audioPhone = document.getElementById("audioPhone");
+ if (audioPhone && audioPhone.srcObject && audioPhone.srcObject.getTracks()) {
+ audioPhone.srcObject.getTracks().forEach((track) => track.stop());
+ }
+ }
+
+ render() {
+ const { chooseUser } = this.props;
+ return (
+ <>
+
+ }
+ disabled={chooseUser.toUser === ''}
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ chooseUser: state.panelReducer.chooseUser,
+ socket: state.panelReducer.socket,
+ peer: state.panelReducer.peer,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setMedia: (data) => dispatch(actions.setMedia(data)),
+ setPeer: (data) => dispatch(actions.setPeer(data)),
+ }
+}
+
+ChatAudioOline = connect(mapStateToProps, mapDispatchToProps)(ChatAudioOline)
+
+export default ChatAudioOline
\ No newline at end of file
diff --git a/src/chat/panel/right/component/ChatDetails.jsx b/src/chat/panel/right/component/ChatDetails.jsx
new file mode 100644
index 0000000..c790e5d
--- /dev/null
+++ b/src/chat/panel/right/component/ChatDetails.jsx
@@ -0,0 +1,156 @@
+import React from 'react';
+import {
+ Avatar,
+ Drawer,
+ List,
+ Badge,
+ Card,
+ Comment
+} from 'antd';
+
+import {
+ MoreOutlined,
+} from '@ant-design/icons';
+
+import InfiniteScroll from 'react-infinite-scroll-component';
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+import * as Params from '../../../common/param/Params'
+import { axiosGet } from '../../../util/Request';
+
+const CommentList = ({ comments }) => (
+
+ }
+ />
+
+);
+
+class ChatDetails extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ groupUsers: [],
+ drawerVisible: false,
+ messageList: []
+ }
+ }
+
+ static getDerivedStateFromProps(nextProps, preState) {
+ if (nextProps.messageList !== preState.messageList) {
+ return {
+ ...preState,
+ messageList: nextProps.messageList,
+ }
+ }
+ return null;
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.messageList !== this.state.messageList) {
+ this.scrollToBottom();
+ }
+ }
+
+ componentDidMount() {
+
+ }
+
+ /**
+ * 发送消息或者接受消息后,滚动到最后
+ */
+ scrollToBottom = () => {
+ let div = document.getElementById("scrollableDiv")
+ div.scrollTop = div.scrollHeight
+ }
+
+ /**
+ * 获取群聊信息,群成员列表
+ */
+ chatDetails = () => {
+ axiosGet(Params.GROUP_USER_URL + this.props.chooseUser.toUser)
+ .then(response => {
+ if (null == response.data) {
+ return;
+ }
+ this.setState({
+ drawerVisible: true,
+ groupUsers: response.data
+ })
+ });
+
+ }
+
+ drawerOnClose = () => {
+ this.setState({
+ drawerVisible: false,
+ })
+ }
+
+
+ render() {
+
+ return (
+ <>
+
+ }>
+
+
+
+ {this.props.messageList.length > 0 && }
+
+
+
+
+
+
+ (
+
+ }
+ title={item.username}
+ description=""
+ />
+
+ )}
+ />
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ chooseUser: state.panelReducer.chooseUser,
+ messageList: state.panelReducer.messageList,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setUser: (data) => dispatch(actions.setUser(data)),
+ }
+}
+
+ChatDetails = connect(mapStateToProps, mapDispatchToProps)(ChatDetails)
+
+export default ChatDetails
\ No newline at end of file
diff --git a/src/chat/panel/right/component/ChatEdit.jsx b/src/chat/panel/right/component/ChatEdit.jsx
new file mode 100644
index 0000000..2103e1a
--- /dev/null
+++ b/src/chat/panel/right/component/ChatEdit.jsx
@@ -0,0 +1,153 @@
+import React from 'react';
+import {
+ Form,
+ Input,
+ Button,
+ Comment
+} from 'antd';
+
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+
+const { TextArea } = Input;
+
+const Editor = ({ onChange, onSubmit, submitting, value, toUser }) => (
+ <>
+
+
+
+
+
+ Send
+
+
+ >
+);
+
+
+class ChatEdit extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ submitting: false,
+ value: '',
+ }
+ }
+
+ componentDidMount() {
+ this.bindParse();
+ }
+
+ /**
+ * 解析剪切板的文件
+ */
+ bindParse = () => {
+ document.getElementById("messageArea").addEventListener("paste", (e) => {
+ var data = e.clipboardData
+ if (!data.items) {
+ return;
+ }
+ var items = data.items
+
+ if (null == items || items.length <= 0) {
+ return;
+ }
+
+ let item = items[0]
+ if (item.kind !== 'file') {
+ return;
+ }
+ let blob = item.getAsFile()
+
+ let reader = new FileReader()
+ reader.readAsArrayBuffer(blob)
+
+ reader.onload = ((e) => {
+ let imgData = e.target.result
+
+ // 上传文件必须将ArrayBuffer转换为Uint8Array
+ let data = {
+ content: this.state.value,
+ contentType: 3,
+ file: new Uint8Array(imgData)
+ }
+ this.props.sendMessage(data)
+
+ this.props.appendImgToPanel(imgData)
+ })
+
+ }, false)
+ }
+ /**
+ * 每次输入框输入后,将值存放在state中
+ * @param {事件} e
+ */
+ handleChange = e => {
+ this.setState({
+ value: e.target.value,
+ });
+ };
+
+ /**
+ * 发送消息
+ * @returns
+ */
+ handleSubmit = () => {
+ if (!this.state.value) {
+ return;
+ }
+
+ let message = {
+ content: this.state.value,
+ contentType: 1,
+ }
+
+ this.props.sendMessage(message)
+
+ this.props.appendMessage(this.state.value);
+ this.setState({
+ submitting: false,
+ value: '',
+ });
+
+ };
+
+ render() {
+ const { submitting, value } = this.state;
+ const { toUser } = this.props.chooseUser;
+ return (
+ <>
+
+
+ }
+ />
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ chooseUser: state.panelReducer.chooseUser,
+ messageList: state.panelReducer.messageList,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setUser: (data) => dispatch(actions.setUser(data)),
+ }
+}
+
+ChatEdit = connect(mapStateToProps, mapDispatchToProps)(ChatEdit)
+
+export default ChatEdit
\ No newline at end of file
diff --git a/src/chat/panel/right/component/ChatFile.jsx b/src/chat/panel/right/component/ChatFile.jsx
new file mode 100644
index 0000000..e75081a
--- /dev/null
+++ b/src/chat/panel/right/component/ChatFile.jsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import {
+ Tooltip,
+ Button,
+ message
+} from 'antd';
+
+import {
+ FileAddOutlined,
+ FileOutlined
+} from '@ant-design/icons';
+
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+
+
+class ChatFile extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+
+ }
+ }
+
+ componentDidMount() {
+
+ }
+
+ /**
+ * 隐藏真正的文件上传控件,通过按钮模拟点击文件上传控件
+ */
+ clickFile = () => {
+ let file = document.getElementById("file")
+ file.click();
+ }
+
+ /**
+ * 上传文件
+ * @param {事件} e
+ * @returns
+ */
+ uploadFile = (e) => {
+ let files = e.target.files
+ 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) => {
+ let file = event.target.result
+ // Uint8数组可以直观的看到ArrayBuffer中每个字节(1字节 == 8位)的值。一般我们要将ArrayBuffer转成Uint类型数组后才能对其中的字节进行存取操作。
+ // 上传文件必须转换为Uint8Array
+ var u8 = new Uint8Array(file);
+
+ let data = {
+ content: this.state.value,
+ contentType: 3,
+ fileSuffix: fileSuffix,
+ file: u8
+ }
+ this.props.sendMessage(data)
+
+ if (["jpeg", "jpg", "png", "gif", "tif", "bmp", "dwg"].indexOf(fileSuffix) !== -1) {
+ this.props.appendImgToPanel(file)
+ } else {
+ this.props.appendMessage()
+ }
+
+ })
+ reader.readAsArrayBuffer(files[0])
+ }
+
+ render() {
+ const { chooseUser } = this.props;
+ return (
+ <>
+
+
+ }
+ disabled={chooseUser.toUser === ''}
+ />
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ user: state.userInfoReducer.user,
+ chooseUser: state.panelReducer.chooseUser,
+ messageList: state.panelReducer.messageList,
+ socket: state.panelReducer.socket,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setMessageList: (data) => dispatch(actions.setMessageList(data)),
+ }
+}
+
+ChatFile = connect(mapStateToProps, mapDispatchToProps)(ChatFile)
+
+export default ChatFile
\ No newline at end of file
diff --git a/src/chat/panel/right/component/ChatShareScreen.jsx b/src/chat/panel/right/component/ChatShareScreen.jsx
new file mode 100644
index 0000000..f479b22
--- /dev/null
+++ b/src/chat/panel/right/component/ChatShareScreen.jsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import {
+ Tooltip,
+ Button,
+ Drawer
+} from 'antd';
+
+import {
+ DesktopOutlined,
+ PoweroffOutlined
+} from '@ant-design/icons';
+
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+
+
+class ChatShareScreen extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ mediaPanelDrawerVisible: false,
+ share: {
+ height: 540,
+ width: 750
+ },
+ currentScreen: {
+ height: 0,
+ width: 0
+ },
+ }
+ }
+
+ componentDidMount() {
+
+ }
+ /**
+ * 屏幕共享
+ */
+ startShareOnline = () => {
+ navigator.getUserMedia = navigator.getUserMedia ||
+ navigator.webkitGetUserMedia ||
+ navigator.mozGetUserMedia ||
+ navigator.msGetUserMedia; //获取媒体对象(这里指摄像头)
+ if (!this.props.checkMediaPermisssion()) {
+ return;
+ }
+
+ let media = {
+ isRecord: false
+ }
+ this.props.setMedia(media);
+
+ let preview = document.getElementById("preview");
+ this.setState({
+ mediaPanelDrawerVisible: true
+ })
+
+ navigator.mediaDevices
+ .getDisplayMedia({
+ video: true,
+ }).then((stream) => {
+ preview.srcObject = stream;
+ });
+
+
+ var canvas = document.getElementById("canvas");
+ var ctx = canvas.getContext('2d');
+ this.interval = window.setInterval(() => {
+ let width = this.state.share.width
+ let height = this.state.share.height
+ let currentScreen = {
+ width: width,
+ height: height
+ }
+ this.setState({
+ currentScreen: currentScreen
+ })
+ ctx.drawImage(preview, 0, 0, width, height);
+ let data = {
+ content: canvas.toDataURL("image/jpeg", 0.5),
+ contentType: 9,
+ }
+ this.props.sendMessage(data);
+ }, 60);
+ }
+
+ /**
+ * 停止视频电话,屏幕共享
+ */
+ stopVideoOnline = () => {
+ this.props.setMedia({isRecord: false});
+ let preview1 = document.getElementById("preview");
+ if (preview1 && preview1.srcObject && preview1.srcObject.getTracks()) {
+ preview1.srcObject.getTracks().forEach((track) => track.stop());
+ }
+
+ // 停止视频或者屏幕共享时,将画布最小
+ let currentScreen = {
+ width: 0,
+ height: 0
+ }
+ this.setState({
+ currentScreen: currentScreen
+ })
+ }
+
+ /**
+ * 显示视频或者音频的面板
+ */
+ mediaPanelDrawerOnClose = () => {
+ this.setState({
+ mediaPanelDrawerVisible: false,
+ })
+ }
+
+ render() {
+ const { chooseUser } = this.props;
+ return (
+ <>
+
+ } disabled={chooseUser.toUser === ''}
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ chooseUser: state.panelReducer.chooseUser,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setMedia: (data) => dispatch(actions.setMedia(data)),
+ }
+}
+
+ChatShareScreen = connect(mapStateToProps, mapDispatchToProps)(ChatShareScreen)
+
+export default ChatShareScreen
\ No newline at end of file
diff --git a/src/chat/panel/right/component/ChatVideo.jsx b/src/chat/panel/right/component/ChatVideo.jsx
new file mode 100644
index 0000000..a89ceb3
--- /dev/null
+++ b/src/chat/panel/right/component/ChatVideo.jsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import {
+ Tooltip,
+ Button,
+ Popover
+} from 'antd';
+
+import {
+ VideoCameraAddOutlined,
+} from '@ant-design/icons';
+
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+
+
+class ChatVideo extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+
+ }
+ }
+
+ componentDidMount() {
+
+ }
+ /**
+ * 当按下按钮时录制视频
+ */
+ dataChunks = [];
+ recorder = null;
+ hasVideoPermission = true;
+ startVideoRecord = (e) => {
+ this.hasVideoPermission = true;
+ navigator.getUserMedia = navigator.getUserMedia ||
+ navigator.webkitGetUserMedia ||
+ navigator.mozGetUserMedia ||
+ navigator.msGetUserMedia; //获取媒体对象(这里指摄像头)
+ if (!this.props.checkMediaPermisssion()) {
+ this.hasVideoPermission = false;
+ return;
+ }
+
+ let preview = document.getElementById("preview");
+ let media = {
+ isRecord: true
+ }
+ this.props.setMedia(media);
+
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ video: true,
+ }).then((stream) => {
+ preview.srcObject = stream;
+ this.recorder = new MediaRecorder(stream);
+
+ this.recorder.ondataavailable = (event) => {
+ let data = event.data;
+ this.dataChunks.push(data);
+ };
+ this.recorder.start(1000);
+ });
+ }
+
+ /**
+ * 松开按钮发送视频到服务器
+ * @param {事件} e
+ */
+ stopVideoRecord = (e) => {
+ let media = {
+ isRecord: false
+ }
+ this.props.setMedia(media);
+ if (!this.hasVideoPermission) {
+ return;
+ }
+
+ let recordedBlob = new Blob(this.dataChunks, { type: "video/webm" });
+
+ let reader = new FileReader()
+ reader.readAsArrayBuffer(recordedBlob)
+
+ reader.onload = ((e) => {
+ let fileData = e.target.result
+
+ // 上传文件必须将ArrayBuffer转换为Uint8Array
+ let data = {
+ content: this.state.value,
+ contentType: 3,
+ fileSuffix: "webm",
+ file: new Uint8Array(fileData)
+ }
+ this.props.sendMessage(data)
+ })
+
+ this.props.appendMessage();
+
+ if (this.recorder) {
+ this.recorder.stop()
+ this.recorder = null
+ }
+ let preview = document.getElementById("preview");
+ if (preview.srcObject && preview.srcObject.getTracks()) {
+ preview.srcObject.getTracks().forEach((track) => track.stop());
+ }
+ this.dataChunks = []
+ }
+
+ render() {
+ const { chooseUser } = this.props;
+ return (
+ <>
+
+ } title="视频">
+ }
+ disabled={chooseUser.toUser === ''}
+ />
+
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ chooseUser: state.panelReducer.chooseUser,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setMedia: (data) => dispatch(actions.setMedia(data)),
+ }
+}
+
+ChatVideo = connect(mapStateToProps, mapDispatchToProps)(ChatVideo)
+
+export default ChatVideo
\ No newline at end of file
diff --git a/src/chat/panel/right/component/ChatVideoOline.jsx b/src/chat/panel/right/component/ChatVideoOline.jsx
new file mode 100644
index 0000000..17fcf40
--- /dev/null
+++ b/src/chat/panel/right/component/ChatVideoOline.jsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import {
+ Tooltip,
+ Button,
+ Drawer
+} from 'antd';
+
+import {
+ VideoCameraOutlined,
+ PoweroffOutlined
+} from '@ant-design/icons';
+
+import * as Constant from '../../../common/constant/Constant'
+import { connect } from 'react-redux'
+import { actions } from '../../../redux/module/panel'
+
+let localPeer = null;
+class ChatVideoOline extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+
+ }
+ }
+
+ componentDidMount() {
+ localPeer = new RTCPeerConnection();
+ let peer = {
+ ...this.props.peer,
+ localPeer: localPeer
+ }
+ this.props.setPeer(peer);
+ }
+ /**
+ * 开启视频电话
+ */
+ startVideoOnline = () => {
+ if (!this.props.checkMediaPermisssion()) {
+ return;
+ }
+ this.webrtcConnection();
+ let preview = document.getElementById("preview");
+ this.setState({
+ onlineType: 1,
+ rtcType: 'offer'
+ })
+
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ video: true,
+ }).then((stream) => {
+ console.log(stream)
+ preview.srcObject = stream;
+ stream.getTracks().forEach(track => {
+ this.props.peer.localPeer.addTrack(track, stream);
+ });
+
+ // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行offer创建。不然不能获取到流,从而不能播放视频。
+ this.props.peer.localPeer.createOffer()
+ .then(offer => {
+ this.props.peer.localPeer.setLocalDescription(offer);
+ let data = {
+ contentType: Constant.VIDEO_ONLINE,
+ content: JSON.stringify(offer),
+ type: Constant.MESSAGE_TRANS_TYPE,
+ }
+ this.props.sendMessage(data);
+ });
+ });
+
+ this.setState({
+ mediaPanelDrawerVisible: true
+ })
+ }
+
+ /**
+ * webrtc 绑定事件
+ */
+ webrtcConnection = () => {
+
+
+ /**
+ * 对等方收到ice信息后,通过调用 addIceCandidate 将接收的候选者信息传递给浏览器的ICE代理。
+ * @param {候选人信息} e
+ */
+ localPeer.onicecandidate = (e) => {
+ console.log(e)
+ if (e.candidate) {
+ // rtcType参数默认是对端值为answer,如果是发起端,会将值设置为offer
+ let candidate = {
+ type: 'offer_ice',
+ iceCandidate: e.candidate
+ }
+ let message = {
+ content: JSON.stringify(candidate),
+ type: Constant.MESSAGE_TRANS_TYPE,
+ }
+ this.props.sendMessage(message);
+ }
+
+ };
+
+ /**
+ * 当连接成功后,从里面获取语音视频流
+ * @param {包含语音视频流} e
+ */
+ localPeer.ontrack = (e) => {
+ if (e && e.streams) {
+ let remoteVideo = document.getElementById("remoteVideo");
+ remoteVideo.srcObject = e.streams[0];
+ }
+ };
+ }
+
+ /**
+ * 停止视频电话,屏幕共享
+ */
+ stopVideoOnline = () => {
+ let preview = document.getElementById("preview");
+ if (preview && preview.srcObject && preview.srcObject.getTracks()) {
+ preview.srcObject.getTracks().forEach((track) => track.stop());
+ }
+
+ let remoteVideo = document.getElementById("remoteVideo");
+ if (remoteVideo && remoteVideo.srcObject && remoteVideo.srcObject.getTracks()) {
+ remoteVideo.srcObject.getTracks().forEach((track) => track.stop());
+ }
+ }
+
+ render() {
+ const { chooseUser } = this.props;
+ return (
+ <>
+
+ } disabled={chooseUser.toUser === ''}
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+
+ >
+ );
+ }
+}
+
+
+function mapStateToProps(state) {
+ return {
+ chooseUser: state.panelReducer.chooseUser,
+ socket: state.panelReducer.socket,
+ peer: state.panelReducer.peer,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setMedia: (data) => dispatch(actions.setMedia(data)),
+ setPeer: (data) => dispatch(actions.setPeer(data)),
+ }
+}
+
+ChatVideoOline = connect(mapStateToProps, mapDispatchToProps)(ChatVideoOline)
+
+export default ChatVideoOline
\ No newline at end of file
diff --git a/src/chat/panel/right/index.jsx b/src/chat/panel/right/index.jsx
new file mode 100644
index 0000000..65d5238
--- /dev/null
+++ b/src/chat/panel/right/index.jsx
@@ -0,0 +1,199 @@
+import React from 'react';
+
+import {
+ message,
+ Tag,
+ Tooltip,
+ Button
+} from 'antd';
+
+import {
+ SyncOutlined,
+ UngroupOutlined
+} from '@ant-design/icons';
+
+import ChatDetails from './component/ChatDetails';
+import ChatFile from './component/ChatFile';
+import ChatAudio from './component/ChatAudio';
+import ChatVideo from './component/ChatVideo';
+import ChatShareScreen from './component/ChatShareScreen';
+import ChatAudioOline from './component/ChatAudioOline';
+import ChatVideoOline from './component/ChatVideoOline';
+import ChatEdit from './component/ChatEdit';
+
+import moment from 'moment';
+import protobuf from '../../proto/proto';
+import { connect } from 'react-redux';
+import { actions } from '../../redux/module/panel';
+
+class RightIndex extends React.Component {
+
+ /**
+ * 将发送的消息追加到消息面板
+ * @param {消息内容,包括图片视频消息标签} content
+ */
+ appendMessage = (content) => {
+ let messageList = [
+ ...this.props.messageList,
+ {
+ author: localStorage.username,
+ avatar: this.props.user.avatar,
+ content: {content}
,
+ datetime: moment().fromNow(),
+ },
+ ];
+ this.props.setMessageList(messageList);
+ }
+
+ /**
+ * 本地上传后,将图片追加到聊天框
+ * @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.appendMessage(
);
+ }
+
+ /**
+ * 发送消息
+ * @param {消息内容} messageData
+ */
+ sendMessage = (messageData) => {
+ let data = {
+ ...messageData,
+ messageType: this.props.chooseUser.messageType, // 消息类型,1.单聊 2.群聊
+ fromUsername: localStorage.username,
+ from: localStorage.uuid,
+ to: this.props.chooseUser.toUser,
+ }
+ let message = protobuf.lookup("protocol.Message")
+ const messagePB = message.create(data)
+
+ let socket = this.props.socket;
+ if (null == socket) {
+ message.error("socket未连接");
+ return;
+ }
+ socket.send(message.encode(messagePB).finish())
+ }
+
+ /**
+ * 检查媒体权限是否开启
+ * @returns 媒体权限是否开启
+ */
+ checkMediaPermisssion = () => {
+ navigator.getUserMedia = navigator.getUserMedia ||
+ navigator.webkitGetUserMedia ||
+ navigator.mozGetUserMedia ||
+ navigator.msGetUserMedia; //获取媒体对象(这里指摄像头)
+ if (!navigator || !navigator.mediaDevices) {
+ message.error("获取摄像头权限失败!")
+ return false;
+ }
+ return true;
+ }
+
+ showMediaPanel = () => {
+ let media = {
+ ...this.props.media,
+ showMediaPanel: true,
+ }
+ this.props.setMedia(media)
+ }
+
+ render() {
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ } color="processing" hidden={!this.props.media.isRecord}>
+ 录制中
+
+
+
+
+ >
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ user: state.userInfoReducer.user,
+ media: state.panelReducer.media,
+ chooseUser: state.panelReducer.chooseUser,
+ messageList: state.panelReducer.messageList,
+ socket: state.panelReducer.socket,
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ setMessageList: (data) => dispatch(actions.setMessageList(data)),
+ setMedia: (data) => dispatch(actions.setMedia(data)),
+ }
+}
+
+RightIndex = connect(mapStateToProps, mapDispatchToProps)(RightIndex)
+
+export default RightIndex
\ No newline at end of file
diff --git a/src/chat/redux/module/index.jsx b/src/chat/redux/module/index.jsx
index 1166c74..b9748c0 100644
--- a/src/chat/redux/module/index.jsx
+++ b/src/chat/redux/module/index.jsx
@@ -1,9 +1,11 @@
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import userInfoReducer from './userInfo'
+import panelReducer from './panel'
const reducer = combineReducers({
- userInfoReducer
+ userInfoReducer,
+ panelReducer
});
export default createStore(reducer, applyMiddleware(thunk));
\ No newline at end of file
diff --git a/src/chat/redux/module/panel.jsx b/src/chat/redux/module/panel.jsx
new file mode 100644
index 0000000..774aa2b
--- /dev/null
+++ b/src/chat/redux/module/panel.jsx
@@ -0,0 +1,76 @@
+const initialState = {
+ userList: [],
+ chooseUser: {
+ toUser: '', // 接收方uuid
+ toUsername: '', // 接收方用户名
+ messageType: 1, // 消息类型,1.单聊 2.群聊
+ avatar: '', // 接收方的头像
+ },
+ messageList: [],
+ socket: null,
+ media: {
+ isRecord: false,
+ showMediaPanel: false,
+ },
+ peer: {
+ localPeer: null, // WebRTC peer 发起端
+ remotePeer: null, // WebRTC peer 接收端
+ }
+}
+
+export const types = {
+ USER_LIST_SET: 'USER_LIST/SET',
+ CHOOSE_USER_SET: 'CHOOSE_USER/SET',
+ MESSAGE_LIST_SET: 'MESSAGE_LIST/SET',
+ SOCKET_SET: 'SOCKET/SET',
+ MEDIA_SET: 'MEDIA/SET',
+ PEER_SET: 'PEER/SET',
+}
+
+export const actions = {
+ setUserList: (userList) => ({
+ type: types.USER_LIST_SET,
+ userList: userList
+ }),
+ setChooseUser: (chooseUser) => ({
+ type: types.CHOOSE_USER_SET,
+ chooseUser: chooseUser
+ }),
+ setMessageList: (messageList) => ({
+ type: types.MESSAGE_LIST_SET,
+ messageList: messageList
+ }),
+ setSocket: (socket) => ({
+ type: types.SOCKET_SET,
+ socket: socket
+ }),
+ setMedia: (media) => ({
+ type: types.MEDIA_SET,
+ media: media
+ }),
+ setPeer: (peer) => ({
+ type: types.PEER_SET,
+ peer: peer
+ }),
+}
+
+const PanelReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case types.USER_LIST_SET:
+ return { ...state, userList: action.userList }
+ case types.CHOOSE_USER_SET:
+ return { ...state, chooseUser: action.chooseUser }
+ case types.MESSAGE_LIST_SET:
+ return { ...state, messageList: action.messageList }
+ case types.SOCKET_SET:
+ return { ...state, socket: action.socket }
+ case types.MEDIA_SET:
+ return { ...state, media: action.media }
+ case types.PEER_SET:
+ return { ...state, peer: action.peer }
+ default:
+ return state
+ }
+}
+
+export default PanelReducer